// Parsers for the host pressure inputs Q198 D4 names, and the effective CPU // count and memory limit they give. Every function takes file text the caller // already read and returns numbers. Nothing here opens a file. Text that isn't // the shape the kernel writes throws a SupervisorError of kind "parse"; the // caller turns that into a missing signal. import { SupervisorError } from "./errors.mjs"; const fail = (what, why) => { throw new SupervisorError(`${what}: ${why}`, "parse"); }; function text(what, value) { if (typeof value !== "string") fail(what, "expected file text"); return value.endsWith("\n") ? value.slice(0, -1) : value; } function integer(what, value) { if (!/^\d+$/.test(value)) fail(what, `not a non-negative integer: ${JSON.stringify(value)}`); return Number(value); } function decimal(what, value) { if (!/^\d+(\.\d+)?$/.test(value)) fail(what, `not a non-negative number: ${JSON.stringify(value)}`); return Number(value); } // "key value" lines (memory.events, /proc/vmstat). A repeated key throws. function keyedIntegers(what, raw) { const out = new Map(); for (const line of text(what, raw).split("\n")) { const m = /^([a-z0-9_]+) (\d+)$/.exec(line); if (!m) fail(what, `bad line ${JSON.stringify(line)}`); if (out.has(m[1])) fail(what, `repeated key ${m[1]}`); out.set(m[1], integer(what, m[2])); } return out; } // /proc/loadavg: "0.52 0.58 0.59 1/1234 56789". export function parseLoadavg(raw) { const m = /^(\d+\.\d+) (\d+\.\d+) (\d+\.\d+) (\d+)\/(\d+) (\d+)$/.exec(text("loadavg", raw)); if (!m) fail("loadavg", "not the /proc/loadavg shape"); return { load1: Number(m[1]), load5: Number(m[2]), load15: Number(m[3]) }; } // /proc/meminfo. Values are returned in bytes; lines without a unit (the // HugePages counts) are kept as plain counts. export function parseMeminfo(raw) { const fields = new Map(); for (const line of text("meminfo", raw).split("\n")) { const m = /^([A-Za-z0-9_()]+):\s+(\d+)( kB)?$/.exec(line); if (!m) fail("meminfo", `bad line ${JSON.stringify(line)}`); if (fields.has(m[1])) fail("meminfo", `repeated key ${m[1]}`); fields.set(m[1], integer("meminfo", m[2]) * (m[3] ? 1024 : 1)); } for (const key of ["MemTotal", "MemAvailable"]) if (!fields.has(key)) fail("meminfo", `no ${key}`); return { memTotal: fields.get("MemTotal"), memAvailable: fields.get("MemAvailable"), swapTotal: fields.get("SwapTotal") ?? null, swapFree: fields.get("SwapFree") ?? null, }; } // /proc/pressure/{cpu,memory,io} or a cgroup's *.pressure file. The cpu file // may lack the "full" line on older kernels; memory and io must have it. export function parsePsi(raw, resource) { if (!["cpu", "memory", "io"].includes(resource)) fail("psi", `unknown resource ${JSON.stringify(resource)}`); const what = `psi ${resource}`; const out = { some: null, full: null }; for (const line of text(what, raw).split("\n")) { const m = /^(some|full) avg10=(\S+) avg60=(\S+) avg300=(\S+) total=(\S+)$/.exec(line); if (!m) fail(what, `bad line ${JSON.stringify(line)}`); if (out[m[1]]) fail(what, `repeated ${m[1]} line`); out[m[1]] = { avg10: decimal(what, m[2]), avg60: decimal(what, m[3]), avg300: decimal(what, m[4]), total: integer(what, m[5]) }; } if (!out.some) fail(what, "no some line"); if (!out.full && resource !== "cpu") fail(what, "no full line"); return out; } // A CPU list such as /sys/devices/system/cpu/online or cpuset.cpus.effective: // "0-3,6,8-9". Returns how many distinct CPUs it names. export function parseCpuList(raw) { const body = text("cpu list", raw); if (body === "") fail("cpu list", "empty"); const cpus = new Set(); for (const part of body.split(",")) { const m = /^(\d+)(?:-(\d+))?$/.exec(part); if (!m) fail("cpu list", `bad range ${JSON.stringify(part)}`); const lo = integer("cpu list", m[1]); const hi = m[2] === undefined ? lo : integer("cpu list", m[2]); if (hi < lo) fail("cpu list", `range ${part} runs backwards`); for (let c = lo; c <= hi; c++) cpus.add(c); } return cpus.size; } // cgroup v2 cpu.max: "max 100000" (no quota) or "50000 100000". Returns the // CPUs the quota allows, or null for no quota. export function parseCpuMax(raw) { const m = /^(max|\d+) (\d+)$/.exec(text("cpu.max", raw)); if (!m) fail("cpu.max", "not the cpu.max shape"); const period = integer("cpu.max", m[2]); if (period === 0) fail("cpu.max", "period is 0"); return m[1] === "max" ? null : integer("cpu.max", m[1]) / period; } // cgroup v2 memory.max: "max" (no limit) or bytes. Returns null for "max". export function parseMemoryMax(raw) { const body = text("memory.max", raw); return body === "max" ? null : integer("memory.max", body); } // cgroup v2 memory.current: bytes. export function parseMemoryCurrent(raw) { return integer("memory.current", text("memory.current", raw)); } // cgroup v2 memory.events. oom_kill is the counter the critical row watches. export function parseMemoryEvents(raw) { const fields = keyedIntegers("memory.events", raw); if (!fields.has("oom_kill")) fail("memory.events", "no oom_kill"); return { oom: fields.get("oom") ?? null, oomKill: fields.get("oom_kill") }; } // /proc/vmstat. Swap activity is reported only (D4 has no threshold for it). // oom_kill is the host-only OOM counter; it is null on a kernel without it. export function parseVmstat(raw) { const fields = keyedIntegers("vmstat", raw); for (const key of ["pswpin", "pswpout"]) if (!fields.has(key)) fail("vmstat", `no ${key}`); return { pswpin: fields.get("pswpin"), pswpout: fields.get("pswpout"), oomKill: fields.get("oom_kill") ?? null }; } // Effective CPUs: the smallest of the host's online CPUs, the cpuset and the // quota at every cgroup level given. With no cgroup input it is the host count. // A fractional quota stays fractional. export function effectiveCpus({ online, cpuset = null, cpuMax = [] }) { let cpus = parseCpuList(online); if (cpuset !== null) cpus = Math.min(cpus, parseCpuList(cpuset)); for (const level of cpuMax) { const quota = parseCpuMax(level); if (quota !== null) cpus = Math.min(cpus, quota); } return cpus; } // Effective memory. `levels` lists the cgroup levels that apply, each // { max, current } as file text. The limit is the smallest of MemTotal and every // numeric memory.max. Available memory is the smallest of MemAvailable and // (max - current) at every limited level. memory.current counts page cache, // so this errs toward holding. With no levels it is the host's figures. export function effectiveMemory({ meminfo, levels = [] }) { const host = parseMeminfo(meminfo); let limit = host.memTotal; let available = host.memAvailable; for (const level of levels) { const max = parseMemoryMax(level.max); if (max === null) continue; if (level.current === undefined || level.current === null) fail("memory.current", "missing for a limited level"); const current = parseMemoryCurrent(level.current); limit = Math.min(limit, max); available = Math.min(available, Math.max(0, max - current)); } return { limit, available }; }