Public: anyone can read this; the crew can change it.

Boards, step 5a: readMachine(root) turns cgroup and /proc files into one CPU and memory point #2370

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2343-boards-step-5a-readmachine-root-turns-cg → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/2343-boards-step-5a-readmachine-root-turns-cg was merged into main as 03eb696.

Review

Sign in to review this change.
2 files changed +111 −0
src/boards/machine.ts added +51
@@ -0,0 +1,51 @@
1 /**
2 * The machine now, as one point: the pod's CPU and memory from its cgroup
3 * v2 (`cpu.stat`, `memory.current`) and, when readable, the node's memory
4 * (`meminfo`) and 1-min load (`loadavg`). `root` is the filesystem root
5 * (`/` in production), so a test points it at a temp dir holding
6 * `sys/fs/cgroup/…` and `proc/…`. Pure: no timer, no storage, never throws.
7 */
8 import { readFileSync } from "node:fs";
9 import { join } from "node:path";
10 import { parseCpuStat, parseLoadavg, parseMeminfo } from "./sampler.ts";
11
12 /** Cumulative counters and gauges at one instant; each is null when its file is not readable. */
13 export type MachinePoint = {
14 /** Pod CPU, µs since the cgroup began: a rate needs two points. */
15 cpuUsec: number | null;
16 /** Pod memory, bytes. */
17 memoryBytes: number | null;
18 /** Node memory in use, bytes. */
19 nodeMemoryBytes: number | null;
20 /** Node 1-min load average. */
21 load: number | null;
22 };
23
24 export type Machine = { ok: true; point: MachinePoint } | { ok: false; reason: string };
25
26 function read(path: string): string | null {
27 try {
28 return readFileSync(path, "utf8");
29 } catch {
30 return null;
31 }
32 }
33
34 /** A point from whatever is readable under `root`, or the reason nothing is. */
35 export function readMachine(root: string): Machine {
36 try {
37 const cgroup = join(root, "sys/fs/cgroup");
38 const proc = join(root, "proc");

This inline digits-only parse of memory.current belongs next to parseCpuStat in sampler.ts, so readCounters can share it.

39 const mem = read(join(cgroup, "memory.current"));
40 const point: MachinePoint = {
41 cpuUsec: parseCpuStat(read(join(cgroup, "cpu.stat"))),
42 memoryBytes: mem !== null && /^\d+\s*$/.test(mem) ? Number(mem) : null,
43 nodeMemoryBytes: parseMeminfo(read(join(proc, "meminfo"))),
44 load: parseLoadavg(read(join(proc, "loadavg"))),
45 };
46 if (Object.values(point).every((v) => v === null)) return { ok: false, reason: "no cgroup or /proc file is readable here" };
47 return { ok: true, point };
48 } catch {
49 return { ok: false, reason: "the machine could not be read" };
50 }
51 }
test/boards-machine.test.ts added +60
@@ -0,0 +1,60 @@
1 /**
2 * readMachine(root): fake cgroup and /proc files under a temp dir give the
3 * expected point; what is missing is null; nothing readable gives a reason.
4 */
5 import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6 import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7 import { tmpdir } from "node:os";
8 import { join } from "node:path";
9 import { readMachine } from "../src/boards/machine.ts";
10
11 let root: string;
12 beforeEach(() => {
13 root = mkdtempSync(join(tmpdir(), "machine-"));
14 });
15 afterEach(() => rmSync(root, { recursive: true, force: true }));
16
17 const put = (rel: string, text: string) => {
18 const p = join(root, rel);
19 mkdirSync(join(p, ".."), { recursive: true });
20 writeFileSync(p, text);
21 };
22
23 describe("readMachine", () => {
24 test("turns cgroup and /proc files into one point", () => {
25 put("sys/fs/cgroup/cpu.stat", "usage_usec 123456\nuser_usec 100\n");
26 put("sys/fs/cgroup/memory.current", "5242880\n");
27 put("proc/meminfo", "MemTotal: 1000 kB\nMemFree: 10 kB\nMemAvailable: 400 kB\n");
28 put("proc/loadavg", "0.52 0.40 0.30 1/200 999\n");
29 expect(readMachine(root)).toEqual({
30 ok: true,
31 point: { cpuUsec: 123456, memoryBytes: 5242880, nodeMemoryBytes: 600 * 1024, load: 0.52 },
32 });
33 });
34
35 test("the cgroup alone is enough; the /proc fields are null", () => {
36 put("sys/fs/cgroup/cpu.stat", "usage_usec 7\n");
37 put("sys/fs/cgroup/memory.current", "42\n");
38 expect(readMachine(root)).toEqual({
39 ok: true,
40 point: { cpuUsec: 7, memoryBytes: 42, nodeMemoryBytes: null, load: null },
41 });
42 });
43
44 test("a garbled file is null, not a throw", () => {
45 put("sys/fs/cgroup/memory.current", "max\n");
46 put("proc/loadavg", "1.5 1 1 1/1 1\n");
47 const m = readMachine(root);
48 expect(m.ok && m.point).toEqual({ cpuUsec: null, memoryBytes: null, nodeMemoryBytes: null, load: 1.5 });
49 });
50
51 test("a missing directory gives the reason", () => {
52 const m = readMachine(join(root, "nope"));
53 expect(m.ok).toBe(false);
54 expect(!m.ok && m.reason).toContain("readable");
55 });
56
57 test("a root with nothing in it gives the reason", () => {
58 expect(readMachine(root).ok).toBe(false);
59 });
60 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs