From e5b88781d97f4dbe73313d925ee4ba07796814e5 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:33:59 +0000 Subject: [PATCH] Worker dashboard: scheduling classes on the cards (kind, nice, cores), gate-queued marker, wait and gate-pending files The slot line now ends "; kind= nice= cores=; agent=" (build box scheduling, 69034dc0): lib.mjs lifts the three into their own fields and keeps the bare label; the old line shape still derives its kind. The collector reads /srv/builds/_locks/wait- and gate-pending- directly (dead pids skipped), marks a gate with priority gate and sorts it first; the page shows the class line on Now building and Queue cards, a "gate queued" pill, and the class in the closing lines. JSONL nice, cores, jobs and priority carried through. 14 tests pass. Co-Authored-By: Claude Fable 5.1 --- tools/workers/collect.mjs | 23 +++++++++++++++++------ tools/workers/lib.mjs | 23 +++++++++++++++++++---- tools/workers/page/workers.html | 8 ++++---- tools/workers/shape.js | 5 ++++- tools/workers/test/parsers.test.mjs | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/tools/workers/collect.mjs b/tools/workers/collect.mjs index 42131a8c8..fad99beb4 100644 --- a/tools/workers/collect.mjs +++ b/tools/workers/collect.mjs @@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process'; import { hostname } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, BASELINES } from './lib.mjs'; +import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, parseSlotLine, BASELINES } from './lib.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const argv = process.argv.slice(2); @@ -68,6 +68,7 @@ function slots(now) { } function procs(bootSec, clk, now) { const running = [], waiters = []; let compilers = 0; + const flockParents = new Map(); // ppid -> since, from the flock -w processes the waiters run for (const d of safeList('/proc')) { if (!/^\d+$/.test(d)) continue; const pid = Number(d); const comm = read(`/proc/${d}/comm`).trim(); @@ -82,17 +83,26 @@ function procs(bootSec, clk, now) { if (args[1] === '-w' && /^\d+$/.test(args[2] || '')) { const stat = read(`/proc/${d}/stat`); const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' '); const ppid = Number(rest[1]); const startTicks = Number(rest[19]); - const since = Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null; - const wait = read(join(LOCKS, `wait-${ppid}`)).trim(); // a waiter's own line, when the appender writes one - waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: wait || null, ...(wait ? { kind: deriveKind(parseLabelSafe(wait)) } : {}) }); + flockParents.set(ppid, Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null); } } } - // the innermost cargo per worktree: a `cargo build` spawns no nested cargo, but a `cargo test` runs test binaries, not cargo + // the waiters' own lines: wait- (a queued build) and gate-pending- (a gate that takes the next free slot + // ahead of queued suites and benches), written by remote-run.sh in the slot-file format and removed on take or give-up; + // a file whose pid is gone is a leftover, not a waiter + const seen = new Set(); + for (const f of safeList(LOCKS)) { + const m = /^(wait|gate-pending)-(\d+)$/.exec(f); if (!m) continue; + const pid = Number(m[2]); if (!alive(pid) || seen.has(pid)) continue; seen.add(pid); + const line = parseSlotLine(read(join(LOCKS, f)).split('\n')[0], { now }); + const since = (line && line.since) || flockParents.get(pid) || null; + waiters.push({ pid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: line ? line.label : null, kind: line ? deriveKind(line) : null, agent: line ? line.agent : null, worktree: line ? line.worktree : null, crate: line ? line.crate : null, command: line ? line.command : null, nice: line ? line.nice : null, cores: line ? line.cores : null, priority: m[1] === 'gate-pending' ? 'gate' : 'normal' }); + } + for (const [ppid, since] of flockParents) if (!seen.has(ppid)) waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: null, priority: 'normal' }); + waiters.sort((a, b) => (a.priority === 'gate' ? 0 : 1) - (b.priority === 'gate' ? 0 : 1) || String(a.since || '').localeCompare(String(b.since || ''))); running.sort((a, b) => String(a.started_at).localeCompare(String(b.started_at))); return { running, waiters, compilers }; } -function parseLabelSafe(line) { const r = /: (.*)$/.exec(line); return { command: r ? r[1] : line }; } function sccache() { const bin = existsSync('/home/build/.cargo/bin/sccache') ? '/home/build/.cargo/bin/sccache' : 'sccache'; @@ -121,6 +131,7 @@ export function collect() { for (const r of p.running) { const h = s.held.find(h => h.worktree === r.worktree && (!h.crate || h.crate === r.crate || !r.crate)); r.slot = h ? h.slot : null; r.agent = h ? h.agent : null; r.queued_at = h ? h.since : null; r.waited_s = h ? h.waited_s : null; + if (h && h.class) r.kind = h.class; r.nice = h ? h.nice : null; r.cores = h ? h.cores : null; } const logText = read(LOG); const recent = parseBuildsJsonl(logText, { limit: 200 }); diff --git a/tools/workers/lib.mjs b/tools/workers/lib.mjs index 71f4ad550..6933e9148 100644 --- a/tools/workers/lib.mjs +++ b/tools/workers/lib.mjs @@ -65,15 +65,28 @@ export function parseSlotLine(line, { day, now } = {}) { const d = day || new Date(nowMs).toISOString().slice(0, 10); let since = Date.parse(`${d}T${r[2]}:${r[3]}:${r[4]}Z`); if (since > nowMs + 60_000) since -= 86_400_000; - const label = r[6].trim(); - const agent = (/;\s*agent=([^;]+)\s*$/.exec(label) || [])[1] || null; - return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: agent ? label.replace(/;\s*agent=[^;]+\s*$/, '').trim() : label, agent, ...splitLabel(label) }; + const parts = splitLabelParts(r[6]); const sl = splitLabel(parts.label); + return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: parts.label, agent: parts.agent, class: parts.class, nice: parts.nice, cores: parts.cores, ...sl, kind: parts.class || deriveKind({ command: sl.command, crate: sl.crate, target: sl.target }) }; +} + +/// The label's trailing segments (7 October 2026, build box scheduling classes): "