@camstack/system 1.2.122 → 1.2.123
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/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/kernel/heap-watch.d.ts +80 -1
- package/dist/{manifest-python-deps-76dZLxD0.js → manifest-python-deps-B3_4YiDK.js} +91 -6
- package/dist/{manifest-python-deps-DZSNlHjU.mjs → manifest-python-deps-DIIXlR1x.mjs} +91 -6
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
2
|
+
const require_manifest_python_deps = require("./manifest-python-deps-B3_4YiDK.js");
|
|
3
3
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
|
4
4
|
let node_path = require("node:path");
|
|
5
5
|
node_path = require_chunk.__toESM(node_path);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-
|
|
1
|
+
import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-DIIXlR1x.mjs";
|
|
2
2
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
3
3
|
import { register } from "node:module";
|
|
4
4
|
import * as path$1 from "node:path";
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const require_builtins_system_config_system_config_addon = require("./builtins/s
|
|
|
25
25
|
require("./builtins/system-config/index.js");
|
|
26
26
|
const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
|
|
27
27
|
const require_file_data_plane = require("./file-data-plane-DO8KbxCe.js");
|
|
28
|
-
const require_manifest_python_deps = require("./manifest-python-deps-
|
|
28
|
+
const require_manifest_python_deps = require("./manifest-python-deps-B3_4YiDK.js");
|
|
29
29
|
const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
|
|
30
30
|
const require_lan_http_bind = require("./lan-http-bind-DmgpFP6_.js");
|
|
31
31
|
const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
|
package/dist/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.
|
|
|
24
24
|
import "./builtins/system-config/index.mjs";
|
|
25
25
|
import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
|
|
26
26
|
import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-BhKdxJgf.mjs";
|
|
27
|
-
import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-
|
|
27
|
+
import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-DIIXlR1x.mjs";
|
|
28
28
|
import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
|
|
29
29
|
import { C as evaluateExistingCert, S as SERVER_AUTH_OID, _ as CA_COMMON_NAME, a as closeLanHttp, b as LEAF_RENEWAL_WINDOW_DAYS, c as readExtraSans, d as writeExtraSans, f as writeTlsMode, g as reissueTlsLeaf, h as loadTlsCert, i as bindPendingLanHttp, l as readTlsAccessStatus, m as ensureTlsCert, n as allFamiliesListenHost, o as readLanHttpState, p as validateUploadedTls, r as applyLanHttp, s as registerLanHttpHandler, t as DEFAULT_LAN_HTTP_PORT, u as readTlsMode, v as collectCertIdentity, x as MAX_LEAF_VALIDITY_DAYS, y as CA_VALIDITY_DAYS } from "./lan-http-bind-jKrj6OjQ.mjs";
|
|
30
30
|
import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
|
|
@@ -99,6 +99,85 @@ export declare const HEAP_RECLAIM_TRIGGER_MB = 1024;
|
|
|
99
99
|
* router's time, against a ~1.5GiB peak it removes each pass.
|
|
100
100
|
*/
|
|
101
101
|
export declare const HEAP_RECLAIM_MIN_INTERVAL_MS = 120000;
|
|
102
|
+
/**
|
|
103
|
+
* Consecutive floor-to-floor passes after which the reclaim is announced as
|
|
104
|
+
* STEADY STATE rather than a rescue.
|
|
105
|
+
*
|
|
106
|
+
* The floor above is documented as a bound on a BURST, on the measured premise
|
|
107
|
+
* that re-stranding the trigger takes ~20 minutes. Measured on the live hub
|
|
108
|
+
* 2026-08-25 it takes under two: 13 passes in 30 minutes, every one of them
|
|
109
|
+
* firing the instant {@link HEAP_RECLAIM_MIN_INTERVAL_MS} elapsed, `took=`
|
|
110
|
+
* 556-695ms each. Nothing in the record said so, because a pass that fires
|
|
111
|
+
* every floor period and a pass that fires once an hour print the same line.
|
|
112
|
+
*
|
|
113
|
+
* Six is 12 minutes of back-to-back passes: long enough that a genuine burst
|
|
114
|
+
* (which subsides) never reaches it, short enough that a process which has
|
|
115
|
+
* quietly made the stop-the-world part of its steady state is named within the
|
|
116
|
+
* quarter-hour. The warn changes no behaviour — the pass is load-bearing and
|
|
117
|
+
* keeps running. It exists so the OPERATOR is told that the safety net has
|
|
118
|
+
* become the mechanism, which is the moment to go after the allocation source
|
|
119
|
+
* instead of the symptom.
|
|
120
|
+
*/
|
|
121
|
+
export declare const RECLAIM_STEADY_STATE_PASSES = 6;
|
|
122
|
+
/**
|
|
123
|
+
* Event-loop delay over the window since the previous read, in milliseconds.
|
|
124
|
+
*
|
|
125
|
+
* Why the heartbeat carries this at all: hub-main's event loop is the cluster's
|
|
126
|
+
* only queue (D181 — every settings read from every runner and every agent,
|
|
127
|
+
* every UDS handshake and every cross-runner cap call is served on it, and a
|
|
128
|
+
* live CPU profile of a boot put `(idle)` at 0.1%). The reclaim pass is a
|
|
129
|
+
* stop-the-world ON that loop, and `took=` prices the PASS without ever pricing
|
|
130
|
+
* what it delays. 590ms against a p99 of 4ms is a visible stall; the same 590ms
|
|
131
|
+
* against a p99 of 900ms is noise inside a queue that was already saturated.
|
|
132
|
+
* Those are opposite conclusions, and until now the log could not tell them
|
|
133
|
+
* apart.
|
|
134
|
+
*/
|
|
135
|
+
export interface LoopDelaySample {
|
|
136
|
+
readonly p50Ms: number;
|
|
137
|
+
readonly p99Ms: number;
|
|
138
|
+
readonly maxMs: number;
|
|
139
|
+
}
|
|
140
|
+
/** What a loop with nothing recorded yet reports. */
|
|
141
|
+
export declare const ZERO_LOOP_DELAY: LoopDelaySample;
|
|
142
|
+
export interface LoopDelayMeter {
|
|
143
|
+
/** Percentiles over the window since the previous read. Reading RESETS it. */
|
|
144
|
+
readonly read: () => LoopDelaySample;
|
|
145
|
+
readonly stop: () => void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A `perf_hooks` interval histogram, read once per LOGGED line.
|
|
149
|
+
*
|
|
150
|
+
* The histogram samples in libuv at `resolutionMs` and costs nothing to keep
|
|
151
|
+
* enabled; the expensive part would be reading it on every 2s probe, which
|
|
152
|
+
* would also destroy the window the line claims to describe (probe and log
|
|
153
|
+
* cadences are deliberately different — see {@link startHeapWatch}).
|
|
154
|
+
*
|
|
155
|
+
* An EMPTY histogram is the trap: Node answers `min = 9223372036854776000` and
|
|
156
|
+
* `percentile()` throws on some versions, so a first heartbeat that read it
|
|
157
|
+
* naively would print `loopMax=9223372036854ms` and be read as a catastrophe on
|
|
158
|
+
* every boot. `count === 0` short-circuits to {@link ZERO_LOOP_DELAY}.
|
|
159
|
+
*
|
|
160
|
+
* Returns undefined if the platform declines, so the caller keeps a heartbeat
|
|
161
|
+
* either way.
|
|
162
|
+
*/
|
|
163
|
+
export declare function createLoopDelayMeter(resolutionMs?: number): LoopDelayMeter | undefined;
|
|
164
|
+
/** Whether anyone CHOSE this process's old-space ceiling, or V8 derived one. */
|
|
165
|
+
export type HeapCeilingOrigin = 'explicit' | 'v8-default';
|
|
166
|
+
/**
|
|
167
|
+
* Read the origin of the old-space ceiling off node's own options.
|
|
168
|
+
*
|
|
169
|
+
* hub-main is the only process in the cluster whose ceiling nobody chose. Every
|
|
170
|
+
* forked runner is given V8 heap flags by `process-service.ts`; hub-main gets
|
|
171
|
+
* `--max-old-space-size` from `docker-entrypoint.sh` ONLY when the operator sets
|
|
172
|
+
* `CAMSTACK_HUB_HEAP_MB`, and on the live container it is unset — verified
|
|
173
|
+
* 2026-08-25 by reading pid 7's argv, a bare `node .../starter.js`. So the
|
|
174
|
+
* ceiling in force is whatever V8 derives from the host (4288MB there), and the
|
|
175
|
+
* only record of that was a comment inside an image nobody rebuilds to read.
|
|
176
|
+
*
|
|
177
|
+
* A fact about the running process belongs in the running process's log, and
|
|
178
|
+
* `execArgv` answers it for free at boot.
|
|
179
|
+
*/
|
|
180
|
+
export declare function heapCeilingOrigin(execArgv?: readonly string[]): HeapCeilingOrigin;
|
|
102
181
|
export interface HeapSample {
|
|
103
182
|
readonly rssMb: number;
|
|
104
183
|
readonly heapUsedMb: number;
|
|
@@ -167,7 +246,7 @@ export interface HeapReclaimOptions {
|
|
|
167
246
|
* `unref`ed so it can never hold the process open, and every read is wrapped:
|
|
168
247
|
* a diagnostic that can crash the process it watches is worse than none.
|
|
169
248
|
*/
|
|
170
|
-
export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions): () => void;
|
|
249
|
+
export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions, loopDelay?: LoopDelayMeter | undefined, execArgv?: readonly string[]): () => void;
|
|
171
250
|
/**
|
|
172
251
|
* Steady heartbeat period for a HEAVY addon runner.
|
|
173
252
|
*
|
|
@@ -12,6 +12,7 @@ node_os = require_chunk.__toESM(node_os);
|
|
|
12
12
|
let node_fs = require("node:fs");
|
|
13
13
|
node_fs = require_chunk.__toESM(node_fs);
|
|
14
14
|
let node_http = require("node:http");
|
|
15
|
+
let node_perf_hooks = require("node:perf_hooks");
|
|
15
16
|
let node_v8 = require("node:v8");
|
|
16
17
|
node_v8 = require_chunk.__toESM(node_v8);
|
|
17
18
|
let node_vm = require("node:vm");
|
|
@@ -170,6 +171,72 @@ var HEAP_RECLAIM_TRIGGER_MB = 1024;
|
|
|
170
171
|
* router's time, against a ~1.5GiB peak it removes each pass.
|
|
171
172
|
*/
|
|
172
173
|
var HEAP_RECLAIM_MIN_INTERVAL_MS = 12e4;
|
|
174
|
+
/** What a loop with nothing recorded yet reports. */
|
|
175
|
+
var ZERO_LOOP_DELAY = {
|
|
176
|
+
p50Ms: 0,
|
|
177
|
+
p99Ms: 0,
|
|
178
|
+
maxMs: 0
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* A `perf_hooks` interval histogram, read once per LOGGED line.
|
|
182
|
+
*
|
|
183
|
+
* The histogram samples in libuv at `resolutionMs` and costs nothing to keep
|
|
184
|
+
* enabled; the expensive part would be reading it on every 2s probe, which
|
|
185
|
+
* would also destroy the window the line claims to describe (probe and log
|
|
186
|
+
* cadences are deliberately different — see {@link startHeapWatch}).
|
|
187
|
+
*
|
|
188
|
+
* An EMPTY histogram is the trap: Node answers `min = 9223372036854776000` and
|
|
189
|
+
* `percentile()` throws on some versions, so a first heartbeat that read it
|
|
190
|
+
* naively would print `loopMax=9223372036854ms` and be read as a catastrophe on
|
|
191
|
+
* every boot. `count === 0` short-circuits to {@link ZERO_LOOP_DELAY}.
|
|
192
|
+
*
|
|
193
|
+
* Returns undefined if the platform declines, so the caller keeps a heartbeat
|
|
194
|
+
* either way.
|
|
195
|
+
*/
|
|
196
|
+
function createLoopDelayMeter(resolutionMs = 20) {
|
|
197
|
+
try {
|
|
198
|
+
const histogram = (0, node_perf_hooks.monitorEventLoopDelay)({ resolution: resolutionMs });
|
|
199
|
+
histogram.enable();
|
|
200
|
+
return {
|
|
201
|
+
read: () => {
|
|
202
|
+
try {
|
|
203
|
+
if (histogram.count === 0) return ZERO_LOOP_DELAY;
|
|
204
|
+
const sample = {
|
|
205
|
+
p50Ms: Math.round(histogram.percentile(50) / 1e6),
|
|
206
|
+
p99Ms: Math.round(histogram.percentile(99) / 1e6),
|
|
207
|
+
maxMs: Math.round(histogram.max / 1e6)
|
|
208
|
+
};
|
|
209
|
+
histogram.reset();
|
|
210
|
+
return sample;
|
|
211
|
+
} catch {
|
|
212
|
+
return ZERO_LOOP_DELAY;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
stop: () => {
|
|
216
|
+
histogram.disable();
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Read the origin of the old-space ceiling off node's own options.
|
|
225
|
+
*
|
|
226
|
+
* hub-main is the only process in the cluster whose ceiling nobody chose. Every
|
|
227
|
+
* forked runner is given V8 heap flags by `process-service.ts`; hub-main gets
|
|
228
|
+
* `--max-old-space-size` from `docker-entrypoint.sh` ONLY when the operator sets
|
|
229
|
+
* `CAMSTACK_HUB_HEAP_MB`, and on the live container it is unset — verified
|
|
230
|
+
* 2026-08-25 by reading pid 7's argv, a bare `node .../starter.js`. So the
|
|
231
|
+
* ceiling in force is whatever V8 derives from the host (4288MB there), and the
|
|
232
|
+
* only record of that was a comment inside an image nobody rebuilds to read.
|
|
233
|
+
*
|
|
234
|
+
* A fact about the running process belongs in the running process's log, and
|
|
235
|
+
* `execArgv` answers it for free at boot.
|
|
236
|
+
*/
|
|
237
|
+
function heapCeilingOrigin(execArgv = process.execArgv) {
|
|
238
|
+
return execArgv.some((arg) => /^--max[-_]old[-_]space[-_]size(=|$)/.test(arg)) ? "explicit" : "v8-default";
|
|
239
|
+
}
|
|
173
240
|
var MB = (bytes) => Math.round(bytes / 1048576);
|
|
174
241
|
/**
|
|
175
242
|
* Build a sample from raw readings. Pure so the thresholds are testable without
|
|
@@ -233,8 +300,10 @@ function createV8Reclaimer() {
|
|
|
233
300
|
return;
|
|
234
301
|
}
|
|
235
302
|
}
|
|
236
|
-
function format(label, s) {
|
|
237
|
-
|
|
303
|
+
function format(label, s, loop) {
|
|
304
|
+
const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
|
|
305
|
+
if (loop === void 0) return line;
|
|
306
|
+
return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms`;
|
|
238
307
|
}
|
|
239
308
|
/**
|
|
240
309
|
* Start the heartbeat. Returns a stop function.
|
|
@@ -242,7 +311,7 @@ function format(label, s) {
|
|
|
242
311
|
* `unref`ed so it can never hold the process open, and every read is wrapped:
|
|
243
312
|
* a diagnostic that can crash the process it watches is worse than none.
|
|
244
313
|
*/
|
|
245
|
-
function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions) {
|
|
314
|
+
function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv) {
|
|
246
315
|
const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
|
|
247
316
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
248
317
|
const triggerMb = reclaimOptions?.triggerMb ?? 1024;
|
|
@@ -251,14 +320,24 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
251
320
|
const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
|
|
252
321
|
const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
|
|
253
322
|
let lastReclaimAt = Number.NEGATIVE_INFINITY;
|
|
323
|
+
/** Consecutive passes that fired the instant the floor elapsed. See
|
|
324
|
+
* {@link RECLAIM_STEADY_STATE_PASSES}. */
|
|
325
|
+
let passesAtFloor = 0;
|
|
326
|
+
let steadyStateAnnounced = false;
|
|
254
327
|
const read = () => {
|
|
255
328
|
const limit = reclaimOptions?.heapLimitBytes ?? node_v8.getHeapStatistics().heap_size_limit;
|
|
256
329
|
return buildHeapSample(readMemory(), limit);
|
|
257
330
|
};
|
|
258
331
|
const maybeReclaim = (sample) => {
|
|
259
332
|
if (reclaimOptions === void 0) return;
|
|
260
|
-
if (!shouldReclaim(sample, triggerMb))
|
|
261
|
-
|
|
333
|
+
if (!shouldReclaim(sample, triggerMb)) {
|
|
334
|
+
passesAtFloor = 0;
|
|
335
|
+
steadyStateAnnounced = false;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const sinceLast = now() - lastReclaimAt;
|
|
339
|
+
if (sinceLast < minIntervalMs) return;
|
|
340
|
+
passesAtFloor = sinceLast <= minIntervalMs * 2 ? passesAtFloor + 1 : 1;
|
|
262
341
|
lastReclaimAt = now();
|
|
263
342
|
const startedAt = now();
|
|
264
343
|
try {
|
|
@@ -269,6 +348,10 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
269
348
|
}
|
|
270
349
|
const after = read();
|
|
271
350
|
sink.info(`[mem] reclaim ${label} stranded=${strandedMb(sample)}MB rss=${sample.rssMb}MB→${after.rssMb}MB freed=${sample.rssMb - after.rssMb}MB arrayBuffers=${sample.arrayBuffersMb}MB→${after.arrayBuffersMb}MB took=${now() - startedAt}ms`);
|
|
351
|
+
if (passesAtFloor >= 6 && !steadyStateAnnounced) {
|
|
352
|
+
steadyStateAnnounced = true;
|
|
353
|
+
sink.warn(`[mem] reclaim ${label} has run at the floor for ${passesAtFloor} consecutive passes (every ${minIntervalMs}ms) — the stop-the-world is now STEADY STATE, not a rescue. The allocation source is what needs the fix; this pass only bounds its peak.`);
|
|
354
|
+
}
|
|
272
355
|
};
|
|
273
356
|
let mode = "steady";
|
|
274
357
|
let lastLoggedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -297,7 +380,7 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
297
380
|
const due = at - lastLoggedAt >= intervalMs;
|
|
298
381
|
if (mode === "escalated" || due) {
|
|
299
382
|
lastLoggedAt = at;
|
|
300
|
-
const line = format(label, sample);
|
|
383
|
+
const line = format(label, sample, loopDelay?.read());
|
|
301
384
|
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
302
385
|
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
303
386
|
else sink.info(line);
|
|
@@ -309,11 +392,13 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
309
392
|
const timer = setInterval(tick, probeIntervalMs);
|
|
310
393
|
timer.unref?.();
|
|
311
394
|
tick();
|
|
395
|
+
if (heapCeilingOrigin(execArgv) === "v8-default") sink.info(`[mem] ${label} heap ceiling is V8's DEFAULT (${read().heapLimitMb}MB) — no --max-old-space-size on argv. Nothing here CHOSE that number; it is derived from host RAM and moves with it.`);
|
|
312
396
|
let stopped = false;
|
|
313
397
|
return () => {
|
|
314
398
|
if (stopped) return;
|
|
315
399
|
stopped = true;
|
|
316
400
|
clearInterval(timer);
|
|
401
|
+
loopDelay?.stop();
|
|
317
402
|
};
|
|
318
403
|
}
|
|
319
404
|
/**
|
|
@@ -10,6 +10,7 @@ import * as os from "node:os";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
13
14
|
import * as v8 from "node:v8";
|
|
14
15
|
import * as vm from "node:vm";
|
|
15
16
|
import { BOOT_RECOVERY_BACKOFF_MS, DATAPLANE_SECRET_HEADER, DeviceType, DisposerChain, EventCategory, ReadinessRegistry, asJsonObject, asString, createDeviceProxy, createEventBusSliceSource, deviceOpsCapability, emitReadiness, errMsg, expandCapMethods, readNodePin, scopeKey, sleep, withCapInputDefaults } from "@camstack/types/addon";
|
|
@@ -166,6 +167,72 @@ var HEAP_RECLAIM_TRIGGER_MB = 1024;
|
|
|
166
167
|
* router's time, against a ~1.5GiB peak it removes each pass.
|
|
167
168
|
*/
|
|
168
169
|
var HEAP_RECLAIM_MIN_INTERVAL_MS = 12e4;
|
|
170
|
+
/** What a loop with nothing recorded yet reports. */
|
|
171
|
+
var ZERO_LOOP_DELAY = {
|
|
172
|
+
p50Ms: 0,
|
|
173
|
+
p99Ms: 0,
|
|
174
|
+
maxMs: 0
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* A `perf_hooks` interval histogram, read once per LOGGED line.
|
|
178
|
+
*
|
|
179
|
+
* The histogram samples in libuv at `resolutionMs` and costs nothing to keep
|
|
180
|
+
* enabled; the expensive part would be reading it on every 2s probe, which
|
|
181
|
+
* would also destroy the window the line claims to describe (probe and log
|
|
182
|
+
* cadences are deliberately different — see {@link startHeapWatch}).
|
|
183
|
+
*
|
|
184
|
+
* An EMPTY histogram is the trap: Node answers `min = 9223372036854776000` and
|
|
185
|
+
* `percentile()` throws on some versions, so a first heartbeat that read it
|
|
186
|
+
* naively would print `loopMax=9223372036854ms` and be read as a catastrophe on
|
|
187
|
+
* every boot. `count === 0` short-circuits to {@link ZERO_LOOP_DELAY}.
|
|
188
|
+
*
|
|
189
|
+
* Returns undefined if the platform declines, so the caller keeps a heartbeat
|
|
190
|
+
* either way.
|
|
191
|
+
*/
|
|
192
|
+
function createLoopDelayMeter(resolutionMs = 20) {
|
|
193
|
+
try {
|
|
194
|
+
const histogram = monitorEventLoopDelay({ resolution: resolutionMs });
|
|
195
|
+
histogram.enable();
|
|
196
|
+
return {
|
|
197
|
+
read: () => {
|
|
198
|
+
try {
|
|
199
|
+
if (histogram.count === 0) return ZERO_LOOP_DELAY;
|
|
200
|
+
const sample = {
|
|
201
|
+
p50Ms: Math.round(histogram.percentile(50) / 1e6),
|
|
202
|
+
p99Ms: Math.round(histogram.percentile(99) / 1e6),
|
|
203
|
+
maxMs: Math.round(histogram.max / 1e6)
|
|
204
|
+
};
|
|
205
|
+
histogram.reset();
|
|
206
|
+
return sample;
|
|
207
|
+
} catch {
|
|
208
|
+
return ZERO_LOOP_DELAY;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
stop: () => {
|
|
212
|
+
histogram.disable();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Read the origin of the old-space ceiling off node's own options.
|
|
221
|
+
*
|
|
222
|
+
* hub-main is the only process in the cluster whose ceiling nobody chose. Every
|
|
223
|
+
* forked runner is given V8 heap flags by `process-service.ts`; hub-main gets
|
|
224
|
+
* `--max-old-space-size` from `docker-entrypoint.sh` ONLY when the operator sets
|
|
225
|
+
* `CAMSTACK_HUB_HEAP_MB`, and on the live container it is unset — verified
|
|
226
|
+
* 2026-08-25 by reading pid 7's argv, a bare `node .../starter.js`. So the
|
|
227
|
+
* ceiling in force is whatever V8 derives from the host (4288MB there), and the
|
|
228
|
+
* only record of that was a comment inside an image nobody rebuilds to read.
|
|
229
|
+
*
|
|
230
|
+
* A fact about the running process belongs in the running process's log, and
|
|
231
|
+
* `execArgv` answers it for free at boot.
|
|
232
|
+
*/
|
|
233
|
+
function heapCeilingOrigin(execArgv = process.execArgv) {
|
|
234
|
+
return execArgv.some((arg) => /^--max[-_]old[-_]space[-_]size(=|$)/.test(arg)) ? "explicit" : "v8-default";
|
|
235
|
+
}
|
|
169
236
|
var MB = (bytes) => Math.round(bytes / 1048576);
|
|
170
237
|
/**
|
|
171
238
|
* Build a sample from raw readings. Pure so the thresholds are testable without
|
|
@@ -229,8 +296,10 @@ function createV8Reclaimer() {
|
|
|
229
296
|
return;
|
|
230
297
|
}
|
|
231
298
|
}
|
|
232
|
-
function format(label, s) {
|
|
233
|
-
|
|
299
|
+
function format(label, s, loop) {
|
|
300
|
+
const line = `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
|
|
301
|
+
if (loop === void 0) return line;
|
|
302
|
+
return `${line} loopP50=${loop.p50Ms}ms loopP99=${loop.p99Ms}ms loopMax=${loop.maxMs}ms`;
|
|
234
303
|
}
|
|
235
304
|
/**
|
|
236
305
|
* Start the heartbeat. Returns a stop function.
|
|
@@ -238,7 +307,7 @@ function format(label, s) {
|
|
|
238
307
|
* `unref`ed so it can never hold the process open, and every read is wrapped:
|
|
239
308
|
* a diagnostic that can crash the process it watches is worse than none.
|
|
240
309
|
*/
|
|
241
|
-
function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions) {
|
|
310
|
+
function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions, loopDelay = createLoopDelayMeter(), execArgv = process.execArgv) {
|
|
242
311
|
const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
|
|
243
312
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
244
313
|
const triggerMb = reclaimOptions?.triggerMb ?? 1024;
|
|
@@ -247,14 +316,24 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
247
316
|
const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
|
|
248
317
|
const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
|
|
249
318
|
let lastReclaimAt = Number.NEGATIVE_INFINITY;
|
|
319
|
+
/** Consecutive passes that fired the instant the floor elapsed. See
|
|
320
|
+
* {@link RECLAIM_STEADY_STATE_PASSES}. */
|
|
321
|
+
let passesAtFloor = 0;
|
|
322
|
+
let steadyStateAnnounced = false;
|
|
250
323
|
const read = () => {
|
|
251
324
|
const limit = reclaimOptions?.heapLimitBytes ?? v8.getHeapStatistics().heap_size_limit;
|
|
252
325
|
return buildHeapSample(readMemory(), limit);
|
|
253
326
|
};
|
|
254
327
|
const maybeReclaim = (sample) => {
|
|
255
328
|
if (reclaimOptions === void 0) return;
|
|
256
|
-
if (!shouldReclaim(sample, triggerMb))
|
|
257
|
-
|
|
329
|
+
if (!shouldReclaim(sample, triggerMb)) {
|
|
330
|
+
passesAtFloor = 0;
|
|
331
|
+
steadyStateAnnounced = false;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const sinceLast = now() - lastReclaimAt;
|
|
335
|
+
if (sinceLast < minIntervalMs) return;
|
|
336
|
+
passesAtFloor = sinceLast <= minIntervalMs * 2 ? passesAtFloor + 1 : 1;
|
|
258
337
|
lastReclaimAt = now();
|
|
259
338
|
const startedAt = now();
|
|
260
339
|
try {
|
|
@@ -265,6 +344,10 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
265
344
|
}
|
|
266
345
|
const after = read();
|
|
267
346
|
sink.info(`[mem] reclaim ${label} stranded=${strandedMb(sample)}MB rss=${sample.rssMb}MB→${after.rssMb}MB freed=${sample.rssMb - after.rssMb}MB arrayBuffers=${sample.arrayBuffersMb}MB→${after.arrayBuffersMb}MB took=${now() - startedAt}ms`);
|
|
347
|
+
if (passesAtFloor >= 6 && !steadyStateAnnounced) {
|
|
348
|
+
steadyStateAnnounced = true;
|
|
349
|
+
sink.warn(`[mem] reclaim ${label} has run at the floor for ${passesAtFloor} consecutive passes (every ${minIntervalMs}ms) — the stop-the-world is now STEADY STATE, not a rescue. The allocation source is what needs the fix; this pass only bounds its peak.`);
|
|
350
|
+
}
|
|
268
351
|
};
|
|
269
352
|
let mode = "steady";
|
|
270
353
|
let lastLoggedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -293,7 +376,7 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
293
376
|
const due = at - lastLoggedAt >= intervalMs;
|
|
294
377
|
if (mode === "escalated" || due) {
|
|
295
378
|
lastLoggedAt = at;
|
|
296
|
-
const line = format(label, sample);
|
|
379
|
+
const line = format(label, sample, loopDelay?.read());
|
|
297
380
|
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
298
381
|
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
299
382
|
else sink.info(line);
|
|
@@ -305,11 +388,13 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
|
|
|
305
388
|
const timer = setInterval(tick, probeIntervalMs);
|
|
306
389
|
timer.unref?.();
|
|
307
390
|
tick();
|
|
391
|
+
if (heapCeilingOrigin(execArgv) === "v8-default") sink.info(`[mem] ${label} heap ceiling is V8's DEFAULT (${read().heapLimitMb}MB) — no --max-old-space-size on argv. Nothing here CHOSE that number; it is derived from host RAM and moves with it.`);
|
|
308
392
|
let stopped = false;
|
|
309
393
|
return () => {
|
|
310
394
|
if (stopped) return;
|
|
311
395
|
stopped = true;
|
|
312
396
|
clearInterval(timer);
|
|
397
|
+
loopDelay?.stop();
|
|
313
398
|
};
|
|
314
399
|
}
|
|
315
400
|
/**
|