Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

109 lines · 4.5 KBRaw History
1import { mkdirSync, writeFileSync } from "node:fs";
2import path from "node:path";
3import { describe, expect, it, vi } from "vitest";
4import { BUSY_BUCKETS } from "@diffinery/shared";
5import { UsageSampler, describeUsage, saturatedAt } from "../src/usage.js";
6import { tmp } from "./helpers.js";
7
8/** A fake /proc: CPU ticks (busy, idle) summed over all CPUs, memory in use and an OOM kill count. */
9function fakeProc(dir: string) {
10 mkdirSync(dir, { recursive: true });
11 return (busy: number, idle: number, usedMiB: number, oom = 0, totalMiB = 4096) => {
12 // user nice system idle iowait irq softirq steal: busy split over user and system; steal isn't busy.
13 writeFileSync(path.join(dir, "stat"), `cpu ${busy - 10} 0 10 ${idle} 0 0 0 7 0 0\ncpu0 1 2 3\n`);
14 writeFileSync(path.join(dir, "meminfo"), `MemTotal: ${totalMiB * 1024} kB\nMemFree: 1 kB\nMemAvailable: ${(totalMiB - usedMiB) * 1024} kB\n`);
15 writeFileSync(path.join(dir, "vmstat"), `pgfault 5\noom_kill ${oom}\n`);
16 };
17}
18
19describe("usage sampler", () => {
20 it("measures a short burst at its height with its default quarter-second windows", () => {
21 vi.useFakeTimers({ now: 0 });
22 try {
23 for (const [windowMs, expected] of [[undefined, 1], [2000, 0.13]] as const) {
24 const proc = path.join(tmp(), "proc");
25 const set = fakeProc(proc);
26 let busy = 10;
27 let idle = 0;
28 set(busy, idle, 500);
29 const s = new UsageSampler({ procRoot: proc, diskPath: proc, visible: 4, ...(windowMs ? { windowMs, sampleMs: 500 } : {}) });
30 s.start();
31 // 1.75 s idle, then a quarter second with one of four CPUs busy, then idle: 10 ms ticks.
32 for (let t = 0; t < 400; t++) {
33 const on = t >= 175 && t < 200;
34 busy += on ? 1 : 0;
35 idle += on ? 3 : 4;
36 set(busy, idle, 500);
37 vi.advanceTimersByTime(10);
38 }
39 const u = s.stop()!;
40 expect(u.coresMax).toBeCloseTo(expected, 1);
41 expect(u.windowSeconds).toBe((windowMs ?? 250) / 1000);
42 }
43 } finally {
44 vi.useRealTimers();
45 }
46 });
47
48 it("measures busy cores per window, peak memory and OOM kills", () => {
49 vi.useFakeTimers({ now: 0 });
50 try {
51 const proc = path.join(tmp(), "proc");
52 const set = fakeProc(proc);
53 set(0, 0, 500);
54 const s = new UsageSampler({ procRoot: proc, diskPath: proc, sampleMs: 500, windowMs: 2000, visible: 4 });
55 s.start();
56 // First 2 s: 1 of 4 CPUs busy (busy is a quarter of all ticks). Memory peaks at 1500 MiB mid-window.
57 set(100, 300, 1500);
58 vi.advanceTimersByTime(1000);
59 set(200, 600, 800);
60 vi.advanceTimersByTime(1000);
61 // Next 2 s: all 4 busy, and one OOM kill.
62 set(1000, 600, 900, 1);
63 vi.advanceTimersByTime(2000);
64 const u = s.stop()!;
65 expect(u.seconds).toBe(4);
66 expect(u.cores).toBe(4);
67 expect(u.cpuSeconds).toBeCloseTo(10, 1);
68 expect(u.coresMax).toBe(4);
69 expect(u.memoryPeak).toBe(1500 * 1024 * 1024);
70 expect(u.memory).toBe(4096 * 1024 * 1024);
71 expect(u.oomKills).toBe(1);
72 expect(u.saturated).toBe(0.5);
73 expect(u.busySeconds).toHaveLength(BUSY_BUCKETS.length);
74 expect(u.busySeconds[BUSY_BUCKETS.indexOf(1)]).toBe(2);
75 expect(u.busySeconds[BUSY_BUCKETS.indexOf(4)]).toBe(2);
76 expect(describeUsage(u)).toBe("Resources: 2.5 of 4 vCPUs busy on average (4.0 at the 95th percentile), peak memory 1.5 GiB of 4.0 GiB, peak disk " + `${(u.diskPeak / 1e9).toFixed(1)} GB of ${(u.disk / 1e9).toFixed(1)} GB, 1 process(es) killed for want of memory`);
77 } finally {
78 vi.useRealTimers();
79 }
80 });
81
82 it("counts busy cores against a fractional share of CPU", () => {
83 vi.useFakeTimers({ now: 0 });
84 try {
85 const proc = path.join(tmp(), "proc");
86 const set = fakeProc(proc);
87 set(0, 0, 100);
88 // One CPU visible, a quarter of it allotted: a quarter of the ticks busy is all it has.
89 const s = new UsageSampler({ procRoot: proc, diskPath: proc, windowMs: 2000, visible: 1, cores: 0.25 });
90 s.start();
91 set(50, 150, 100);
92 vi.advanceTimersByTime(2000);
93 const u = s.stop()!;
94 expect(u.cores).toBe(0.25);
95 expect(u.coresMax).toBe(0.25);
96 expect(u.saturated).toBe(1);
97 expect(saturatedAt(0.25)).toBeCloseTo(0.225);
98 expect(saturatedAt(4)).toBe(3.75);
99 } finally {
100 vi.useRealTimers();
101 }
102 });
103
104 it("measures nothing where /proc can't be read", () => {
105 const s = new UsageSampler({ procRoot: path.join(tmp(), "missing") });
106 s.start();
107 expect(s.stop()).toBeNull();
108 });
109});