166 lines
7.0 KiB
JavaScript
166 lines
7.0 KiB
JavaScript
// 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 };
|
|
}
|