mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
|
import {
|
|
_resetCgroupLimitCacheForTests,
|
|
isLowMemorySystem,
|
|
LOW_MEMORY_TOTAL_MB_THRESHOLD,
|
|
parseCgroupLimitMb,
|
|
} from "./systemMemory.js";
|
|
|
|
const BYTES_PER_MIB = 1024 * 1024;
|
|
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
|
|
type SystemMemoryModule = typeof import("./systemMemory.js");
|
|
|
|
type MockSystemMemoryOptions = {
|
|
files?: Record<string, string>;
|
|
hostTotalMb?: number;
|
|
platform?: NodeJS.Platform;
|
|
readErrors?: Record<string, NodeJS.ErrnoException>;
|
|
onRead?: (path: string) => void;
|
|
throwOnFileRead?: boolean;
|
|
};
|
|
|
|
function stubPlatform(platform: NodeJS.Platform): () => void {
|
|
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
|
Object.defineProperty(process, "platform", { value: platform });
|
|
|
|
return () => {
|
|
if (descriptor) {
|
|
Object.defineProperty(process, "platform", descriptor);
|
|
}
|
|
};
|
|
}
|
|
|
|
async function withSystemMemoryMocks(
|
|
options: MockSystemMemoryOptions,
|
|
run: (systemMemory: SystemMemoryModule) => void | Promise<void>,
|
|
): Promise<void> {
|
|
const {
|
|
files = {},
|
|
hostTotalMb = 32768,
|
|
platform = "linux",
|
|
readErrors = {},
|
|
onRead,
|
|
throwOnFileRead = false,
|
|
} = options;
|
|
const restorePlatform = stubPlatform(platform);
|
|
|
|
vi.resetModules();
|
|
vi.doMock("os", () => ({
|
|
totalmem: () => hostTotalMb * BYTES_PER_MIB,
|
|
}));
|
|
vi.doMock("fs", () => ({
|
|
readFileSync: (path: string) => {
|
|
onRead?.(path);
|
|
|
|
if (throwOnFileRead) {
|
|
throw new Error(`/sys read should not happen: ${path}`);
|
|
}
|
|
|
|
if (path in readErrors) {
|
|
throw readErrors[path];
|
|
}
|
|
|
|
if (path in files) {
|
|
return files[path];
|
|
}
|
|
|
|
throw Object.assign(new Error("missing cgroup file"), { code: "ENOENT" });
|
|
},
|
|
}));
|
|
|
|
try {
|
|
const systemMemory = await import("./systemMemory.js");
|
|
systemMemory._resetCgroupLimitCacheForTests();
|
|
await run(systemMemory);
|
|
} finally {
|
|
vi.doUnmock("fs");
|
|
vi.doUnmock("os");
|
|
vi.resetModules();
|
|
restorePlatform();
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
_resetCgroupLimitCacheForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
_resetCgroupLimitCacheForTests();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("isLowMemorySystem", () => {
|
|
it("treats sub-threshold RAM as low-memory", () => {
|
|
expect(isLowMemorySystem(4096)).toBe(true);
|
|
expect(isLowMemorySystem(6000)).toBe(true);
|
|
expect(isLowMemorySystem(7600)).toBe(true);
|
|
});
|
|
|
|
it("includes machines reporting exactly the threshold (8 GB boundary)", () => {
|
|
// Real "8 GB" hosts report at/just under 8192 MiB after firmware/iGPU
|
|
// reservations — the inclusive bound is the whole point (issue #1219).
|
|
expect(isLowMemorySystem(LOW_MEMORY_TOTAL_MB_THRESHOLD)).toBe(true);
|
|
expect(isLowMemorySystem(8192)).toBe(true);
|
|
});
|
|
|
|
it("treats above-threshold RAM as normal", () => {
|
|
expect(isLowMemorySystem(8193)).toBe(false);
|
|
expect(isLowMemorySystem(16384)).toBe(false);
|
|
expect(isLowMemorySystem(65536)).toBe(false);
|
|
});
|
|
|
|
it("treats a 4 GiB cgroup v2 limit on a 32 GiB host as low-memory", async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: `${4096 * BYTES_PER_MIB}`,
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(4096);
|
|
expect(isLowMemorySystem()).toBe(true);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseCgroupLimitMb", () => {
|
|
it("parses cgroup v2 numeric limits", () => {
|
|
expect(parseCgroupLimitMb(`${4096 * BYTES_PER_MIB}`, null)).toBe(4096);
|
|
});
|
|
|
|
it('ignores cgroup v2 "max" limits', () => {
|
|
expect(parseCgroupLimitMb("max", null)).toBeNull();
|
|
});
|
|
|
|
it("parses cgroup v1 numeric limits and ignores no-limit sentinels", () => {
|
|
expect(parseCgroupLimitMb(null, `${6144 * BYTES_PER_MIB}`)).toBe(6144);
|
|
expect(parseCgroupLimitMb(null, "9223372036854771712")).toBeNull();
|
|
});
|
|
|
|
it("ignores absent and malformed limits", () => {
|
|
expect(parseCgroupLimitMb(null, null)).toBeNull();
|
|
|
|
for (const content of ["", "garbage", "-1", "0"]) {
|
|
expect(parseCgroupLimitMb(content, null)).toBeNull();
|
|
expect(parseCgroupLimitMb(null, content)).toBeNull();
|
|
}
|
|
});
|
|
|
|
it("uses cgroup v2 when both v2 and v1 contents are present", () => {
|
|
expect(parseCgroupLimitMb(`${4096 * BYTES_PER_MIB}`, `${2048 * BYTES_PER_MIB}`)).toBe(4096);
|
|
});
|
|
});
|
|
|
|
describe("getSystemTotalMb", () => {
|
|
it("caches cgroup probes until the test reset hook clears the cache", async () => {
|
|
const readCalls: string[] = [];
|
|
const files = {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: `${4096 * BYTES_PER_MIB}`,
|
|
};
|
|
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files,
|
|
onRead: (path) => readCalls.push(path),
|
|
},
|
|
({ _resetCgroupLimitCacheForTests, getSystemTotalMb }) => {
|
|
expect(getSystemTotalMb()).toBe(4096);
|
|
expect(getSystemTotalMb()).toBe(4096);
|
|
expect(readCalls).toEqual([CGROUP_V2_MEMORY_MAX_PATH]);
|
|
|
|
files[CGROUP_V2_MEMORY_MAX_PATH] = `${2048 * BYTES_PER_MIB}`;
|
|
_resetCgroupLimitCacheForTests();
|
|
|
|
expect(getSystemTotalMb()).toBe(2048);
|
|
expect(readCalls).toEqual([CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V2_MEMORY_MAX_PATH]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('uses the host total when cgroup v2 reports "max"', async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: "max",
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("honors cgroup v1 numeric limits when cgroup v2 is absent", async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V1_MEMORY_LIMIT_PATH]: `${6144 * BYTES_PER_MIB}`,
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(6144);
|
|
expect(isLowMemorySystem()).toBe(true);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("uses the host total when cgroup v1 reports a no-limit sentinel", async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V1_MEMORY_LIMIT_PATH]: "9223372036854771712",
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("uses the host total when cgroup files are absent", async () => {
|
|
await withSystemMemoryMocks({}, ({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("warns once and uses the host total when a cgroup file is unreadable", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
await withSystemMemoryMocks(
|
|
{
|
|
readErrors: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: Object.assign(new Error("permission denied"), {
|
|
code: "EACCES",
|
|
}),
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(warn.mock.calls[0]?.[0]).toContain(
|
|
"[SystemMemory] Unable to read cgroup memory limit",
|
|
);
|
|
expect(warn.mock.calls[0]?.[0]).toContain("EACCES");
|
|
},
|
|
);
|
|
});
|
|
|
|
it("logs the cgroup-limit notice to stderr, not stdout (keeps --json output clean)", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
const info = vi.spyOn(console, "info").mockImplementation(() => {});
|
|
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: `${4096 * BYTES_PER_MIB}`,
|
|
},
|
|
},
|
|
({ getSystemTotalMb }) => {
|
|
expect(getSystemTotalMb()).toBe(4096);
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(warn.mock.calls[0]?.[0]).toContain("[SystemMemory] cgroup memory limit detected");
|
|
// stdout must stay clean so `check --json` output is machine-parseable.
|
|
expect(info).not.toHaveBeenCalled();
|
|
expect(log).not.toHaveBeenCalled();
|
|
},
|
|
);
|
|
});
|
|
|
|
it("stays silent when cgroup files are absent", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
await withSystemMemoryMocks({}, ({ getSystemTotalMb }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(warn).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it.each(["", "garbage", "-1", "0"])(
|
|
"uses the host total for malformed cgroup v2 content %j",
|
|
async (content) => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: content,
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(["", "garbage", "-1", "0"])(
|
|
"uses the host total for malformed cgroup v1 content %j",
|
|
async (content) => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V1_MEMORY_LIMIT_PATH]: content,
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
it("uses the host total when a cgroup limit is larger than host RAM", async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
files: {
|
|
[CGROUP_V2_MEMORY_MAX_PATH]: `${65536 * BYTES_PER_MIB}`,
|
|
},
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("does not read cgroup files on non-Linux platforms", async () => {
|
|
await withSystemMemoryMocks(
|
|
{
|
|
platform: "darwin",
|
|
throwOnFileRead: true,
|
|
},
|
|
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
expect(getSystemTotalMb()).toBe(32768);
|
|
expect(isLowMemorySystem()).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
});
|