@intentic/constants 1.396.0 → 1.398.0
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 +1 -1
- package/src/memory-room.d.mts +5 -3
- package/src/memory-room.mjs +65 -7
package/package.json
CHANGED
package/src/memory-room.d.mts
CHANGED
|
@@ -20,14 +20,16 @@ export const ROOM_SOCKET: string;
|
|
|
20
20
|
export const READING_FILES: readonly string[];
|
|
21
21
|
|
|
22
22
|
export interface MemoryReading {
|
|
23
|
-
// memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it.
|
|
23
|
+
// memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it. Never above the workload
|
|
24
|
+
// leaf's memory.max plus what runs outside that leaf, where the leaf is bounded: admitted work runs in the leaf.
|
|
24
25
|
readonly limitBytes: number | undefined;
|
|
25
26
|
// Working set plus swap; undefined where nothing measures it. Free memory is counted against its resident part.
|
|
26
27
|
readonly usedBytes: number | undefined;
|
|
27
28
|
// The swapped part of `usedBytes`; 0 when swap is off or unaccounted.
|
|
28
29
|
readonly swapBytes: number;
|
|
29
|
-
// What swap can hold: memory.swap.max, never more than what is swapped plus the machine's SwapFree
|
|
30
|
-
//
|
|
30
|
+
// What swap can hold: memory.swap.max, never more than what is swapped plus the machine's SwapFree, nor than the
|
|
31
|
+
// workload leaf's memory.swap.max plus what is swapped outside it; at a root cgroup the machine's SwapTotal.
|
|
32
|
+
// Undefined where nothing bounds it. Past SWAP_FULL_SHARE of it, swap counts as used.
|
|
31
33
|
readonly swapLimitBytes?: number | undefined;
|
|
32
34
|
// The machine's MemAvailable, which free memory never exceeds; absent where the machine does not say.
|
|
33
35
|
readonly availableBytes?: number | undefined;
|
package/src/memory-room.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Shared admission formula for ResourceBudget and scripts without a daemon; dependency-free for pre-install use.
|
|
3
|
-
// Limit is memory.high, else memory.max, else host RAM, capped at host RAM
|
|
3
|
+
// Limit is memory.high, else memory.max, else host RAM, capped at host RAM, and never above the workload leaf's own wall
|
|
4
|
+
// plus what runs outside that leaf; free resident room is also capped at MemAvailable.
|
|
4
5
|
// Exclude reclaimable inactive cache; count swapped pages only near full swap. Memory full PSI avg10/avg60 catches paging pressure.
|
|
5
6
|
// I/O full PSI holds background work only: a busy disk may slow a person's turn but must not refuse it.
|
|
6
7
|
import { spawnSync } from "node:child_process";
|
|
@@ -86,6 +87,8 @@ export const RESERVATION_MS = 90_000;
|
|
|
86
87
|
export const ROOM_SOCKET = process.env.INTENTIC_ROOM_SOCKET ?? "/run/intentic/room.sock";
|
|
87
88
|
|
|
88
89
|
const CGROUP = "/sys/fs/cgroup";
|
|
90
|
+
// The leaf the entrypoint puts everything but netd and the daemon in, and bounds below the sandbox's cap (workloadWalls).
|
|
91
|
+
const WORKLOAD = `${CGROUP}/workload`;
|
|
89
92
|
// Every file the reading is made of, for a caller that reads them itself (the daemon, asynchronously).
|
|
90
93
|
export const READING_FILES = Object.freeze([
|
|
91
94
|
`${CGROUP}/memory.high`,
|
|
@@ -97,6 +100,11 @@ export const READING_FILES = Object.freeze([
|
|
|
97
100
|
`${CGROUP}/memory.pressure`,
|
|
98
101
|
`${CGROUP}/memory.events`,
|
|
99
102
|
`${CGROUP}/io.pressure`,
|
|
103
|
+
`${WORKLOAD}/memory.max`,
|
|
104
|
+
`${WORKLOAD}/memory.current`,
|
|
105
|
+
`${WORKLOAD}/memory.stat`,
|
|
106
|
+
`${WORKLOAD}/memory.swap.current`,
|
|
107
|
+
`${WORKLOAD}/memory.swap.max`,
|
|
100
108
|
"/proc/meminfo",
|
|
101
109
|
"/proc/pressure/memory",
|
|
102
110
|
"/proc/pressure/io",
|
|
@@ -133,25 +141,75 @@ const stallOf = (read, resource) => {
|
|
|
133
141
|
return { recent: fullAvg(text, "avg10") ?? 0, sustained: fullAvg(text, "avg60") ?? 0 };
|
|
134
142
|
};
|
|
135
143
|
|
|
144
|
+
// A cgroup's working set (memory.current less the inactive file cache the kernel reclaims first) and its swap, or
|
|
145
|
+
// undefined where it keeps no memory.current. Unaccounted swap (swapaccount off) is none, never unknown: it must not
|
|
146
|
+
// blank a measurable ceiling.
|
|
147
|
+
const chargeOf = (read, dir) => {
|
|
148
|
+
const current = numeric(read(`${dir}/memory.current`));
|
|
149
|
+
return current === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: { residentBytes: Math.max(0, current - (keyed(read(`${dir}/memory.stat`)).inactive_file ?? 0)), swapBytes: numeric(read(`${dir}/memory.swap.current`)) ?? 0 };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// (2026-11-05) THE WORKLOAD LEAF'S WALLS. Everything this gate admits (a turn, a build, a test run) runs in the workload
|
|
155
|
+
// leaf, which the entrypoint and daemon-cgroup.ts hold below the sandbox's cap (memory-records.ts workloadMemoryMax; the swap
|
|
156
|
+
// default is @intentic/sandbox-run defaultSandboxSwapGib).
|
|
157
|
+
// On a 16 GiB cap the container's memory.high is 14.4 GiB and the leaf's memory.max 12.5 GiB, so a gate
|
|
158
|
+
// judging by the container alone admitted work the leaf had no room for, which then ended in an OOM kill inside the leaf
|
|
159
|
+
// instead of a wait for room.
|
|
160
|
+
//
|
|
161
|
+
// The leaf's wall bounds only what runs in it, so as a ceiling on the whole sandbox it is that wall plus what runs
|
|
162
|
+
// outside the leaf now. Outside is the container's charge less the leaf's, not the daemon leaf's own figure: the
|
|
163
|
+
// sandbox's own Docker containers sit beside the two leaves too. The limit less what counts against it then comes to the
|
|
164
|
+
// leaf's wall less the leaf's own working set, the workload judged against its own max. That is kept as a lower limit
|
|
165
|
+
// rather than a second reading so that used stays the whole sandbox's (the gauge, the shortage sentence and the cap a
|
|
166
|
+
// client offers to raise all speak of the sandbox), and every caller of the limit sees the lower wall without knowing
|
|
167
|
+
// the leaf exists.
|
|
168
|
+
//
|
|
169
|
+
// Swap the same way: the leaf's memory.swap.max plus what is swapped outside it (normally nothing: the daemon's leaf
|
|
170
|
+
// allows none), so swap counts as full once the leaf's own is nearly spent. From then on the swapped pages count against
|
|
171
|
+
// the limit too (countedBytesOf), and the outside's swapped pages join the offset, so the leaf is still judged by its own.
|
|
172
|
+
//
|
|
173
|
+
// A leaf that is absent, or whose bound reads `max`, is no wall, and the reading is the container's as before. A root
|
|
174
|
+
// cgroup has none either: with no container charge there is no inside to tell from outside.
|
|
175
|
+
const workloadWalls = (read, sandbox, swapLimit) => {
|
|
176
|
+
const leaf = chargeOf(read, WORKLOAD);
|
|
177
|
+
if (leaf === undefined) {
|
|
178
|
+
return { limit: Number.POSITIVE_INFINITY, swapLimit };
|
|
179
|
+
}
|
|
180
|
+
const outsideResident = Math.max(0, sandbox.residentBytes - leaf.residentBytes);
|
|
181
|
+
const outsideSwap = Math.max(0, sandbox.swapBytes - leaf.swapBytes);
|
|
182
|
+
const swapWall = numeric(read(`${WORKLOAD}/memory.swap.max`));
|
|
183
|
+
const walledSwap = Math.min(swapLimit, swapWall === undefined ? Number.POSITIVE_INFINITY : swapWall + outsideSwap);
|
|
184
|
+
const wall = numeric(read(`${WORKLOAD}/memory.max`));
|
|
185
|
+
if (wall === undefined) {
|
|
186
|
+
return { limit: Number.POSITIVE_INFINITY, swapLimit: walledSwap };
|
|
187
|
+
}
|
|
188
|
+
const full = swapFullOf({ swapBytes: sandbox.swapBytes, swapLimitBytes: Number.isFinite(walledSwap) ? walledSwap : undefined });
|
|
189
|
+
return { limit: wall + outsideResident + (full ? outsideSwap : 0), swapLimit: walledSwap };
|
|
190
|
+
};
|
|
191
|
+
|
|
136
192
|
/** The reading, from a reader that answers each of READING_FILES with its text or undefined. */
|
|
137
193
|
export const readingFrom = (read) => {
|
|
138
194
|
const meminfo = keyed(read("/proc/meminfo"));
|
|
139
195
|
const machine = kib(meminfo, "MemTotal");
|
|
140
|
-
|
|
196
|
+
let bound = Math.min(
|
|
141
197
|
numeric(read(`${CGROUP}/memory.high`)) ?? Number.POSITIVE_INFINITY,
|
|
142
198
|
numeric(read(`${CGROUP}/memory.max`)) ?? Number.POSITIVE_INFINITY,
|
|
143
199
|
machine ?? Number.POSITIVE_INFINITY,
|
|
144
200
|
);
|
|
145
|
-
const
|
|
201
|
+
const sandbox = chargeOf(read, CGROUP);
|
|
146
202
|
let usedBytes;
|
|
147
203
|
let swapBytes = 0;
|
|
148
204
|
let swapLimit = Number.POSITIVE_INFINITY;
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
usedBytes = Math.max(0, current - (keyed(read(`${CGROUP}/memory.stat`)).inactive_file ?? 0)) + swapBytes;
|
|
205
|
+
if (sandbox !== undefined) {
|
|
206
|
+
swapBytes = sandbox.swapBytes;
|
|
207
|
+
usedBytes = sandbox.residentBytes + swapBytes;
|
|
153
208
|
// The cgroup's own swap ceiling, and never more than what it holds plus what the machine's swap can still take.
|
|
154
209
|
swapLimit = Math.min(numeric(read(`${CGROUP}/memory.swap.max`)) ?? Number.POSITIVE_INFINITY, swapBytes + (kib(meminfo, "SwapFree") ?? Number.POSITIVE_INFINITY));
|
|
210
|
+
const walls = workloadWalls(read, sandbox, swapLimit);
|
|
211
|
+
bound = Math.min(bound, walls.limit);
|
|
212
|
+
swapLimit = walls.swapLimit;
|
|
155
213
|
} else if (machine !== undefined && kib(meminfo, "MemAvailable") !== undefined) {
|
|
156
214
|
swapBytes = Math.max(0, (kib(meminfo, "SwapTotal") ?? 0) - (kib(meminfo, "SwapFree") ?? 0));
|
|
157
215
|
usedBytes = Math.max(0, machine - (kib(meminfo, "MemAvailable") ?? 0)) + swapBytes;
|