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.

81 lines · 3.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 busy cores per window, peak memory and OOM kills", () => {
21 vi.useFakeTimers({ now: 0 });
22 try {
23 const proc = path.join(tmp(), "proc");
24 const set = fakeProc(proc);
25 set(0, 0, 500);
26 const s = new UsageSampler({ procRoot: proc, diskPath: proc, sampleMs: 500, windowMs: 2000, visible: 4 });
27 s.start();
28 // First 2 s: 1 of 4 CPUs busy (busy is a quarter of all ticks). Memory peaks at 1500 MiB mid-window.
29 set(100, 300, 1500);
30 vi.advanceTimersByTime(1000);
31 set(200, 600, 800);
32 vi.advanceTimersByTime(1000);
33 // Next 2 s: all 4 busy, and one OOM kill.
34 set(1000, 600, 900, 1);
35 vi.advanceTimersByTime(2000);
36 const u = s.stop()!;
37 expect(u.seconds).toBe(4);
38 expect(u.cores).toBe(4);
39 expect(u.cpuSeconds).toBeCloseTo(10, 1);
40 expect(u.coresMax).toBe(4);
41 expect(u.memoryPeak).toBe(1500 * 1024 * 1024);
42 expect(u.memory).toBe(4096 * 1024 * 1024);
43 expect(u.oomKills).toBe(1);
44 expect(u.saturated).toBe(0.5);
45 expect(u.busySeconds).toHaveLength(BUSY_BUCKETS.length);
46 expect(u.busySeconds[BUSY_BUCKETS.indexOf(1)]).toBe(2);
47 expect(u.busySeconds[BUSY_BUCKETS.indexOf(4)]).toBe(2);
48 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`);
49 } finally {
50 vi.useRealTimers();
51 }
52 });
53
54 it("counts busy cores against a fractional share of CPU", () => {
55 vi.useFakeTimers({ now: 0 });
56 try {
57 const proc = path.join(tmp(), "proc");
58 const set = fakeProc(proc);
59 set(0, 0, 100);
60 // One CPU visible, a quarter of it allotted: a quarter of the ticks busy is all it has.
61 const s = new UsageSampler({ procRoot: proc, diskPath: proc, windowMs: 2000, visible: 1, cores: 0.25 });
62 s.start();
63 set(50, 150, 100);
64 vi.advanceTimersByTime(2000);
65 const u = s.stop()!;
66 expect(u.cores).toBe(0.25);
67 expect(u.coresMax).toBe(0.25);
68 expect(u.saturated).toBe(1);
69 expect(saturatedAt(0.25)).toBeCloseTo(0.225);
70 expect(saturatedAt(4)).toBe(3.75);
71 } finally {
72 vi.useRealTimers();
73 }
74 });
75
76 it("measures nothing where /proc can't be read", () => {
77 const s = new UsageSampler({ procRoot: path.join(tmp(), "missing") });
78 s.start();
79 expect(s.stop()).toBeNull();
80 });
81});