231 lines · 6.6 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import * as DateTime from "effect/DateTime";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",          },        ],      });       expect(samples.map((sample) => sample.pid)).toEqual([100, 101, 102]);      expect(samples.map((sample) => sample.depth)).toEqual([0, 1, 2]);      expect(samples[0]?.isServerRoot).toBe(true);      expect(samples[1]?.isServerRoot).toBe(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: 60_000,        bucketMs: 10_000,        lastError: null,      });       expect(Option.isNone(result.error)).toBe(true);      expect(result.topProcesses).toHaveLength(1);      expect(result.topProcesses[0]?.avgCpuPercent).toBe(20);      expect(result.topProcesses[0]?.maxCpuPercent).toBe(30);      expect(result.topProcesses[0]?.cpuSecondsApprox).toBe(2);      expect(result.totalCpuSecondsApprox).toBe(2);      expect(result.buckets.some((bucket) => bucket.maxCpuPercent === 30)).toBe(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: 60_000,        bucketMs: 10_000,        lastError: null,      });       expect(result.topProcesses).toHaveLength(1);      expect(result.topProcesses[0]?.isServerRoot).toBe(true);      expect(result.topProcesses[0]?.sampleCount).toBe(2);      expect(result.topProcesses[0]?.maxRssBytes).toBe(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: 60_000,        bucketMs: 10_000,        lastError: null,      });       expect(result.topProcesses).toHaveLength(36);      expect(result.topProcesses.some((process) => process.command === "worker 34")).toBe(true);    }),  );});