// Parsers shared by tools/workers/collect.mjs (on igneum-build-1) and tools/workers/push.mjs (on the Mac). Pure functions // over text the machine gives us (/proc files, lock files, the builds JSONL), so node --test can run them against the // fixtures in tools/workers/fixtures without a box. Every reader here takes the machine's own facts, never a reported number. /// /proc/stat -> { total: [{user, nice, system, idle, iowait, irq, softirq, steal}], cores: [...] } (jiffies). export function parseProcStat(text) { const cores = []; let total = null; for (const line of String(text).split('\n')) { if (!line.startsWith('cpu')) continue; const f = line.trim().split(/\s+/); const n = f.slice(1, 9).map(Number); const row = { user: n[0], nice: n[1], system: n[2], idle: n[3], iowait: n[4] || 0, irq: n[5] || 0, softirq: n[6] || 0, steal: n[7] || 0 }; if (f[0] === 'cpu') total = row; else cores.push(row); } return { total, cores }; } /// Busy percentage between two /proc/stat samples: 0 to 100 for the whole box and per core, in core order. export function cpuBusy(before, after) { const pct = (a, b) => { if (!a || !b) return 0; const sum = r => r.user + r.nice + r.system + r.idle + r.iowait + r.irq + r.softirq + r.steal; const idle = r => r.idle + r.iowait; const dt = sum(b) - sum(a); const di = idle(b) - idle(a); if (dt <= 0) return 0; return Math.max(0, Math.min(100, Math.round((1 - di / dt) * 1000) / 10)); }; const n = Math.min(before.cores.length, after.cores.length); const per = []; for (let i = 0; i < n; i++) per.push(pct(before.cores[i], after.cores[i])); return { busy_pct: pct(before.total, after.total), per_core: per, cores_busy: per.filter(p => p >= 50).length }; } /// /proc/meminfo -> kB numbers and a used percentage (used = total minus available, what a build can still take). export function parseMeminfo(text) { const m = {}; for (const line of String(text).split('\n')) { const r = /^(\w+):\s+(\d+)/.exec(line); if (r) m[r[1]] = Number(r[2]); } const total = m.MemTotal || 0, avail = m.MemAvailable ?? m.MemFree ?? 0; return { total_kb: total, available_kb: avail, used_kb: Math.max(0, total - avail), used_pct: total ? Math.round(((total - avail) / total) * 1000) / 10 : 0, swap_total_kb: m.SwapTotal || 0, swap_free_kb: m.SwapFree || 0 }; } /// /proc/net/dev -> [{ iface, rx_bytes, tx_bytes }] without lo. export function parseNetDev(text) { const out = []; for (const line of String(text).split('\n').slice(2)) { const r = /^\s*([^:\s]+):\s*(.*)$/.exec(line); if (!r) continue; const f = r[2].trim().split(/\s+/).map(Number); if (r[1] === 'lo') continue; out.push({ iface: r[1], rx_bytes: f[0] || 0, tx_bytes: f[8] || 0 }); } return out; } /// /proc/loadavg -> { load: [1, 5, 15], running, threads }. export function parseLoadavg(text) { const f = String(text).trim().split(/\s+/); const rq = (f[3] || '0/0').split('/'); return { load: [Number(f[0]) || 0, Number(f[1]) || 0, Number(f[2]) || 0], running: Number(rq[0]) || 0, threads: Number(rq[1]) || 0 }; } /// The slot-file line both with-lock.sh and bs_remote_run write: "pid N since HH:MM:SSZ waited S s: