@hyperframes/engine 0.6.94 → 0.6.95

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.
@@ -8,11 +8,140 @@
8
8
  * it lives here once instead of being re-derived inline.
9
9
  */
10
10
 
11
+ import { readFileSync } from "fs";
11
12
  import { totalmem } from "os";
12
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
+
13
139
  /** Total physical RAM in MiB. */
14
140
  export function getSystemTotalMb(): number {
15
- return Math.floor(totalmem() / (1024 * 1024));
141
+ const hostTotalMb = Math.floor(totalmem() / BYTES_PER_MIB);
142
+ const cgroupLimitMb = getCgroupLimitMb();
143
+
144
+ return cgroupLimitMb === null ? hostTotalMb : Math.min(hostTotalMb, cgroupLimitMb);
16
145
  }
17
146
 
18
147
  /**
@@ -38,14 +167,13 @@ export const LOW_MEMORY_TOTAL_MB_THRESHOLD = 8192;
38
167
  * survive". Accepts an explicit `totalMb` so callers (and tests) can pass
39
168
  * a known value instead of re-probing.
40
169
  *
41
- * Caveat: `os.totalmem()` reports the *host's* physical RAM, not a
42
- * cgroup/container memory limit. A 4 GB container on a 32 GB host will not
43
- * auto-flag as low-memory, and an 8 GB container on a 64 GB host won't
44
- * either. Containerised and serverless callers (Docker `--docker` renders,
45
- * Lambda) that want a specific profile should set `PRODUCER_LOW_MEMORY_MODE`
46
- * explicitly rather than relying on auto-detection. Hosts whose *total* RAM
47
- * is genuinely <= the threshold (laptops, small VMs, small Lambda tiers) are
48
- * detected correctly regardless of container nesting.
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.
49
177
  */
50
178
  export function isLowMemorySystem(totalMb: number = getSystemTotalMb()): boolean {
51
179
  return totalMb <= LOW_MEMORY_TOTAL_MB_THRESHOLD;