263 lines · 7.7 KB · TypeScript
RawDownload
import { assert, describe, it } from "@effect/vitest";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Option from "effect/Option"; import {  aggregateProcessResourceHistory,  collectMonitoredSamples,} from "./ProcessResourceMonitor.ts"; describe("ProcessResourceMonitor", () => {  it.effect("samples the server root process and descendants", () =>    Effect.sync(() => {      const sampledAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z");      const samples = collectMonitoredSamples({        serverPid: 100,        sampledAt,        sampledAtMs: DateTime.toEpochMillis(sampledAt),        rows: [          {            pid: 100,            ppid: 1,            pgid: 100,            status: "S",            cpuPercent: 2,            rssBytes: 1_000,            elapsed: "01:00",            command: "t3 server",          },          {            pid: 101,            ppid: 100,            pgid: 100,            status: "S",            cpuPercent: 10,            rssBytes: 2_000,            elapsed: "00:20",            command: "codex app-server",          },          {            pid: 102,            ppid: 101,            pgid: 100,            status: "R",            cpuPercent: 50,            rssBytes: 3_000,            elapsed: "00:05",            command: "rg needle",          },          {            pid: 200,            ppid: 1,            pgid: 200,            status: "R",            cpuPercent: 99,            rssBytes: 9_000,            elapsed: "00:05",            command: "unrelated",          },        ],      });       assert.deepStrictEqual(        samples.map((sample) => sample.pid),        [100, 101, 102],      );      assert.deepStrictEqual(        samples.map((sample) => sample.depth),        [0, 1, 2],      );      assert.equal(samples[0]?.isServerRoot, true);      assert.equal(samples[1]?.isServerRoot, false);    }),  );   it.effect("rolls samples up by process and CPU time", () =>    Effect.sync(() => {      const firstAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z");      const secondAt = DateTime.makeUnsafe("2026-05-05T10:00:05.000Z");      const samples = [        ...collectMonitoredSamples({          serverPid: 100,          sampledAt: firstAt,          sampledAtMs: DateTime.toEpochMillis(firstAt),          rows: [            {              pid: 100,              ppid: 1,              pgid: 100,              status: "S",              cpuPercent: 10,              rssBytes: 1_000,              elapsed: "01:00",              command: "t3 server",            },          ],        }),        ...collectMonitoredSamples({          serverPid: 100,          sampledAt: secondAt,          sampledAtMs: DateTime.toEpochMillis(secondAt),          rows: [            {              pid: 100,              ppid: 1,              pgid: 100,              status: "S",              cpuPercent: 30,              rssBytes: 2_000,              elapsed: "01:05",              command: "t3 server",            },          ],        }),      ];       const result = aggregateProcessResourceHistory({        samples,        readAt: secondAt,        readAtMs: DateTime.toEpochMillis(secondAt),        windowMs: Duration.toMillis(Duration.minutes(1)),        bucketMs: Duration.toMillis(Duration.seconds(10)),        lastError: Option.none(),      });       assert.equal(Option.isNone(result.error), true);      assert.equal(result.topProcesses.length, 1);      assert.equal(result.topProcesses[0]?.avgCpuPercent, 20);      assert.equal(result.topProcesses[0]?.maxCpuPercent, 30);      assert.equal(result.topProcesses[0]?.cpuSecondsApprox, 2);      assert.equal(result.totalCpuSecondsApprox, 2);      assert.equal(        result.buckets.some((bucket) => bucket.maxCpuPercent === 30),        true,      );    }),  );   it.effect("keeps a process grouped when elapsed time drifts between samples", () =>    Effect.sync(() => {      const firstAt = DateTime.makeUnsafe("2026-05-05T10:00:00.400Z");      const secondAt = DateTime.makeUnsafe("2026-05-05T10:00:05.900Z");      const samples = [        ...collectMonitoredSamples({          serverPid: 100,          sampledAt: firstAt,          sampledAtMs: DateTime.toEpochMillis(firstAt),          rows: [            {              pid: 100,              ppid: 1,              pgid: 100,              status: "S",              cpuPercent: 1,              rssBytes: 1_000,              elapsed: "01:00",              command: "t3 server",            },          ],        }),        ...collectMonitoredSamples({          serverPid: 100,          sampledAt: secondAt,          sampledAtMs: DateTime.toEpochMillis(secondAt),          rows: [            {              pid: 100,              ppid: 1,              pgid: 100,              status: "S",              cpuPercent: 2,              rssBytes: 2_000,              elapsed: "01:06",              command: "t3 server",            },          ],        }),      ];       const result = aggregateProcessResourceHistory({        samples,        readAt: secondAt,        readAtMs: DateTime.toEpochMillis(secondAt),        windowMs: Duration.toMillis(Duration.minutes(1)),        bucketMs: Duration.toMillis(Duration.seconds(10)),        lastError: Option.none(),      });       assert.equal(result.topProcesses.length, 1);      assert.equal(result.topProcesses[0]?.isServerRoot, true);      assert.equal(result.topProcesses[0]?.sampleCount, 2);      assert.equal(result.topProcesses[0]?.maxRssBytes, 2_000);    }),  );   it.effect("returns all process summaries in the selected window", () =>    Effect.sync(() => {      const sampledAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z");      const samples = collectMonitoredSamples({        serverPid: 100,        sampledAt,        sampledAtMs: DateTime.toEpochMillis(sampledAt),        rows: [          {            pid: 100,            ppid: 1,            pgid: 100,            status: "S",            cpuPercent: 1,            rssBytes: 1_000,            elapsed: "01:00",            command: "t3 server",          },          ...Array.from({ length: 35 }, (_, index) => ({            pid: 200 + index,            ppid: index === 0 ? 100 : 199 + index,            pgid: 100,            status: "S",            cpuPercent: 35 - index,            rssBytes: 2_000 + index,            elapsed: "00:10",            command: `worker ${index}`,          })),        ],      });       const result = aggregateProcessResourceHistory({        samples,        readAt: sampledAt,        readAtMs: DateTime.toEpochMillis(sampledAt),        windowMs: Duration.toMillis(Duration.minutes(1)),        bucketMs: Duration.toMillis(Duration.seconds(10)),        lastError: Option.none(),      });       assert.equal(result.topProcesses.length, 36);      assert.equal(        result.topProcesses.some((process) => process.command === "worker 34"),        true,      );    }),  );   it.effect("maps the latest sampling error option into the response", () =>    Effect.sync(() => {      const readAt = DateTime.makeUnsafe("2026-05-05T10:00:00.000Z");      const result = aggregateProcessResourceHistory({        samples: [],        readAt,        readAtMs: DateTime.toEpochMillis(readAt),        windowMs: Duration.toMillis(Duration.minutes(1)),        bucketMs: Duration.toMillis(Duration.seconds(10)),        lastError: Option.some("ps failed"),      });       if (Option.isNone(result.error)) {        assert.fail("Expected response error");      }      assert.deepStrictEqual(result.error.value, { message: "ps failed" });    }),  );});