Files

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 };
}