Files
UHD_Media_Tool/apps/server/test/system-resources.test.ts
T

207 lines
8.7 KiB
TypeScript

import { SystemResources } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import {
bucketSamples,
computePoint,
createProcessCpuMeter,
createResourceSampler,
parseCgroupBytes,
parseCgroupCpuMax,
parseCgroupCpuUsage,
parseDiskstats,
parseIoStat,
parseLoadavg,
parseMeminfo,
parseNetDev,
parseProcStat,
type ReadText,
readResources,
} from "../src/modules/system/resources.ts";
// 资源监控采集:/proc 与 cgroup v2 的解析、速率计算、降级与采样器(不碰 PG;读文件用注入的假实现)。
const STAT = (user: number, idle: number) => `cpu ${user} 0 100 ${idle} 50 0 0 0 0 0\ncpu0 1 2 3 4 5 6 7 8 0 0\n`;
const MEMINFO = `MemTotal: 16000000 kB
MemFree: 2000000 kB
MemAvailable: 8000000 kB
SwapTotal: 1000000 kB
SwapFree: 600000 kB
`;
const NETDEV = (rx: number, tx: number) => `Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 999999 10 0 0 0 0 0 0 999999 10 0 0 0 0 0 0
eth0: ${rx} 100 0 0 0 0 0 0 ${tx} 80 0 0 0 0 0 0
`;
const IOSTAT = (r: number, w: number) =>
`8:0 rbytes=${r} wbytes=${w} rios=10 wios=5 dbytes=0 dios=0\n259:0 rbytes=0 wbytes=0\n`;
/** 假文件系统:按路径给内容,没给的读不到 */
function fakeFs(files: Record<string, string>): ReadText {
return (path) => Promise.resolve(files[path] ?? null);
}
describe("解析", () => {
it("/proc 各文件", () => {
expect(parseProcStat(STAT(1000, 8000))).toEqual({ total: 1000 + 100 + 8000 + 50, idle: 8050 });
expect(parseProcStat("garbage")).toBeNull();
expect(parseMeminfo(MEMINFO)).toEqual({
totalBytes: 16000000 * 1024,
availableBytes: 8000000 * 1024,
swapUsedBytes: 400000 * 1024,
});
expect(parseMeminfo(null)).toBeNull();
expect(parseLoadavg("0.52 0.40 0.31 2/345 6789")).toEqual([0.52, 0.4, 0.31]);
expect(parseLoadavg("")).toBeNull();
// lo 不计
expect(parseNetDev(NETDEV(5000, 3000))).toEqual({ rx: 5000, tx: 3000 });
// 整盘才计(分区、loop 不计)
expect(
parseDiskstats(
" 8 0 sda 100 0 2000 0 50 0 4000 0 0 0 0\n 8 1 sda1 100 0 2000 0 50 0 4000 0 0 0 0\n 7 0 loop0 1 0 8 0 0 0 0 0 0 0 0\n",
),
).toEqual({ read: 2000 * 512, write: 4000 * 512 });
});
it("cgroup v2", () => {
expect(parseCgroupCpuUsage("usage_usec 123456\nuser_usec 1\n")).toBe(123456);
expect(parseCgroupCpuMax("max 100000")).toBeNull();
expect(parseCgroupCpuMax("200000 100000\n")).toBe(2);
expect(parseCgroupBytes("max\n")).toBeNull();
expect(parseCgroupBytes("1048576\n")).toBe(1048576);
expect(parseIoStat(IOSTAT(4096, 8192))).toEqual({ read: 4096, write: 8192 });
expect(parseIoStat("")).toBeNull();
});
});
describe("读数与速率", () => {
it("容器:CPU 按 cgroup 用量 / 核数,内存扣掉 inactive_file,磁盘读写取 io.stat", async () => {
const files = (usage: number, user: number, idle: number, rx: number, io: number) =>
fakeFs({
"/proc/stat": STAT(user, idle),
"/sys/fs/cgroup/cpu.stat": `usage_usec ${usage}\n`,
"/sys/fs/cgroup/cpu.max": "200000 100000",
"/proc/loadavg": "1.00 0.50 0.25 1/2 3",
"/proc/meminfo": MEMINFO,
"/sys/fs/cgroup/memory.current": String(600 * 1024 * 1024),
"/sys/fs/cgroup/memory.max": String(1024 * 1024 * 1024),
"/sys/fs/cgroup/memory.stat": `anon 1\ninactive_file ${100 * 1024 * 1024}\n`,
"/proc/net/dev": NETDEV(rx, rx / 2),
"/sys/fs/cgroup/io.stat": IOSTAT(io, io * 2),
"/proc/uptime": "3600.5 7000.1",
});
const a = await readResources(files(0, 1000, 9000, 0, 0), 0);
const b = await readResources(files(5_000_000, 2000, 10000, 10_000_000, 1_000_000), 5000);
const first = computePoint(null, a);
expect(first.sample.cpuPercent).toBeNull();
expect(first.sample.netRxBps).toBeNull();
const p = computePoint(a, b);
// 5 秒用了 5 秒 CPU,限额 2 核 → 50%
expect(p.current.cpu).toMatchObject({ scope: "cgroup", percent: 50, cores: 2, load: [1, 0.5, 0.25] });
// 整机:total 增 2000、idle 增 1000 → 50%
expect(p.current.cpu.hostPercent).toBe(50);
expect(p.current.memory).toMatchObject({
scope: "cgroup",
usedBytes: 500 * 1024 * 1024,
limitBytes: 1024 * 1024 * 1024,
percent: 48.8,
});
expect(p.sample).toMatchObject({ netRxBps: 2_000_000, netTxBps: 1_000_000, diskReadBps: 200_000 });
expect(p.current.diskIo).toMatchObject({ scope: "cgroup", writeBps: 400_000 });
expect(p.current.hostUptimeSec).toBe(3600.5);
});
it("宿主(没有 cgroup 文件):退回 /proc 整机口径", async () => {
const files = (user: number, idle: number) =>
fakeFs({ "/proc/stat": STAT(user, idle), "/proc/meminfo": MEMINFO, "/proc/net/dev": NETDEV(0, 0) });
const p = computePoint(await readResources(files(1000, 9000), 0), await readResources(files(1500, 9500), 1000));
expect(p.current.cpu).toMatchObject({ scope: "host", percent: 50 });
expect(p.current.memory).toMatchObject({ scope: "host", usedBytes: 8000000 * 1024, percent: 50 });
expect(p.current.diskIo).toMatchObject({ scope: "none", readBps: null });
expect(p.current.network.scope).toBe("host");
});
it("什么都读不到:全部为 null,不抛错", async () => {
const none = fakeFs({});
const p = computePoint(await readResources(none, 0), await readResources(none, 1000));
expect(p.current.cpu).toMatchObject({ scope: "none", percent: null, hostPercent: null, load: null });
expect(p.current.memory).toMatchObject({ scope: "none", usedBytes: null, percent: null });
expect(p.current.network).toMatchObject({ scope: "none", rxBps: null });
});
it("计数器回绕 / 重置时速率为 null,不出负数", async () => {
const f = (rx: number) => fakeFs({ "/proc/net/dev": NETDEV(rx, rx) });
const p = computePoint(await readResources(f(1000), 0), await readResources(f(10), 1000));
expect(p.sample.netRxBps).toBeNull();
});
});
describe("采样器", () => {
it("环形保留、按桶平均、快照符合契约", async () => {
let now = Date.parse("2026-10-06T00:00:00Z");
let usage = 0;
const read: ReadText = (path) => {
if (path === "/sys/fs/cgroup/cpu.stat") return Promise.resolve(`usage_usec ${usage}\n`);
if (path === "/sys/fs/cgroup/cpu.max") return Promise.resolve("100000 100000");
if (path === "/proc/meminfo") return Promise.resolve(MEMINFO);
return Promise.resolve(null);
};
const s = createResourceSampler({ read, autoStart: false, intervalMs: 5000, retentionMs: 60_000, now: () => now });
for (let i = 0; i < 30; i++) {
// 每 5 秒用 2.5 秒 CPU(1 核)→ 50%
usage += 2_500_000;
now += 5000;
await s.tick();
}
const snap = SystemResources.parse(await s.snapshot(5));
// 保留 60 秒(含边界)→ 13 个点;第一次读数没有速率,不进趋势
expect(snap.samples).toHaveLength(13);
expect(snap.samples.every((x) => x.cpuPercent === 50)).toBe(true);
expect(snap.cpu).toMatchObject({ scope: "cgroup", percent: 50, cores: 1 });
expect(snap.intervalSec).toBe(5);
s.stop();
// 长窗口:按桶取平均,点数不超过 360
const many = Array.from({ length: 4000 }, (_, i) => ({
at: new Date(Date.parse("2026-10-06T00:00:00Z") + i * 5000).toISOString(),
cpuPercent: i % 2 ? 40 : 60,
memoryUsedBytes: 100,
memoryPercent: 10,
netRxBps: null,
netTxBps: null,
diskReadBps: null,
diskWriteBps: null,
}));
const b = bucketSamples(many, 5, 360);
expect(b.bucketSec).toBe(60);
expect(b.points.length).toBeLessThanOrEqual(360);
expect(b.points[0]).toMatchObject({ cpuPercent: 50, memoryUsedBytes: 100, netRxBps: null });
});
it("刚启动没有趋势时,快照现采两次给出当前值", async () => {
let usage = 0;
const read: ReadText = (path) => {
if (path === "/sys/fs/cgroup/cpu.stat") {
usage += 100_000;
return Promise.resolve(`usage_usec ${usage}\n`);
}
return Promise.resolve(null);
};
const s = createResourceSampler({ read, autoStart: false });
const snap = await s.snapshot(60);
expect(snap.cpu.scope).toBe("cgroup");
expect(snap.cpu.percent).not.toBeNull();
expect(snap.samples.length).toBeGreaterThanOrEqual(1);
});
it("进程 CPU:首次为 null,之后为非负数", () => {
const m = createProcessCpuMeter();
expect(m()).toBeNull();
let x = 0;
for (let i = 0; i < 1e5; i++) x += Math.sqrt(i);
expect(x).toBeGreaterThan(0);
const v = m();
expect(v).not.toBeNull();
expect(v!).toBeGreaterThanOrEqual(0);
});
});