@hyperframes/engine 0.6.119 → 0.6.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -7
- package/scripts/generate-lut-reference.py +0 -168
- package/scripts/test-fitTextFontSize-browser.ts +0 -135
- package/src/cdp-headless-experimental.d.ts +0 -54
- package/src/config.test.ts +0 -213
- package/src/config.ts +0 -417
- package/src/index.ts +0 -273
- package/src/services/audioMixer.test.ts +0 -326
- package/src/services/audioMixer.ts +0 -604
- package/src/services/audioMixer.types.ts +0 -35
- package/src/services/audioVolumeEnvelope.test.ts +0 -176
- package/src/services/audioVolumeEnvelope.ts +0 -138
- package/src/services/browserManager.test.ts +0 -330
- package/src/services/browserManager.ts +0 -670
- package/src/services/chunkEncoder.test.ts +0 -1415
- package/src/services/chunkEncoder.ts +0 -831
- package/src/services/chunkEncoder.types.ts +0 -60
- package/src/services/extractionCache.test.ts +0 -199
- package/src/services/extractionCache.ts +0 -216
- package/src/services/fileServer.ts +0 -110
- package/src/services/frameCapture-discardWarmup.test.ts +0 -183
- package/src/services/frameCapture-namePolyfill.test.ts +0 -78
- package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
- package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
- package/src/services/frameCapture-warmupTicks.test.ts +0 -174
- package/src/services/frameCapture.test.ts +0 -192
- package/src/services/frameCapture.ts +0 -1934
- package/src/services/hdrCapture.test.ts +0 -159
- package/src/services/hdrCapture.ts +0 -315
- package/src/services/parallelCoordinator.test.ts +0 -139
- package/src/services/parallelCoordinator.ts +0 -437
- package/src/services/screenshotService.test.ts +0 -510
- package/src/services/screenshotService.ts +0 -615
- package/src/services/streamingEncoder.test.ts +0 -832
- package/src/services/streamingEncoder.ts +0 -594
- package/src/services/systemMemory.test.ts +0 -324
- package/src/services/systemMemory.ts +0 -180
- package/src/services/videoFrameExtractor.test.ts +0 -1062
- package/src/services/videoFrameExtractor.ts +0 -1139
- package/src/services/videoFrameInjector.test.ts +0 -300
- package/src/services/videoFrameInjector.ts +0 -687
- package/src/services/vp9Options.ts +0 -13
- package/src/types.ts +0 -191
- package/src/utils/alphaBlit.test.ts +0 -1349
- package/src/utils/alphaBlit.ts +0 -1015
- package/src/utils/assertSwiftShader.test.ts +0 -130
- package/src/utils/assertSwiftShader.ts +0 -126
- package/src/utils/ffmpegBinaries.test.ts +0 -43
- package/src/utils/ffmpegBinaries.ts +0 -63
- package/src/utils/ffprobe.test.ts +0 -342
- package/src/utils/ffprobe.ts +0 -457
- package/src/utils/gpuEncoder.test.ts +0 -140
- package/src/utils/gpuEncoder.ts +0 -268
- package/src/utils/hdr.test.ts +0 -191
- package/src/utils/hdr.ts +0 -137
- package/src/utils/hdrCompositing.test.ts +0 -130
- package/src/utils/htmlTemplate.test.ts +0 -42
- package/src/utils/htmlTemplate.ts +0 -42
- package/src/utils/layerCompositor.test.ts +0 -150
- package/src/utils/layerCompositor.ts +0 -58
- package/src/utils/parityContract.ts +0 -1
- package/src/utils/processTracker.test.ts +0 -74
- package/src/utils/processTracker.ts +0 -41
- package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
- package/src/utils/runFfmpeg.test.ts +0 -102
- package/src/utils/runFfmpeg.ts +0 -136
- package/src/utils/shaderTransitions.test.ts +0 -738
- package/src/utils/shaderTransitions.ts +0 -1130
- package/src/utils/uint16-alignment-audit.test.ts +0 -125
- package/src/utils/urlDownloader.test.ts +0 -65
- package/src/utils/urlDownloader.ts +0 -143
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -7
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
_resetCgroupLimitCacheForTests,
|
|
4
|
-
isLowMemorySystem,
|
|
5
|
-
LOW_MEMORY_TOTAL_MB_THRESHOLD,
|
|
6
|
-
parseCgroupLimitMb,
|
|
7
|
-
} from "./systemMemory.js";
|
|
8
|
-
|
|
9
|
-
const BYTES_PER_MIB = 1024 * 1024;
|
|
10
|
-
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
11
|
-
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
12
|
-
|
|
13
|
-
type SystemMemoryModule = typeof import("./systemMemory.js");
|
|
14
|
-
|
|
15
|
-
type MockSystemMemoryOptions = {
|
|
16
|
-
files?: Record<string, string>;
|
|
17
|
-
hostTotalMb?: number;
|
|
18
|
-
platform?: NodeJS.Platform;
|
|
19
|
-
readErrors?: Record<string, NodeJS.ErrnoException>;
|
|
20
|
-
onRead?: (path: string) => void;
|
|
21
|
-
throwOnFileRead?: boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function stubPlatform(platform: NodeJS.Platform): () => void {
|
|
25
|
-
const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
|
26
|
-
Object.defineProperty(process, "platform", { value: platform });
|
|
27
|
-
|
|
28
|
-
return () => {
|
|
29
|
-
if (descriptor) {
|
|
30
|
-
Object.defineProperty(process, "platform", descriptor);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function withSystemMemoryMocks(
|
|
36
|
-
options: MockSystemMemoryOptions,
|
|
37
|
-
run: (systemMemory: SystemMemoryModule) => void | Promise<void>,
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
const {
|
|
40
|
-
files = {},
|
|
41
|
-
hostTotalMb = 32768,
|
|
42
|
-
platform = "linux",
|
|
43
|
-
readErrors = {},
|
|
44
|
-
onRead,
|
|
45
|
-
throwOnFileRead = false,
|
|
46
|
-
} = options;
|
|
47
|
-
const restorePlatform = stubPlatform(platform);
|
|
48
|
-
|
|
49
|
-
vi.resetModules();
|
|
50
|
-
vi.doMock("os", () => ({
|
|
51
|
-
totalmem: () => hostTotalMb * BYTES_PER_MIB,
|
|
52
|
-
}));
|
|
53
|
-
vi.doMock("fs", () => ({
|
|
54
|
-
readFileSync: (path: string) => {
|
|
55
|
-
onRead?.(path);
|
|
56
|
-
|
|
57
|
-
if (throwOnFileRead) {
|
|
58
|
-
throw new Error(`/sys read should not happen: ${path}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (path in readErrors) {
|
|
62
|
-
throw readErrors[path];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (path in files) {
|
|
66
|
-
return files[path];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw Object.assign(new Error("missing cgroup file"), { code: "ENOENT" });
|
|
70
|
-
},
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const systemMemory = await import("./systemMemory.js");
|
|
75
|
-
systemMemory._resetCgroupLimitCacheForTests();
|
|
76
|
-
await run(systemMemory);
|
|
77
|
-
} finally {
|
|
78
|
-
vi.doUnmock("fs");
|
|
79
|
-
vi.doUnmock("os");
|
|
80
|
-
vi.resetModules();
|
|
81
|
-
restorePlatform();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
beforeEach(() => {
|
|
86
|
-
_resetCgroupLimitCacheForTests();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
afterEach(() => {
|
|
90
|
-
_resetCgroupLimitCacheForTests();
|
|
91
|
-
vi.restoreAllMocks();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("isLowMemorySystem", () => {
|
|
95
|
-
it("treats sub-threshold RAM as low-memory", () => {
|
|
96
|
-
expect(isLowMemorySystem(4096)).toBe(true);
|
|
97
|
-
expect(isLowMemorySystem(6000)).toBe(true);
|
|
98
|
-
expect(isLowMemorySystem(7600)).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("includes machines reporting exactly the threshold (8 GB boundary)", () => {
|
|
102
|
-
// Real "8 GB" hosts report at/just under 8192 MiB after firmware/iGPU
|
|
103
|
-
// reservations — the inclusive bound is the whole point (issue #1219).
|
|
104
|
-
expect(isLowMemorySystem(LOW_MEMORY_TOTAL_MB_THRESHOLD)).toBe(true);
|
|
105
|
-
expect(isLowMemorySystem(8192)).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("treats above-threshold RAM as normal", () => {
|
|
109
|
-
expect(isLowMemorySystem(8193)).toBe(false);
|
|
110
|
-
expect(isLowMemorySystem(16384)).toBe(false);
|
|
111
|
-
expect(isLowMemorySystem(65536)).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("treats a 4 GiB cgroup v2 limit on a 32 GiB host as low-memory", async () => {
|
|
115
|
-
await withSystemMemoryMocks(
|
|
116
|
-
{
|
|
117
|
-
files: {
|
|
118
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: `${4096 * BYTES_PER_MIB}`,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
122
|
-
expect(getSystemTotalMb()).toBe(4096);
|
|
123
|
-
expect(isLowMemorySystem()).toBe(true);
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("parseCgroupLimitMb", () => {
|
|
130
|
-
it("parses cgroup v2 numeric limits", () => {
|
|
131
|
-
expect(parseCgroupLimitMb(`${4096 * BYTES_PER_MIB}`, null)).toBe(4096);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('ignores cgroup v2 "max" limits', () => {
|
|
135
|
-
expect(parseCgroupLimitMb("max", null)).toBeNull();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("parses cgroup v1 numeric limits and ignores no-limit sentinels", () => {
|
|
139
|
-
expect(parseCgroupLimitMb(null, `${6144 * BYTES_PER_MIB}`)).toBe(6144);
|
|
140
|
-
expect(parseCgroupLimitMb(null, "9223372036854771712")).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("ignores absent and malformed limits", () => {
|
|
144
|
-
expect(parseCgroupLimitMb(null, null)).toBeNull();
|
|
145
|
-
|
|
146
|
-
for (const content of ["", "garbage", "-1", "0"]) {
|
|
147
|
-
expect(parseCgroupLimitMb(content, null)).toBeNull();
|
|
148
|
-
expect(parseCgroupLimitMb(null, content)).toBeNull();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("uses cgroup v2 when both v2 and v1 contents are present", () => {
|
|
153
|
-
expect(parseCgroupLimitMb(`${4096 * BYTES_PER_MIB}`, `${2048 * BYTES_PER_MIB}`)).toBe(4096);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe("getSystemTotalMb", () => {
|
|
158
|
-
it("caches cgroup probes until the test reset hook clears the cache", async () => {
|
|
159
|
-
const readCalls: string[] = [];
|
|
160
|
-
const files = {
|
|
161
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: `${4096 * BYTES_PER_MIB}`,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
await withSystemMemoryMocks(
|
|
165
|
-
{
|
|
166
|
-
files,
|
|
167
|
-
onRead: (path) => readCalls.push(path),
|
|
168
|
-
},
|
|
169
|
-
({ _resetCgroupLimitCacheForTests, getSystemTotalMb }) => {
|
|
170
|
-
expect(getSystemTotalMb()).toBe(4096);
|
|
171
|
-
expect(getSystemTotalMb()).toBe(4096);
|
|
172
|
-
expect(readCalls).toEqual([CGROUP_V2_MEMORY_MAX_PATH]);
|
|
173
|
-
|
|
174
|
-
files[CGROUP_V2_MEMORY_MAX_PATH] = `${2048 * BYTES_PER_MIB}`;
|
|
175
|
-
_resetCgroupLimitCacheForTests();
|
|
176
|
-
|
|
177
|
-
expect(getSystemTotalMb()).toBe(2048);
|
|
178
|
-
expect(readCalls).toEqual([CGROUP_V2_MEMORY_MAX_PATH, CGROUP_V2_MEMORY_MAX_PATH]);
|
|
179
|
-
},
|
|
180
|
-
);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('uses the host total when cgroup v2 reports "max"', async () => {
|
|
184
|
-
await withSystemMemoryMocks(
|
|
185
|
-
{
|
|
186
|
-
files: {
|
|
187
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: "max",
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
191
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
192
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("honors cgroup v1 numeric limits when cgroup v2 is absent", async () => {
|
|
198
|
-
await withSystemMemoryMocks(
|
|
199
|
-
{
|
|
200
|
-
files: {
|
|
201
|
-
[CGROUP_V1_MEMORY_LIMIT_PATH]: `${6144 * BYTES_PER_MIB}`,
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
205
|
-
expect(getSystemTotalMb()).toBe(6144);
|
|
206
|
-
expect(isLowMemorySystem()).toBe(true);
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("uses the host total when cgroup v1 reports a no-limit sentinel", async () => {
|
|
212
|
-
await withSystemMemoryMocks(
|
|
213
|
-
{
|
|
214
|
-
files: {
|
|
215
|
-
[CGROUP_V1_MEMORY_LIMIT_PATH]: "9223372036854771712",
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
219
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
220
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
221
|
-
},
|
|
222
|
-
);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("uses the host total when cgroup files are absent", async () => {
|
|
226
|
-
await withSystemMemoryMocks({}, ({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
227
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
228
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("warns once and uses the host total when a cgroup file is unreadable", async () => {
|
|
233
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
234
|
-
await withSystemMemoryMocks(
|
|
235
|
-
{
|
|
236
|
-
readErrors: {
|
|
237
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: Object.assign(new Error("permission denied"), {
|
|
238
|
-
code: "EACCES",
|
|
239
|
-
}),
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
243
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
244
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
245
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
246
|
-
expect(warn).toHaveBeenCalledTimes(1);
|
|
247
|
-
expect(warn.mock.calls[0]?.[0]).toContain(
|
|
248
|
-
"[SystemMemory] Unable to read cgroup memory limit",
|
|
249
|
-
);
|
|
250
|
-
expect(warn.mock.calls[0]?.[0]).toContain("EACCES");
|
|
251
|
-
},
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("stays silent when cgroup files are absent", async () => {
|
|
256
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
257
|
-
await withSystemMemoryMocks({}, ({ getSystemTotalMb }) => {
|
|
258
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
259
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
260
|
-
expect(warn).not.toHaveBeenCalled();
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it.each(["", "garbage", "-1", "0"])(
|
|
265
|
-
"uses the host total for malformed cgroup v2 content %j",
|
|
266
|
-
async (content) => {
|
|
267
|
-
await withSystemMemoryMocks(
|
|
268
|
-
{
|
|
269
|
-
files: {
|
|
270
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: content,
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
274
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
275
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
276
|
-
},
|
|
277
|
-
);
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
it.each(["", "garbage", "-1", "0"])(
|
|
282
|
-
"uses the host total for malformed cgroup v1 content %j",
|
|
283
|
-
async (content) => {
|
|
284
|
-
await withSystemMemoryMocks(
|
|
285
|
-
{
|
|
286
|
-
files: {
|
|
287
|
-
[CGROUP_V1_MEMORY_LIMIT_PATH]: content,
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
291
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
292
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
},
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
it("uses the host total when a cgroup limit is larger than host RAM", async () => {
|
|
299
|
-
await withSystemMemoryMocks(
|
|
300
|
-
{
|
|
301
|
-
files: {
|
|
302
|
-
[CGROUP_V2_MEMORY_MAX_PATH]: `${65536 * BYTES_PER_MIB}`,
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
306
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
307
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
308
|
-
},
|
|
309
|
-
);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("does not read cgroup files on non-Linux platforms", async () => {
|
|
313
|
-
await withSystemMemoryMocks(
|
|
314
|
-
{
|
|
315
|
-
platform: "darwin",
|
|
316
|
-
throwOnFileRead: true,
|
|
317
|
-
},
|
|
318
|
-
({ getSystemTotalMb, isLowMemorySystem }) => {
|
|
319
|
-
expect(getSystemTotalMb()).toBe(32768);
|
|
320
|
-
expect(isLowMemorySystem()).toBe(false);
|
|
321
|
-
},
|
|
322
|
-
);
|
|
323
|
-
});
|
|
324
|
-
});
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* System-memory probing for memory-adaptive render behaviour.
|
|
3
|
-
*
|
|
4
|
-
* The render pipeline tunes itself to the host's RAM in several places —
|
|
5
|
-
* frame-cache sizes (`config.ts`), Chrome heap + GPU budget flags
|
|
6
|
-
* (`browserManager.ts`), and worker count (`parallelCoordinator.ts`).
|
|
7
|
-
* They all need the same "how much memory does this box have" reading, so
|
|
8
|
-
* it lives here once instead of being re-derived inline.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync } from "fs";
|
|
12
|
-
import { totalmem } from "os";
|
|
13
|
-
|
|
14
|
-
const BYTES_PER_MIB = 1024 * 1024;
|
|
15
|
-
const BYTES_PER_MIB_BIGINT = BigInt(BYTES_PER_MIB);
|
|
16
|
-
// These are the paths as seen from INSIDE a container, where the runtime
|
|
17
|
-
// mounts the container's own cgroup at the namespace root — the case this
|
|
18
|
-
// probe exists for. They are deliberately not resolved via /proc/self/cgroup:
|
|
19
|
-
// on a bare host under systemd the process's real limit may live in a nested
|
|
20
|
-
// slice (e.g. /sys/fs/cgroup/user.slice/.../memory.max) that these root paths
|
|
21
|
-
// don't see, and that's acceptable — bare hosts are covered by total-RAM
|
|
22
|
-
// detection, and chasing nested slices adds fragility for no container gain.
|
|
23
|
-
const CGROUP_V2_MEMORY_MAX_PATH = "/sys/fs/cgroup/memory.max";
|
|
24
|
-
const CGROUP_V1_MEMORY_LIMIT_PATH = "/sys/fs/cgroup/memory/memory.limit_in_bytes";
|
|
25
|
-
// Kernel no-limit sentinel is page-rounded 2^63-1 (~9223372036854771712); >= 2^60 is implausible as a real limit.
|
|
26
|
-
const CGROUP_V1_NO_LIMIT_CUTOFF_BYTES = 2n ** 60n;
|
|
27
|
-
|
|
28
|
-
let _cachedCgroupLimitMb: number | null | undefined;
|
|
29
|
-
let _warnedCgroupReadFailure = false;
|
|
30
|
-
|
|
31
|
-
/** Parse cgroup v2/v1 memory limits from sysfs file contents into MiB. */
|
|
32
|
-
export function parseCgroupLimitMb(
|
|
33
|
-
v2Content: string | null,
|
|
34
|
-
v1Content: string | null,
|
|
35
|
-
): number | null {
|
|
36
|
-
if (v2Content !== null) {
|
|
37
|
-
return parseCgroupV2LimitMb(v2Content);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return parseCgroupV1LimitMb(v1Content);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function parseCgroupV2LimitMb(content: string): number | null {
|
|
44
|
-
const trimmed = content.trim();
|
|
45
|
-
if (trimmed === "max") {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return parsePositiveByteLimitMb(trimmed);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function parseCgroupV1LimitMb(content: string | null): number | null {
|
|
53
|
-
if (content === null) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return parsePositiveByteLimitMb(content.trim(), CGROUP_V1_NO_LIMIT_CUTOFF_BYTES);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parsePositiveByteLimitMb(content: string, noLimitCutoffBytes?: bigint): number | null {
|
|
61
|
-
if (!/^\d+$/.test(content)) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const bytes = BigInt(content);
|
|
66
|
-
if (bytes <= 0n) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (noLimitCutoffBytes !== undefined && bytes >= noLimitCutoffBytes) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return Number(bytes / BYTES_PER_MIB_BIGINT);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Test-only: reset the cached cgroup memory probe. */
|
|
78
|
-
export function _resetCgroupLimitCacheForTests(): void {
|
|
79
|
-
_cachedCgroupLimitMb = undefined;
|
|
80
|
-
_warnedCgroupReadFailure = false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function getCgroupLimitMb(): number | null {
|
|
84
|
-
if (_cachedCgroupLimitMb !== undefined) return _cachedCgroupLimitMb;
|
|
85
|
-
|
|
86
|
-
if (process.platform !== "linux") {
|
|
87
|
-
_cachedCgroupLimitMb = null;
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const v2Content = readCgroupFile(CGROUP_V2_MEMORY_MAX_PATH);
|
|
92
|
-
const v1Content = v2Content === null ? readCgroupFile(CGROUP_V1_MEMORY_LIMIT_PATH) : null;
|
|
93
|
-
|
|
94
|
-
_cachedCgroupLimitMb = parseCgroupLimitMb(v2Content, v1Content);
|
|
95
|
-
if (_cachedCgroupLimitMb !== null) {
|
|
96
|
-
console.info(
|
|
97
|
-
`[SystemMemory] cgroup memory limit detected: ${_cachedCgroupLimitMb} MiB — ` +
|
|
98
|
-
`it governs memory-adaptive render behaviour instead of host RAM.`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return _cachedCgroupLimitMb;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function readCgroupFile(path: string): string | null {
|
|
105
|
-
try {
|
|
106
|
-
return readFileSync(path, "utf8");
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const code = getErrorCode(error);
|
|
109
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
110
|
-
warnCgroupReadFailure(path, error);
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getErrorCode(error: unknown): string | undefined {
|
|
117
|
-
if (typeof error !== "object" || error === null || !("code" in error)) {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return typeof error.code === "string" ? error.code : undefined;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function formatCgroupReadError(error: unknown): string {
|
|
125
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
126
|
-
const code = getErrorCode(error);
|
|
127
|
-
return code ? `${code}: ${message}` : message;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function warnCgroupReadFailure(path: string, error: unknown): void {
|
|
131
|
-
if (_warnedCgroupReadFailure) return;
|
|
132
|
-
_warnedCgroupReadFailure = true;
|
|
133
|
-
console.warn(
|
|
134
|
-
`[SystemMemory] Unable to read cgroup memory limit at ${path} ` +
|
|
135
|
-
`(${formatCgroupReadError(error)}); falling back to host RAM.`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Total physical RAM in MiB. */
|
|
140
|
-
export function getSystemTotalMb(): number {
|
|
141
|
-
const hostTotalMb = Math.floor(totalmem() / BYTES_PER_MIB);
|
|
142
|
-
const cgroupLimitMb = getCgroupLimitMb();
|
|
143
|
-
|
|
144
|
-
return cgroupLimitMb === null ? hostTotalMb : Math.min(hostTotalMb, cgroupLimitMb);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Total-RAM ceiling (MiB) at or below which the host is treated as
|
|
149
|
-
* memory-constrained. Tuned to the 8 GB laptops in
|
|
150
|
-
* heygen-com/hyperframes#1218 / #1219: on those boxes the default render
|
|
151
|
-
* shape (probe Chrome + a throwaway calibration Chrome + N capture
|
|
152
|
-
* workers) thrashes, so the pipeline collapses to its cheapest form.
|
|
153
|
-
*
|
|
154
|
-
* `<=` deliberately includes machines that report exactly 8192 MiB —
|
|
155
|
-
* real "8 GB" hardware reports anywhere from ~7600 to 8192 MiB once
|
|
156
|
-
* firmware/integrated-GPU reservations are subtracted, and a strict `<`
|
|
157
|
-
* would skip the optimisation on the very hardware that needs it.
|
|
158
|
-
*/
|
|
159
|
-
export const LOW_MEMORY_TOTAL_MB_THRESHOLD = 8192;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* True when the host should run the low-memory render profile.
|
|
163
|
-
*
|
|
164
|
-
* Keyed on total physical RAM, not free memory: free memory swings
|
|
165
|
-
* moment to moment and is underreported on macOS, whereas total RAM is a
|
|
166
|
-
* stable proxy for "how many concurrent Chrome instances can this box
|
|
167
|
-
* survive". Accepts an explicit `totalMb` so callers (and tests) can pass
|
|
168
|
-
* a known value instead of re-probing.
|
|
169
|
-
*
|
|
170
|
-
* Caveat: Linux cgroup v1/v2 memory limits are consulted when readable, so
|
|
171
|
-
* Docker and serverless runtimes, including Lambda tiers with readable cgroup
|
|
172
|
-
* ceilings, inherit the tighter container limit instead of the host's physical
|
|
173
|
-
* RAM. Environments that hide cgroup files should set
|
|
174
|
-
* `PRODUCER_LOW_MEMORY_MODE` explicitly rather than relying on auto-detection.
|
|
175
|
-
* Hosts whose *effective* total RAM is genuinely <= the threshold (laptops,
|
|
176
|
-
* small VMs, small Lambda tiers, small containers) are detected correctly.
|
|
177
|
-
*/
|
|
178
|
-
export function isLowMemorySystem(totalMb: number = getSystemTotalMb()): boolean {
|
|
179
|
-
return totalMb <= LOW_MEMORY_TOTAL_MB_THRESHOLD;
|
|
180
|
-
}
|