Files
stack/packages/supervisor/tests/pressure.test.mjs

134 lines
7.0 KiB
JavaScript

// Parser and effective-limit tests. Every input is fixture text shaped like
// the kernel's files; nothing reads /proc, /sys or a cgroup.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
SupervisorError, parseLoadavg, parseMeminfo, parsePsi, parseCpuList, parseCpuMax, parseMemoryMax,
parseMemoryCurrent, parseMemoryEvents, parseVmstat, effectiveCpus, effectiveMemory,
} from "../src/index.mjs";
const GiB = 1024 ** 3;
const isParse = (re) => (e) => e instanceof SupervisorError && e.kind === "parse" && re.test(e.message);
const MEMINFO = [
"MemTotal: 32768000 kB",
"MemFree: 1024000 kB",
"MemAvailable: 8192000 kB",
"SwapTotal: 4096000 kB",
"SwapFree: 4000000 kB",
"HugePages_Total: 0",
"Hugepagesize: 2048 kB",
"",
].join("\n");
test("loadavg parses the three loads", () => {
assert.deepEqual(parseLoadavg("0.52 1.58 12.00 3/1234 56789\n"), { load1: 0.52, load5: 1.58, load15: 12 });
});
test("a malformed loadavg throws", () => {
for (const bad of ["", "0.52 0.58 0.59\n", "0.52 0.58 0.59 1/2\n", "x 0.58 0.59 1/2 3\n", "0.52 0.58 0.59 1/2 3 4\n", 42]) {
assert.throws(() => parseLoadavg(bad), isParse(/loadavg/), JSON.stringify(bad));
}
});
test("meminfo returns bytes and keeps unitless counts", () => {
assert.deepEqual(parseMeminfo(MEMINFO), {
memTotal: 32768000 * 1024, memAvailable: 8192000 * 1024, swapTotal: 4096000 * 1024, swapFree: 4000000 * 1024,
});
});
test("meminfo without MemAvailable, with a repeated key or a bad line throws", () => {
assert.throws(() => parseMeminfo("MemTotal: 1 kB\n"), isParse(/no MemAvailable/));
assert.throws(() => parseMeminfo(MEMINFO + "MemTotal: 1 kB\n"), isParse(/repeated key MemTotal/));
assert.throws(() => parseMeminfo(MEMINFO + "Broken line\n"), isParse(/bad line/));
assert.throws(() => parseMeminfo(MEMINFO.replace("8192000", "-1")), isParse(/bad line/));
});
test("PSI parses some and full, and cpu may lack full", () => {
const mem = "some avg10=1.50 avg60=0.20 avg300=0.00 total=123\nfull avg10=0.75 avg60=0.10 avg300=0.00 total=45\n";
assert.deepEqual(parsePsi(mem, "memory"), {
some: { avg10: 1.5, avg60: 0.2, avg300: 0, total: 123 },
full: { avg10: 0.75, avg60: 0.1, avg300: 0, total: 45 },
});
assert.equal(parsePsi("some avg10=20.00 avg60=0.00 avg300=0.00 total=9\n", "cpu").full, null);
});
test("PSI without a full line for memory or io, or with a bad line, throws", () => {
const someOnly = "some avg10=0.00 avg60=0.00 avg300=0.00 total=0\n";
assert.throws(() => parsePsi(someOnly, "memory"), isParse(/no full line/));
assert.throws(() => parsePsi(someOnly, "io"), isParse(/no full line/));
assert.throws(() => parsePsi("full avg10=0.00 avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/no some line/));
assert.throws(() => parsePsi(someOnly + someOnly, "cpu"), isParse(/repeated some/));
assert.throws(() => parsePsi("some avg10=x avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/not a non-negative number/));
assert.throws(() => parsePsi(someOnly, "gpu"), isParse(/unknown resource/));
});
test("CPU lists count distinct CPUs", () => {
assert.equal(parseCpuList("0-3,6,8-9\n"), 7);
assert.equal(parseCpuList("0\n"), 1);
assert.equal(parseCpuList("0-3,2-5"), 6);
for (const bad of ["", "\n", "3-1", "0,,1", "a-b", "0-"]) assert.throws(() => parseCpuList(bad), isParse(/cpu list/), bad);
});
test("cpu.max gives the quota in CPUs, or null for max", () => {
assert.equal(parseCpuMax("max 100000\n"), null);
assert.equal(parseCpuMax("50000 100000\n"), 0.5);
assert.equal(parseCpuMax("200000 100000\n"), 2);
for (const bad of ["max", "50000 0", "-1 100000", "max max"]) assert.throws(() => parseCpuMax(bad), isParse(/cpu.max/), bad);
});
test("memory.max, memory.current and memory.events", () => {
assert.equal(parseMemoryMax("max\n"), null);
assert.equal(parseMemoryMax("4294967296\n"), 4 * GiB);
assert.throws(() => parseMemoryMax("4G\n"), isParse(/memory.max/));
assert.equal(parseMemoryCurrent("123456\n"), 123456);
assert.throws(() => parseMemoryCurrent(""), isParse(/memory.current/));
assert.deepEqual(parseMemoryEvents("low 0\nhigh 4\nmax 2\noom 1\noom_kill 1\noom_group_kill 0\n"), { oom: 1, oomKill: 1 });
assert.throws(() => parseMemoryEvents("low 0\noom 1\n"), isParse(/no oom_kill/));
assert.throws(() => parseMemoryEvents("oom_kill 1\noom_kill 2\n"), isParse(/repeated key/));
});
test("vmstat gives swap counters and the host OOM counter", () => {
assert.deepEqual(parseVmstat("nr_free_pages 100\npswpin 7\npswpout 9\noom_kill 3\n"), { pswpin: 7, pswpout: 9, oomKill: 3 });
assert.deepEqual(parseVmstat("pswpin 0\npswpout 0\n"), { pswpin: 0, pswpout: 0, oomKill: null });
assert.throws(() => parseVmstat("pswpin 0\n"), isParse(/no pswpout/));
});
test("effective CPUs fall back to the host count with no cgroup input", () => {
assert.equal(effectiveCpus({ online: "0-15\n" }), 16);
});
test("effective CPUs take the smallest of host, cpuset and every quota level", () => {
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n" }), 4);
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n", cpuMax: ["max 100000\n", "150000 100000\n"] }), 1.5);
// The outer level is the tighter one.
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["800000 100000\n", "200000 100000\n"] }), 2);
// A quota above the host count doesn't raise it.
assert.equal(effectiveCpus({ online: "0-1\n", cpuMax: ["800000 100000\n"] }), 2);
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["max 100000\n"] }), 16);
});
test("effective memory falls back to the host with no cgroup levels, or only max", () => {
const host = { limit: 32768000 * 1024, available: 8192000 * 1024 };
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO }), host);
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "max\n" }] }), host);
});
test("effective memory uses the tightest limited level", () => {
// 4 GiB limit with 3 GiB used: 1 GiB available, below the host's MemAvailable.
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n`, current: `${3 * GiB}\n` }] }),
{ limit: 4 * GiB, available: 1 * GiB });
// Nested: the inner level is loose, the outer one is nearly full.
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [
{ max: "max\n" }, { max: `${16 * GiB}\n`, current: `${2 * GiB}\n` }, { max: `${6 * GiB}\n`, current: `${5.5 * GiB}\n` },
] }), { limit: 6 * GiB, available: 0.5 * GiB });
// Usage above the limit floors at 0.
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "100\n", current: "200\n" }] }).available, 0);
// A limit above MemTotal doesn't raise the limit.
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${64 * GiB}\n`, current: "0\n" }] }).limit, 32768000 * 1024);
});
test("a limited level without memory.current throws", () => {
assert.throws(() => effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n` }] }), isParse(/missing for a limited level/));
});