@camstack/system 1.2.219 → 1.2.220
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 +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{custom-action-registry-DKWhaWL1.js → custom-action-registry-B42MC_Pv.js} +1 -1
- package/dist/{custom-action-registry-B6gOtUWA.mjs → custom-action-registry-CpR58DXT.mjs} +1 -1
- package/dist/{dist-Cyz5_i7z.mjs → dist-Ca_jaxXs.mjs} +230 -2
- package/dist/{dist-B17W9ngu.js → dist-CllKpcTd.js} +230 -2
- package/dist/index.js +71 -32
- package/dist/index.mjs +65 -33
- package/dist/kernel/gc-census.d.ts +144 -0
- package/dist/kernel/heap-watch.d.ts +8 -0
- package/dist/kernel/socket-plane-report.d.ts +37 -0
- package/dist/kernel/transport/cap-action-census.d.ts +218 -0
- package/dist/kernel/transport/cap-child-index.d.ts +49 -0
- package/dist/kernel/transport/index.d.ts +2 -0
- package/dist/kernel/transport/local-child-registry.d.ts +63 -4
- package/dist/kernel/transport/local-transport.d.ts +9 -0
- package/dist/kernel/transport/socket-channel.d.ts +21 -0
- package/dist/{manifest-system-deps-g_nK0xsF.js → manifest-system-deps-DfpwaXkh.js} +550 -27
- package/dist/{manifest-system-deps-C7dihwWT.mjs → manifest-system-deps-iadzxSBr.mjs} +509 -28
- package/dist/{retired-settings-keys-CjvB7Etx.js → retired-settings-keys-7iEsSzmD.js} +1 -1
- package/dist/{retired-settings-keys-BCYUrdsA.mjs → retired-settings-keys-DQnfr__p.mjs} +1 -1
- package/package.json +1 -1
|
@@ -152,6 +152,166 @@ function formatEventPlane(window) {
|
|
|
152
152
|
return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
|
|
153
153
|
}
|
|
154
154
|
//#endregion
|
|
155
|
+
//#region src/kernel/transport/cap-action-census.ts
|
|
156
|
+
/** A process that has never read the census. */
|
|
157
|
+
var EMPTY_CAP_ACTION_READING = /* @__PURE__ */ new Map();
|
|
158
|
+
/**
|
|
159
|
+
* Bytes that crossed on a frame carrying no cap envelope.
|
|
160
|
+
*
|
|
161
|
+
* Named rather than omitted so the census's coverage is readable off its own
|
|
162
|
+
* output: `register`, `addon-call`, log frames and the event fan-out all land
|
|
163
|
+
* here. A census that reported only what it could name would look like a
|
|
164
|
+
* complete attribution of whatever share it happened to cover.
|
|
165
|
+
*/
|
|
166
|
+
var CAP_ACTION_UNLABELLED = "(unlabelled)";
|
|
167
|
+
/** Bytes of actions past {@link MAX_CAP_ACTIONS_PER_PEER} on one peer. */
|
|
168
|
+
var CAP_ACTION_OVERFLOW_LABEL = "(overflow)";
|
|
169
|
+
/**
|
|
170
|
+
* Distinct named actions kept per peer before the overflow bucket takes over.
|
|
171
|
+
*
|
|
172
|
+
* Sixty-four against a measured worst case of 14 (`pipeline-analytics`, the
|
|
173
|
+
* heaviest peer on this hub, 2026-09-11) — four times the observed shape, so a
|
|
174
|
+
* peer has to change character before it overflows, and a bound that is never
|
|
175
|
+
* reached in practice still forbids the unbounded growth a wire-supplied key
|
|
176
|
+
* makes possible in principle.
|
|
177
|
+
*/
|
|
178
|
+
var MAX_CAP_ACTIONS_PER_PEER = 64;
|
|
179
|
+
/**
|
|
180
|
+
* `<cap>.<method>` for a cap envelope, `null` for anything else.
|
|
181
|
+
*
|
|
182
|
+
* Both directions are labelled by the same function: a child's `cap-call-out`
|
|
183
|
+
* and the parent's `cap-call` carry `capName` + `method` in the same two keys,
|
|
184
|
+
* so one predicate covers the whole plane. Anything else — a `register`, an
|
|
185
|
+
* `addon-call`, a log or an event — is deliberately unlabelled; see
|
|
186
|
+
* {@link CAP_ACTION_UNLABELLED} for where its bytes go.
|
|
187
|
+
*
|
|
188
|
+
* Cost: two property reads, two `typeof`s and one concatenation of two short
|
|
189
|
+
* strings, called once per `req` frame (not per frame). Against the
|
|
190
|
+
* `encodeFrame` it sits beside — 1 948–1 975 ns on a 0.33 kB body — it is
|
|
191
|
+
* noise, and unlike `recordSocketFrame` it is not on the `evt` path at all.
|
|
192
|
+
*/
|
|
193
|
+
function capActionLabel(body) {
|
|
194
|
+
if (typeof body !== "object" || body === null) return null;
|
|
195
|
+
const env = body;
|
|
196
|
+
if (env.kind !== "cap-call" && env.kind !== "cap-call-out") return null;
|
|
197
|
+
if (typeof env.capName !== "string" || typeof env.method !== "string") return null;
|
|
198
|
+
return `${env.capName}.${env.method}`;
|
|
199
|
+
}
|
|
200
|
+
function emptyCounters() {
|
|
201
|
+
return {
|
|
202
|
+
calls: 0,
|
|
203
|
+
reqBytes: 0,
|
|
204
|
+
resBytes: 0,
|
|
205
|
+
raw: 0,
|
|
206
|
+
decoded: 0
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function createCapActionCensus() {
|
|
210
|
+
const peers = /* @__PURE__ */ new Map();
|
|
211
|
+
/**
|
|
212
|
+
* The per-peer slot for `action`, creating it under the bound.
|
|
213
|
+
*
|
|
214
|
+
* Past {@link MAX_CAP_ACTIONS_PER_PEER} a NEW label is folded into the
|
|
215
|
+
* overflow bucket; labels already present keep their own slot, so a peer
|
|
216
|
+
* that overflows does not lose the attribution it had built.
|
|
217
|
+
*/
|
|
218
|
+
const slot = (peerId, action) => {
|
|
219
|
+
let byAction = peers.get(peerId);
|
|
220
|
+
if (byAction === void 0) {
|
|
221
|
+
byAction = /* @__PURE__ */ new Map();
|
|
222
|
+
peers.set(peerId, byAction);
|
|
223
|
+
}
|
|
224
|
+
const existing = byAction.get(action);
|
|
225
|
+
if (existing !== void 0) return existing;
|
|
226
|
+
if (byAction.size >= 64) {
|
|
227
|
+
let overflow = byAction.get(CAP_ACTION_OVERFLOW_LABEL);
|
|
228
|
+
if (overflow === void 0) {
|
|
229
|
+
overflow = emptyCounters();
|
|
230
|
+
byAction.set(CAP_ACTION_OVERFLOW_LABEL, overflow);
|
|
231
|
+
}
|
|
232
|
+
return overflow;
|
|
233
|
+
}
|
|
234
|
+
const fresh = emptyCounters();
|
|
235
|
+
byAction.set(action, fresh);
|
|
236
|
+
return fresh;
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
record(peerId, action, reqBytes, resBytes) {
|
|
240
|
+
const c = slot(peerId, action);
|
|
241
|
+
c.calls += 1;
|
|
242
|
+
c.reqBytes += reqBytes;
|
|
243
|
+
c.resBytes += resBytes;
|
|
244
|
+
},
|
|
245
|
+
recordForward(peerId, action, kind) {
|
|
246
|
+
const c = slot(peerId, action);
|
|
247
|
+
if (kind === "raw") c.raw += 1;
|
|
248
|
+
else c.decoded += 1;
|
|
249
|
+
},
|
|
250
|
+
recordUnlabelled(peerId, bytes) {
|
|
251
|
+
const c = slot(peerId, CAP_ACTION_UNLABELLED);
|
|
252
|
+
c.resBytes += bytes;
|
|
253
|
+
},
|
|
254
|
+
forget(peerId) {
|
|
255
|
+
peers.delete(peerId);
|
|
256
|
+
},
|
|
257
|
+
read() {
|
|
258
|
+
const out = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const [peerId, byAction] of peers) {
|
|
260
|
+
const copy = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const [action, c] of byAction) copy.set(action, {
|
|
262
|
+
calls: c.calls,
|
|
263
|
+
reqBytes: c.reqBytes,
|
|
264
|
+
resBytes: c.resBytes,
|
|
265
|
+
raw: c.raw,
|
|
266
|
+
decoded: c.decoded
|
|
267
|
+
});
|
|
268
|
+
out.set(peerId, copy);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function sampleOf(reading, peerId, action) {
|
|
275
|
+
return reading.get(peerId)?.get(action);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Subtract the previous reading from the current one.
|
|
279
|
+
*
|
|
280
|
+
* A peer whose runner RESPAWNED reconnects under the same id with counters
|
|
281
|
+
* that went backwards. Clamped at 0, exactly as `diffSocketPlane` clamps for
|
|
282
|
+
* the same reason: its traffic in that window is genuinely unknown, and a
|
|
283
|
+
* negative number on a rate reads as a broken meter.
|
|
284
|
+
*/
|
|
285
|
+
function diffCapActionCensus(before, current, topN) {
|
|
286
|
+
const deltas = [];
|
|
287
|
+
let labelledBytes = 0;
|
|
288
|
+
let totalBytes = 0;
|
|
289
|
+
for (const [peerId, byAction] of current) for (const [action, now] of byAction) {
|
|
290
|
+
const then = sampleOf(before, peerId, action);
|
|
291
|
+
const reqBytes = Math.max(0, now.reqBytes - (then?.reqBytes ?? 0));
|
|
292
|
+
const resBytes = Math.max(0, now.resBytes - (then?.resBytes ?? 0));
|
|
293
|
+
const bytes = reqBytes + resBytes;
|
|
294
|
+
totalBytes += bytes;
|
|
295
|
+
if (action === "(unlabelled)" || action === "(overflow)") continue;
|
|
296
|
+
labelledBytes += bytes;
|
|
297
|
+
if (bytes === 0) continue;
|
|
298
|
+
deltas.push({
|
|
299
|
+
peerId,
|
|
300
|
+
action,
|
|
301
|
+
calls: Math.max(0, now.calls - (then?.calls ?? 0)),
|
|
302
|
+
reqBytes,
|
|
303
|
+
resBytes,
|
|
304
|
+
raw: Math.max(0, now.raw - (then?.raw ?? 0)),
|
|
305
|
+
decoded: Math.max(0, now.decoded - (then?.decoded ?? 0))
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
top: deltas.toSorted((a, b) => b.reqBytes + b.resBytes - (a.reqBytes + a.resBytes) || a.peerId.localeCompare(b.peerId) || a.action.localeCompare(b.action)).slice(0, topN),
|
|
310
|
+
labelledBytes,
|
|
311
|
+
totalBytes
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
155
315
|
//#region src/kernel/transport/socket-traffic.ts
|
|
156
316
|
function createSocketDirectionCounters() {
|
|
157
317
|
return {
|
|
@@ -313,10 +473,12 @@ function createSocketPlaneReader(sources) {
|
|
|
313
473
|
});
|
|
314
474
|
}
|
|
315
475
|
const forward = registry.readForwardCensus?.();
|
|
476
|
+
const actions = registry.readActionCensus?.();
|
|
316
477
|
return {
|
|
317
478
|
peers,
|
|
318
479
|
peersConnected: listed.length,
|
|
319
|
-
...forward === void 0 ? {} : { forward }
|
|
480
|
+
...forward === void 0 ? {} : { forward },
|
|
481
|
+
...actions === void 0 ? {} : { actions }
|
|
320
482
|
};
|
|
321
483
|
} catch {
|
|
322
484
|
return;
|
|
@@ -419,6 +581,7 @@ function diffSocketPlane(baseline, current, topN = 5) {
|
|
|
419
581
|
if (peerMessages(delta) > 0) deltas.push(delta);
|
|
420
582
|
}
|
|
421
583
|
const forward = diffForward(current.forward, baseline.forward);
|
|
584
|
+
const actions = current.actions === void 0 ? void 0 : diffCapActionCensus(baseline.actions ?? EMPTY_CAP_ACTION_READING, current.actions, 5);
|
|
422
585
|
return {
|
|
423
586
|
peersConnected: current.peersConnected,
|
|
424
587
|
peersMeasured: current.peers.length,
|
|
@@ -427,7 +590,8 @@ function diffSocketPlane(baseline, current, topN = 5) {
|
|
|
427
590
|
top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
428
591
|
topBytes: deltas.toSorted((a, b) => peerBytes(b) - peerBytes(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
429
592
|
queued: readSocketQueues(current),
|
|
430
|
-
...forward === void 0 ? {} : { forward }
|
|
593
|
+
...forward === void 0 ? {} : { forward },
|
|
594
|
+
...actions === void 0 ? {} : { actions }
|
|
431
595
|
};
|
|
432
596
|
}
|
|
433
597
|
function createSocketPlaneMeter(reader, topN = 5) {
|
|
@@ -438,7 +602,8 @@ function createSocketPlaneMeter(reader, topN = 5) {
|
|
|
438
602
|
const window = diffSocketPlane(baseline, current, topN);
|
|
439
603
|
baseline = {
|
|
440
604
|
peers: new Map(current.peers.map((p) => [p.peerId, p])),
|
|
441
|
-
...current.forward === void 0 ? {} : { forward: current.forward }
|
|
605
|
+
...current.forward === void 0 ? {} : { forward: current.forward },
|
|
606
|
+
...current.actions === void 0 ? {} : { actions: current.actions }
|
|
442
607
|
};
|
|
443
608
|
return window;
|
|
444
609
|
} };
|
|
@@ -475,6 +640,24 @@ function formatPeer(delta) {
|
|
|
475
640
|
return `${delta.peerId}:tx=${delta.tx.reqMessages}/${delta.tx.resMessages}/${delta.tx.evtMessages}:rx=${delta.rx.reqMessages}/${delta.rx.resMessages}/${delta.rx.evtMessages}:txkB=${formatPeerBytes(delta.tx)}:rxkB=${formatPeerBytes(delta.rx)}`;
|
|
476
641
|
}
|
|
477
642
|
/**
|
|
643
|
+
* One named action: `<peer>!<cap>.<method>:<kB>kB/<calls>:r<raw>/d<decoded>`.
|
|
644
|
+
*
|
|
645
|
+
* `!` separates the peer from the action because `.` already separates cap
|
|
646
|
+
* from method and a single delimiter for both would make the token ambiguous
|
|
647
|
+
* to split.
|
|
648
|
+
*
|
|
649
|
+
* The raw/decoded pair is the half that did not exist anywhere before D456.
|
|
650
|
+
* `socketFwd=` answers it for the whole plane — 30 349/5 051 on this hub — and
|
|
651
|
+
* a plane-wide ratio cannot tell a method that is entirely forwarded from one
|
|
652
|
+
* that is entirely decoded, which is the difference between bytes hub-main
|
|
653
|
+
* copies and bytes hub-main reads. The census pays for the two counters it
|
|
654
|
+
* takes; the conclusion they buy is which of the line's megabytes are actually
|
|
655
|
+
* expensive.
|
|
656
|
+
*/
|
|
657
|
+
function formatAction(entry) {
|
|
658
|
+
return `${entry.peerId}!${entry.action}:${kb$1(entry.reqBytes + entry.resBytes)}kB/${entry.calls}:r${entry.raw}/d${entry.decoded}`;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
478
661
|
* The suffix appended to a `[mem]` line — leading space included, empty when
|
|
479
662
|
* this process has no local socket plane.
|
|
480
663
|
*
|
|
@@ -500,7 +683,7 @@ function formatSocketPlane(window) {
|
|
|
500
683
|
const named = new Set(window.top.map((p) => p.peerId));
|
|
501
684
|
const topBytes = window.topBytes.filter((p) => !named.has(p.peerId)).map(formatPeer).join(",");
|
|
502
685
|
const queuedTop = window.queued.top.map((p) => `${p.peerId}:${kb$1(p.bytes)}`).join(",");
|
|
503
|
-
return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`) + (topBytes.length === 0 ? "" : ` socketTopBytes=${topBytes}`) + ` socketQueued=${kb$1(window.queued.totalBytes)}kB` + (window.queued.unmeasured === 0 ? "" : ` socketQueuedUnmeasured=${window.queued.unmeasured}`) + (queuedTop.length === 0 ? "" : ` socketQueuedTop=${queuedTop}`) + (window.forward === void 0 ? "" : ` socketFwd=${window.forward.raw}/${window.forward.decoded}`);
|
|
686
|
+
return ` socketPeers=${window.peersMeasured}/${window.peersConnected} socketTx=${formatDirection(window.tx)} socketRx=${formatDirection(window.rx)}` + (top.length === 0 ? "" : ` socketTop=${top}`) + (topBytes.length === 0 ? "" : ` socketTopBytes=${topBytes}`) + ` socketQueued=${kb$1(window.queued.totalBytes)}kB` + (window.queued.unmeasured === 0 ? "" : ` socketQueuedUnmeasured=${window.queued.unmeasured}`) + (queuedTop.length === 0 ? "" : ` socketQueuedTop=${queuedTop}`) + (window.forward === void 0 ? "" : ` socketFwd=${window.forward.raw}/${window.forward.decoded}`) + (window.actions === void 0 ? "" : ` socketActions=${kb$1(window.actions.labelledBytes)}/${kb$1(window.actions.totalBytes)}kB` + (window.actions.top.length === 0 ? "" : ` socketTopActions=${window.actions.top.map(formatAction).join(",")}`));
|
|
504
687
|
}
|
|
505
688
|
//#endregion
|
|
506
689
|
//#region src/kernel/moleculer/mesh-queue-report.ts
|
|
@@ -1136,6 +1319,125 @@ function formatArrayBufferCensus(label, report, context) {
|
|
|
1136
1319
|
return `[mem] ${label} arrayBuffer census: live=${live}MB in ${report.count} ArrayBuffers (raw arrayBuffers=${context.rawMb}MB at the trigger → ${verdict}) top sizes: ${report.topSizes.map(formatExact).join(", ") || "(none)"}; buckets: ${report.buckets.map(formatBucket).join(", ") || "(none)"}; detached=${report.detached} took=${context.tookMs}ms — cost: ONE forced full GC (the same pause as a reclaim pass), walked in memory, nothing serialised, nothing written; trigger: ${context.reason}`;
|
|
1137
1320
|
}
|
|
1138
1321
|
//#endregion
|
|
1322
|
+
//#region src/kernel/gc-census.ts
|
|
1323
|
+
function countsOf(entries, kinds) {
|
|
1324
|
+
let majors = 0;
|
|
1325
|
+
let minors = 0;
|
|
1326
|
+
let incrementals = 0;
|
|
1327
|
+
let pauseMs = 0;
|
|
1328
|
+
for (const e of entries) {
|
|
1329
|
+
if (e.kind === kinds.major) majors++;
|
|
1330
|
+
else if (e.kind === kinds.minor) minors++;
|
|
1331
|
+
else if (e.kind === kinds.incremental) incrementals++;
|
|
1332
|
+
pauseMs += e.durationMs;
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
majors,
|
|
1336
|
+
minors,
|
|
1337
|
+
incrementals,
|
|
1338
|
+
pauseMs,
|
|
1339
|
+
windowMs: 0
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Build a census over an injected observer.
|
|
1344
|
+
*
|
|
1345
|
+
* Returns `undefined` when the observer cannot be installed — the caller then
|
|
1346
|
+
* prints nothing, which is the honest reading. It must never degrade into a
|
|
1347
|
+
* census that reports zero collections: a process whose GC is not being
|
|
1348
|
+
* observed and a process that is not collecting look identical on the line, and
|
|
1349
|
+
* they are the two answers furthest apart.
|
|
1350
|
+
*/
|
|
1351
|
+
function createGcCensus(install, kinds, startedAt) {
|
|
1352
|
+
let pending = [];
|
|
1353
|
+
const stopObserving = install((entry) => {
|
|
1354
|
+
pending.push(entry);
|
|
1355
|
+
});
|
|
1356
|
+
if (stopObserving === void 0) return void 0;
|
|
1357
|
+
let windowStart = startedAt;
|
|
1358
|
+
return {
|
|
1359
|
+
read: (at) => {
|
|
1360
|
+
const windowMs = at - windowStart;
|
|
1361
|
+
if (windowMs < 1e3) return null;
|
|
1362
|
+
const taken = pending;
|
|
1363
|
+
pending = [];
|
|
1364
|
+
windowStart = at;
|
|
1365
|
+
return {
|
|
1366
|
+
...countsOf(taken, kinds),
|
|
1367
|
+
windowMs
|
|
1368
|
+
};
|
|
1369
|
+
},
|
|
1370
|
+
stop: () => {
|
|
1371
|
+
stopObserving();
|
|
1372
|
+
pending = [];
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* The observer this process actually gets.
|
|
1378
|
+
*
|
|
1379
|
+
* Everything that can fail is inside the try: a runtime without
|
|
1380
|
+
* `perf_hooks`, without the `gc` entry type, or without the kind constants
|
|
1381
|
+
* yields `undefined` from `install`, and {@link createGcCensus} then declines
|
|
1382
|
+
* to exist rather than reporting zeros.
|
|
1383
|
+
*/
|
|
1384
|
+
function nodeGcObserver() {
|
|
1385
|
+
return {
|
|
1386
|
+
kinds: readKinds({
|
|
1387
|
+
major: 4,
|
|
1388
|
+
minor: 1,
|
|
1389
|
+
incremental: 8
|
|
1390
|
+
}),
|
|
1391
|
+
install: (observe) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const observer = new (process.getBuiltinModule("node:perf_hooks")).PerformanceObserver((list) => {
|
|
1394
|
+
for (const entry of list.getEntries()) {
|
|
1395
|
+
const detail = "detail" in entry ? entry.detail : void 0;
|
|
1396
|
+
observe({
|
|
1397
|
+
kind: typeof detail === "object" && detail !== null && "kind" in detail && typeof detail.kind === "number" ? detail.kind : -1,
|
|
1398
|
+
durationMs: entry.duration
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
1403
|
+
return () => {
|
|
1404
|
+
observer.disconnect();
|
|
1405
|
+
};
|
|
1406
|
+
} catch {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function readKinds(fallback) {
|
|
1413
|
+
try {
|
|
1414
|
+
const c = process.getBuiltinModule("node:perf_hooks").constants;
|
|
1415
|
+
return {
|
|
1416
|
+
major: c.NODE_PERFORMANCE_GC_MAJOR,
|
|
1417
|
+
minor: c.NODE_PERFORMANCE_GC_MINOR,
|
|
1418
|
+
incremental: c.NODE_PERFORMANCE_GC_INCREMENTAL
|
|
1419
|
+
};
|
|
1420
|
+
} catch {
|
|
1421
|
+
return fallback;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The two fields, APPENDED LAST on the `[mem]` line.
|
|
1426
|
+
*
|
|
1427
|
+
* D446's rule: no existing field moves, so every Loki prefix written since
|
|
1428
|
+
* 2026-08-29 still matches. `gc=` prints even when nothing was collected — a
|
|
1429
|
+
* window with no collections is a reading, and it is the reading that says a
|
|
1430
|
+
* process is idle rather than unobserved. `gcPauseMs=` is the MAIN-THREAD
|
|
1431
|
+
* pause only; the concurrent marking that cost hub-main 70 % of its CPU is on
|
|
1432
|
+
* the platform workers and no in-process counter sees it, which is why the
|
|
1433
|
+
* field is named for the thread it describes (D456, part 4).
|
|
1434
|
+
*/
|
|
1435
|
+
function formatGcCensus(window) {
|
|
1436
|
+
if (window === null || window === void 0) return "";
|
|
1437
|
+
const perMin = (n) => window.windowMs <= 0 ? "?" : (n / (window.windowMs / 6e4)).toFixed(1);
|
|
1438
|
+
return ` gc=maj${window.majors}/min${window.minors}/inc${window.incrementals}@${Math.round(window.windowMs / 1e3)}s gcMajPerMin=${perMin(window.majors)} gcPauseMs=${Math.round(window.pauseMs)}`;
|
|
1439
|
+
}
|
|
1440
|
+
//#endregion
|
|
1139
1441
|
//#region src/kernel/heap-watch.ts
|
|
1140
1442
|
/**
|
|
1141
1443
|
* heap-watch — hub-main's own memory, on the record, and returned to the OS.
|
|
@@ -1732,6 +2034,8 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1732
2034
|
const socketPlane = probes?.socketPlane === void 0 ? void 0 : createSocketPlaneMeter(probes.socketPlane);
|
|
1733
2035
|
const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
|
|
1734
2036
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
2037
|
+
const gcObserver = probes?.gcObserver ?? nodeGcObserver();
|
|
2038
|
+
const gcCensus = createGcCensus(gcObserver.install, gcObserver.kinds, now());
|
|
1735
2039
|
const triggerMb = reclaimOptions?.triggerMb ?? 1024;
|
|
1736
2040
|
const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
|
|
1737
2041
|
const steadyStateIntervalMs = reclaimOptions?.steadyStateIntervalMs ?? 6e5;
|
|
@@ -1834,7 +2138,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1834
2138
|
const due = at - lastLoggedAt >= intervalMs;
|
|
1835
2139
|
if (mode === "escalated" || due) {
|
|
1836
2140
|
lastLoggedAt = at;
|
|
1837
|
-
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()));
|
|
2141
|
+
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()) + formatGcCensus(gcCensus?.read(at)));
|
|
1838
2142
|
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
1839
2143
|
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
1840
2144
|
else sink.info(line);
|
|
@@ -1891,6 +2195,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1891
2195
|
stopped = true;
|
|
1892
2196
|
clearInterval(timer);
|
|
1893
2197
|
loopDelay?.stop();
|
|
2198
|
+
gcCensus?.stop();
|
|
1894
2199
|
};
|
|
1895
2200
|
}
|
|
1896
2201
|
/**
|
|
@@ -5912,6 +6217,8 @@ var SocketChannel = class {
|
|
|
5912
6217
|
txCounters = createSocketDirectionCounters();
|
|
5913
6218
|
/** Frames read from the peer, cumulative, split by kind. */
|
|
5914
6219
|
rxCounters = createSocketDirectionCounters();
|
|
6220
|
+
/** See {@link CapActionChannelObserver}. Absent = no per-action census. */
|
|
6221
|
+
actionObserver = null;
|
|
5915
6222
|
constructor(socket) {
|
|
5916
6223
|
this.socket = socket;
|
|
5917
6224
|
socket.on("data", (chunk) => this.onData(chunk));
|
|
@@ -5942,16 +6249,26 @@ var SocketChannel = class {
|
|
|
5942
6249
|
}, timeoutMs);
|
|
5943
6250
|
timer.unref?.();
|
|
5944
6251
|
}
|
|
6252
|
+
const label = this.labelSafely(body);
|
|
5945
6253
|
this.pending.set(id, {
|
|
5946
6254
|
resolve,
|
|
5947
6255
|
reject,
|
|
5948
|
-
timer
|
|
6256
|
+
timer,
|
|
6257
|
+
action: null
|
|
5949
6258
|
});
|
|
5950
|
-
this.send({
|
|
6259
|
+
const reqBytes = this.send({
|
|
5951
6260
|
k: "req",
|
|
5952
6261
|
id,
|
|
5953
6262
|
body
|
|
5954
6263
|
}, opts?.payload);
|
|
6264
|
+
const entry = this.pending.get(id);
|
|
6265
|
+
if (entry !== void 0 && label !== void 0) this.pending.set(id, {
|
|
6266
|
+
...entry,
|
|
6267
|
+
action: {
|
|
6268
|
+
label,
|
|
6269
|
+
reqBytes
|
|
6270
|
+
}
|
|
6271
|
+
});
|
|
5955
6272
|
});
|
|
5956
6273
|
}
|
|
5957
6274
|
emit(body) {
|
|
@@ -5961,6 +6278,36 @@ var SocketChannel = class {
|
|
|
5961
6278
|
body
|
|
5962
6279
|
});
|
|
5963
6280
|
}
|
|
6281
|
+
/**
|
|
6282
|
+
* Wire the per-action census. Only one observer is active at a time; an
|
|
6283
|
+
* unwired channel records nothing at all (never zeros).
|
|
6284
|
+
*/
|
|
6285
|
+
observeActions(observer) {
|
|
6286
|
+
this.actionObserver = observer;
|
|
6287
|
+
}
|
|
6288
|
+
/**
|
|
6289
|
+
* The label for one envelope, or `undefined` when there is nothing to
|
|
6290
|
+
* record. Wrapped: a broken labeller must never fail the call it watches —
|
|
6291
|
+
* the same discipline `LocalChildRegistry.recordCapUsage` applies, and for
|
|
6292
|
+
* the same reason.
|
|
6293
|
+
*/
|
|
6294
|
+
labelSafely(body) {
|
|
6295
|
+
const obs = this.actionObserver;
|
|
6296
|
+
if (obs === null) return void 0;
|
|
6297
|
+
try {
|
|
6298
|
+
return obs.label(body);
|
|
6299
|
+
} catch {
|
|
6300
|
+
return null;
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
/** Settle one request/response pair on the census. Never throws. */
|
|
6304
|
+
recordAction(label, reqBytes, resBytes) {
|
|
6305
|
+
const obs = this.actionObserver;
|
|
6306
|
+
if (obs === null) return;
|
|
6307
|
+
try {
|
|
6308
|
+
obs.record(label, reqBytes, resBytes);
|
|
6309
|
+
} catch {}
|
|
6310
|
+
}
|
|
5964
6311
|
/** Replace the request handler. Only one handler is active at a time. */
|
|
5965
6312
|
onRequest(handler) {
|
|
5966
6313
|
this.requestHandler = handler;
|
|
@@ -6008,21 +6355,27 @@ var SocketChannel = class {
|
|
|
6008
6355
|
* are never copied into the head. A forwarded payload is a view into the
|
|
6009
6356
|
* frame it arrived on, and that frame's buffer is never mutated, so handing
|
|
6010
6357
|
* the view to the socket is safe.
|
|
6358
|
+
*
|
|
6359
|
+
* Returns the on-the-wire bytes it charged — the same number the per-kind
|
|
6360
|
+
* counter got, so the per-action census can never disagree with the per-peer
|
|
6361
|
+
* one. `0` when the channel is closed and nothing was written.
|
|
6011
6362
|
*/
|
|
6012
6363
|
send(frame, payload) {
|
|
6013
|
-
if (this.closed) return;
|
|
6364
|
+
if (this.closed) return 0;
|
|
6014
6365
|
if (payload === void 0) {
|
|
6015
6366
|
const encoded = encodeFrame(frame);
|
|
6016
6367
|
recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
|
|
6017
6368
|
this.socket.write(encoded);
|
|
6018
|
-
return;
|
|
6369
|
+
return encoded.byteLength;
|
|
6019
6370
|
}
|
|
6020
6371
|
const head = encodeFrameHead(frame, payload);
|
|
6021
|
-
|
|
6372
|
+
const bytes = head.byteLength + payload.byteLength;
|
|
6373
|
+
recordSocketFrame(this.txCounters, frame.k, bytes);
|
|
6022
6374
|
this.socket.cork();
|
|
6023
6375
|
this.socket.write(head);
|
|
6024
6376
|
this.socket.write(payload);
|
|
6025
6377
|
this.socket.uncork();
|
|
6378
|
+
return bytes;
|
|
6026
6379
|
}
|
|
6027
6380
|
onData(chunk) {
|
|
6028
6381
|
const frames = this.decoder.push(chunk);
|
|
@@ -6041,28 +6394,31 @@ var SocketChannel = class {
|
|
|
6041
6394
|
async handleFrame(frame, wireBytes) {
|
|
6042
6395
|
recordSocketFrame(this.rxCounters, frame.k, wireBytes);
|
|
6043
6396
|
if (frame.k === "req") {
|
|
6397
|
+
const label = this.labelSafely(frame.body);
|
|
6398
|
+
let resBytes = 0;
|
|
6044
6399
|
try {
|
|
6045
6400
|
const result = await this.requestHandler(frame.body, frame.pl);
|
|
6046
|
-
if (result instanceof PayloadReply) this.send({
|
|
6401
|
+
if (result instanceof PayloadReply) resBytes = this.send({
|
|
6047
6402
|
k: "res",
|
|
6048
6403
|
id: frame.id,
|
|
6049
6404
|
ok: true,
|
|
6050
6405
|
body: result.body
|
|
6051
6406
|
}, result.payload);
|
|
6052
|
-
else this.send({
|
|
6407
|
+
else resBytes = this.send({
|
|
6053
6408
|
k: "res",
|
|
6054
6409
|
id: frame.id,
|
|
6055
6410
|
ok: true,
|
|
6056
6411
|
body: result
|
|
6057
6412
|
});
|
|
6058
6413
|
} catch (err) {
|
|
6059
|
-
this.send({
|
|
6414
|
+
resBytes = this.send({
|
|
6060
6415
|
k: "res",
|
|
6061
6416
|
id: frame.id,
|
|
6062
6417
|
ok: false,
|
|
6063
6418
|
err: err instanceof Error ? err.message : String(err)
|
|
6064
6419
|
});
|
|
6065
6420
|
}
|
|
6421
|
+
if (label !== void 0) this.recordAction(label, wireBytes, resBytes);
|
|
6066
6422
|
return;
|
|
6067
6423
|
}
|
|
6068
6424
|
if (frame.k === "res") {
|
|
@@ -6070,6 +6426,7 @@ var SocketChannel = class {
|
|
|
6070
6426
|
if (!p) return;
|
|
6071
6427
|
this.pending.delete(frame.id);
|
|
6072
6428
|
if (p.timer !== null) clearTimeout(p.timer);
|
|
6429
|
+
if (p.action !== null) this.recordAction(p.action.label, p.action.reqBytes, wireBytes);
|
|
6073
6430
|
if (!frame.ok) p.reject(new Error(frame.err));
|
|
6074
6431
|
else if (frame.pl !== void 0) p.resolve(new PayloadReply(frame.body, frame.pl));
|
|
6075
6432
|
else p.resolve(frame.body);
|
|
@@ -6623,6 +6980,64 @@ function deliverShared(state, event) {
|
|
|
6623
6980
|
}
|
|
6624
6981
|
}
|
|
6625
6982
|
//#endregion
|
|
6983
|
+
//#region src/kernel/transport/cap-child-index.ts
|
|
6984
|
+
/** Separator that cannot occur in a kebab cap name, so the key is unambiguous. */
|
|
6985
|
+
var DEVICE_KEY_SEPARATOR = "\0";
|
|
6986
|
+
var NO_CANDIDATES = Object.freeze([]);
|
|
6987
|
+
var MapCapChildIndex = class {
|
|
6988
|
+
deviceOwners;
|
|
6989
|
+
singletons;
|
|
6990
|
+
constructor(deviceOwners, singletons) {
|
|
6991
|
+
this.deviceOwners = deviceOwners;
|
|
6992
|
+
this.singletons = singletons;
|
|
6993
|
+
}
|
|
6994
|
+
deviceOwner(capName, deviceId) {
|
|
6995
|
+
return this.deviceOwners.get(`${capName}${DEVICE_KEY_SEPARATOR}${deviceId}`) ?? null;
|
|
6996
|
+
}
|
|
6997
|
+
singletonCandidates(capName) {
|
|
6998
|
+
return this.singletons.get(capName) ?? NO_CANDIDATES;
|
|
6999
|
+
}
|
|
7000
|
+
};
|
|
7001
|
+
/**
|
|
7002
|
+
* Build the `(cap, device?) → child` index from the connected children.
|
|
7003
|
+
*
|
|
7004
|
+
* A PURE function of what it is handed — it captures nothing, subscribes to
|
|
7005
|
+
* nothing and cannot be updated in place. That is the whole safety argument
|
|
7006
|
+
* (D3, D457): the only way to change what it says is to build a new one from
|
|
7007
|
+
* the `children` map, so an index can never name a child that map has already
|
|
7008
|
+
* dropped. An incremental index — add on register, remove on close — would be
|
|
7009
|
+
* a second registry that can disagree with the first, and a stale one routes
|
|
7010
|
+
* to a dead runner, which is strictly worse than a slow scan.
|
|
7011
|
+
*
|
|
7012
|
+
* Cost: O(children × descriptors), paid once per mutation of the children map
|
|
7013
|
+
* (a register, a re-register, a disconnect) rather than once per cap call.
|
|
7014
|
+
*
|
|
7015
|
+
* The two tiers stay separate, exactly as the scan had them: a device lookup
|
|
7016
|
+
* is never answered from a deviceId-less descriptor. `resolveChildId` owns
|
|
7017
|
+
* the fallback from tier 1 to tier 2, and folding it in here would make it
|
|
7018
|
+
* unreachable.
|
|
7019
|
+
*/
|
|
7020
|
+
function buildCapChildIndex(entries) {
|
|
7021
|
+
const deviceOwners = /* @__PURE__ */ new Map();
|
|
7022
|
+
const singletons = /* @__PURE__ */ new Map();
|
|
7023
|
+
for (const entry of entries) {
|
|
7024
|
+
const namedForThisChild = /* @__PURE__ */ new Set();
|
|
7025
|
+
for (const cap of entry.caps) {
|
|
7026
|
+
if (cap.deviceId === void 0) {
|
|
7027
|
+
if (namedForThisChild.has(cap.capName)) continue;
|
|
7028
|
+
namedForThisChild.add(cap.capName);
|
|
7029
|
+
const candidates = singletons.get(cap.capName);
|
|
7030
|
+
if (candidates === void 0) singletons.set(cap.capName, [entry.childId]);
|
|
7031
|
+
else candidates.push(entry.childId);
|
|
7032
|
+
continue;
|
|
7033
|
+
}
|
|
7034
|
+
const key = `${cap.capName}${DEVICE_KEY_SEPARATOR}${cap.deviceId}`;
|
|
7035
|
+
if (!deviceOwners.has(key)) deviceOwners.set(key, entry.childId);
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
return new MapCapChildIndex(deviceOwners, singletons);
|
|
7039
|
+
}
|
|
7040
|
+
//#endregion
|
|
6626
7041
|
//#region src/kernel/transport/cap-routing-hints.ts
|
|
6627
7042
|
/** Lift {@link CapRoutingHints} from a call's args. Never throws. */
|
|
6628
7043
|
function liftRoutingHints(args) {
|
|
@@ -6634,8 +7049,6 @@ function liftRoutingHints(args) {
|
|
|
6634
7049
|
...typeof addonId === "string" && addonId.length > 0 ? { addonId } : {}
|
|
6635
7050
|
};
|
|
6636
7051
|
}
|
|
6637
|
-
//#endregion
|
|
6638
|
-
//#region src/kernel/transport/local-child-registry.ts
|
|
6639
7052
|
function readFanoutMode() {
|
|
6640
7053
|
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
6641
7054
|
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
@@ -6676,6 +7089,14 @@ var LocalChildRegistry = class {
|
|
|
6676
7089
|
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
6677
7090
|
forwardedRaw = 0;
|
|
6678
7091
|
forwardedDecoded = 0;
|
|
7092
|
+
/**
|
|
7093
|
+
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
7094
|
+
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
7095
|
+
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
7096
|
+
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
7097
|
+
* method with `raw>0` on its neighbours is a different fault.
|
|
7098
|
+
*/
|
|
7099
|
+
actionCensus = createCapActionCensus();
|
|
6679
7100
|
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
6680
7101
|
capTimeoutMs;
|
|
6681
7102
|
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
@@ -6689,6 +7110,12 @@ var LocalChildRegistry = class {
|
|
|
6689
7110
|
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
6690
7111
|
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
6691
7112
|
/**
|
|
7113
|
+
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
7114
|
+
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
7115
|
+
* never a registry (D3, D457).
|
|
7116
|
+
*/
|
|
7117
|
+
capChildIndex = null;
|
|
7118
|
+
/**
|
|
6692
7119
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
6693
7120
|
* or a full `LocalChildRegistryOptions` object.
|
|
6694
7121
|
*
|
|
@@ -6769,6 +7196,21 @@ var LocalChildRegistry = class {
|
|
|
6769
7196
|
};
|
|
6770
7197
|
}
|
|
6771
7198
|
/**
|
|
7199
|
+
* Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
|
|
7200
|
+
* and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
|
|
7201
|
+
* must be one); the heartbeat subtracts the previous reading.
|
|
7202
|
+
*
|
|
7203
|
+
* This is the instrument D454 closed by asking for. `socketTopBytes=` names
|
|
7204
|
+
* the peer that moves the bytes and `txkB=` says they are responses; neither
|
|
7205
|
+
* can say WHICH method originates them, and after raw-forward the bytes that
|
|
7206
|
+
* cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
|
|
7207
|
+
* calls cross unread. What costs is what hub-main LOOKS at, and that is the
|
|
7208
|
+
* `decoded` half of an entry, next to the bytes it carried.
|
|
7209
|
+
*/
|
|
7210
|
+
readActionCensus() {
|
|
7211
|
+
return this.actionCensus.read();
|
|
7212
|
+
}
|
|
7213
|
+
/**
|
|
6772
7214
|
* The regime the counters above were produced under, read once from
|
|
6773
7215
|
* `CAMSTACK_UDS_EVENT_FANOUT` at construction.
|
|
6774
7216
|
*
|
|
@@ -6882,11 +7324,12 @@ var LocalChildRegistry = class {
|
|
|
6882
7324
|
* compete for the same capName.
|
|
6883
7325
|
*/
|
|
6884
7326
|
resolveChildId(capName, deviceId) {
|
|
7327
|
+
const index = this.capIndex();
|
|
6885
7328
|
if (deviceId !== void 0) {
|
|
6886
|
-
const deviceOwner =
|
|
7329
|
+
const deviceOwner = index.deviceOwner(capName, deviceId);
|
|
6887
7330
|
if (deviceOwner !== null) return deviceOwner;
|
|
6888
7331
|
}
|
|
6889
|
-
const candidates =
|
|
7332
|
+
const candidates = index.singletonCandidates(capName);
|
|
6890
7333
|
if (candidates.length === 0) return null;
|
|
6891
7334
|
if (candidates.length === 1) return candidates[0];
|
|
6892
7335
|
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
@@ -6919,16 +7362,37 @@ var LocalChildRegistry = class {
|
|
|
6919
7362
|
});
|
|
6920
7363
|
} catch {}
|
|
6921
7364
|
}
|
|
6922
|
-
/**
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
7365
|
+
/**
|
|
7366
|
+
* The cap index over the CURRENT children, rebuilt on first use after any
|
|
7367
|
+
* mutation of {@link children}.
|
|
7368
|
+
*
|
|
7369
|
+
* This replaced a linear walk of every child's cap manifest per
|
|
7370
|
+
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
7371
|
+
* across 39 children (D456). Why it did not exist before is the whole
|
|
7372
|
+
* design: what it indexes moves — a runner is replaced on every update, a
|
|
7373
|
+
* crash respawns one, a child re-registers with a replacement manifest after
|
|
7374
|
+
* init — and an index that names a dead child routes work into a socket
|
|
7375
|
+
* nobody is reading, which is strictly worse than a slow scan.
|
|
7376
|
+
*
|
|
7377
|
+
* So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
|
|
7378
|
+
* function of `children.values()`, the only two writers of that map
|
|
7379
|
+
* ({@link invalidateCapIndex}'s call sites) drop the index in the same
|
|
7380
|
+
* statement, and nothing else can construct one. There is no add-on-register
|
|
7381
|
+
* / remove-on-close path that could disagree with the map — i.e. no shadow
|
|
7382
|
+
* registry (D3): the map remains the single authority and this holds no fact
|
|
7383
|
+
* it does not.
|
|
7384
|
+
*
|
|
7385
|
+
* The operator's active-singleton preference is deliberately NOT indexed —
|
|
7386
|
+
* it changes without any child connecting or leaving, so `resolveChildId`
|
|
7387
|
+
* reads it live and the index only supplies the candidate list.
|
|
7388
|
+
*/
|
|
7389
|
+
capIndex() {
|
|
7390
|
+
if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
|
|
7391
|
+
return this.capChildIndex;
|
|
6926
7392
|
}
|
|
6927
|
-
/**
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
for (const entry of this.children.values()) if (entry.caps.some(predicate)) out.push(entry.childId);
|
|
6931
|
-
return out;
|
|
7393
|
+
/** Drop the derived index. MUST follow every write to {@link children}. */
|
|
7394
|
+
invalidateCapIndex() {
|
|
7395
|
+
this.capChildIndex = null;
|
|
6932
7396
|
}
|
|
6933
7397
|
/**
|
|
6934
7398
|
* Does the named child currently provide `(capName, deviceId?)`?
|
|
@@ -7124,6 +7588,14 @@ var LocalChildRegistry = class {
|
|
|
7124
7588
|
onConnection(channel) {
|
|
7125
7589
|
let childId = null;
|
|
7126
7590
|
const incarnation = this.nextIncarnation++;
|
|
7591
|
+
channel.observeActions?.({
|
|
7592
|
+
label: capActionLabel,
|
|
7593
|
+
record: (label, reqBytes, resBytes) => {
|
|
7594
|
+
const peerId = childId ?? "(pre-register)";
|
|
7595
|
+
if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
|
|
7596
|
+
else this.actionCensus.record(peerId, label, reqBytes, resBytes);
|
|
7597
|
+
}
|
|
7598
|
+
});
|
|
7127
7599
|
channel.onEvent((body) => {
|
|
7128
7600
|
const msg = body;
|
|
7129
7601
|
if (msg.kind === "event") {
|
|
@@ -7166,6 +7638,7 @@ var LocalChildRegistry = class {
|
|
|
7166
7638
|
incarnation,
|
|
7167
7639
|
rawForward: msg.rawForward === true
|
|
7168
7640
|
});
|
|
7641
|
+
this.invalidateCapIndex();
|
|
7169
7642
|
this.logPatternSet(msg.childId, eventPatterns);
|
|
7170
7643
|
this.registeredHandler({
|
|
7171
7644
|
childId: msg.childId,
|
|
@@ -7197,6 +7670,8 @@ var LocalChildRegistry = class {
|
|
|
7197
7670
|
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
|
|
7198
7671
|
const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
|
|
7199
7672
|
this.recordCapUsage(childId, target, out.capName, out.method);
|
|
7673
|
+
const actionPeer = childId ?? "(pre-register)";
|
|
7674
|
+
const actionLabel = `${out.capName}.${out.method}`;
|
|
7200
7675
|
if (target !== null) {
|
|
7201
7676
|
if (!this.egressRoutedCaps.has(out.capName)) {
|
|
7202
7677
|
this.egressRoutedCaps.add(out.capName);
|
|
@@ -7205,6 +7680,7 @@ var LocalChildRegistry = class {
|
|
|
7205
7680
|
const entry = this.children.get(target);
|
|
7206
7681
|
if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
|
|
7207
7682
|
this.forwardedRaw += 1;
|
|
7683
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
|
|
7208
7684
|
const forward = {
|
|
7209
7685
|
kind: "cap-call",
|
|
7210
7686
|
capName: out.capName,
|
|
@@ -7219,9 +7695,12 @@ var LocalChildRegistry = class {
|
|
|
7219
7695
|
});
|
|
7220
7696
|
}
|
|
7221
7697
|
this.forwardedDecoded += 1;
|
|
7222
|
-
|
|
7698
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7699
|
+
const input = buildInput();
|
|
7700
|
+
return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
|
|
7223
7701
|
}
|
|
7224
7702
|
this.forwardedDecoded += 1;
|
|
7703
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7225
7704
|
if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
|
|
7226
7705
|
throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
|
|
7227
7706
|
}
|
|
@@ -7244,7 +7723,9 @@ var LocalChildRegistry = class {
|
|
|
7244
7723
|
return;
|
|
7245
7724
|
}
|
|
7246
7725
|
this.children.delete(childId);
|
|
7726
|
+
this.invalidateCapIndex();
|
|
7247
7727
|
this.childEventStats.delete(childId);
|
|
7728
|
+
this.actionCensus.forget(childId);
|
|
7248
7729
|
this.loggedPatternSet.delete(childId);
|
|
7249
7730
|
this.goneHandler(childId, incarnation);
|
|
7250
7731
|
});
|
|
@@ -10702,4 +11183,4 @@ async function defaultRun(command, args) {
|
|
|
10702
11183
|
await execFileAsync(command, [...args], { timeout: INSTALL_TIMEOUT_MS });
|
|
10703
11184
|
}
|
|
10704
11185
|
//#endregion
|
|
10705
|
-
export { buildUdsNativeCapProxy as $, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as $t, createUdsLoggerWithControl as A, sampleSocketDirection as An, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, createV8Reclaimer as Bt, ipcParentLink as C, createSocketPlaneReader as Cn, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, EMPTY_SOCKET_DIRECTION as Dn, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, readSocketQueues as En, HEAP_RECLAIM_TRIGGER_MB as Et, CapRouteError as F,
|
|
11186
|
+
export { buildUdsNativeCapProxy as $, ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO as $t, createUdsLoggerWithControl as A, sampleSocketDirection as An, HUB_MAIN_RSS_BUDGET_MB as At, readMoleculerFanoutMode as B, EVENT_PLANE_TOP_N as Bn, createV8Reclaimer as Bt, ipcParentLink as C, createSocketPlaneReader as Cn, createAddonDataPlaneFacility as Ct, createUdsEventBus as D, EMPTY_SOCKET_DIRECTION as Dn, HEAP_WATCH_INTERVAL_MS as Dt, createUdsEventBridge as E, readSocketQueues as En, HEAP_RECLAIM_TRIGGER_MB as Et, CapRouteError as F, EMPTY_CAP_ACTION_READING as Fn, RUNNER_RSS_BUDGET_ENV as Ft, createLocalTransport as G, reclaimIntervalMs as Gt, PARENT_ROUTED_PROVIDER_ADDON_ID as H, createEventPlaneReader as Hn, hubMainRssBudget as Ht, classifyCapRoute as I, MAX_CAP_ACTIONS_PER_PEER as In, ZERO_LOOP_DELAY as It, SocketChannel as J, startRunnerHeapWatch as Jt, UdsLocalTransportClient as K, shouldReclaim as Kt, LocalChildClient as L, capActionLabel as Ln, buildHeapSample as Lt, AGENT_CAP_FWD_SERVICE as M, socketDirectionMessages as Mn, RSS_BUDGET_REANNOUNCE_MIN_MS as Mt, CapRouteResolver as N, CAP_ACTION_OVERFLOW_LABEL as Nn, RSS_BUDGET_RELEASE_RATIO as Nt, udsChildLogToWorkerEntry as O, createSocketDirectionCounters as On, HEAP_WATCH_WARN_RATIO as Ot, callWithServiceDiscovery as P, CAP_ACTION_UNLABELLED as Pn, RUNNER_HEAP_WATCH_INTERVAL_MS as Pt, buildNativeCapProxy as Q, ARRAY_BUFFER_CENSUS_THRESHOLD_MB as Qt, LocalChildRegistry as R, createCapActionCensus as Rn, createDeferredHeapWatchSink as Rt, buildLinkChain as S, createSocketPlaneMeter as Sn, runNpm as St, createParentUnownedCallHandler as T, formatSocketPlane as Tn, HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS as Tt, __resetCapUsageRegistryForTests as U, diffEventPlane as Un, nextRssBudgetState as Ut, CapUsageRegistry as V, createEventPlaneMeter as Vn, describeRss as Vt, getCapUsageRegistry as W, formatEventPlane as Wn, parseRssBudgetMb as Wt, FrameDecoder as X, ARRAY_BUFFER_CENSUS_COOLDOWN_MS as Xt, localEndpointPath as Y, strandedMb as Yt, encodeFrame as Z, ARRAY_BUFFER_CENSUS_SUSTAIN_MS as Zt, AddonDepsManager as _, createMeshQueueReader as _n, CapabilityUnavailableError as _t, createUdsAddonContext as a, HEAP_SPACE_REPORT_MIN_MB as an, createAddonService as at, brokerCallForCap as b, SOCKET_PLANE_TOP_N as bn, resolveAddonClass as bt, getOrInitReadinessRegistryForClient as c, heapSpaceField as cn, capActionName as ct, clusterEventTopic as d, OUTBOUND_QUEUE_COOLDOWN_MS as dn, capServiceName as dt, createArrayBufferCensusGate as en, createBrokerDeviceManagerApi as et, getBrokerEventBus as f, OUTBOUND_QUEUE_SUSTAIN_MS as fn, parseCapAction as ft, subscribePassthrough as g, MESH_QUEUE_TOP_N as gn, CapabilityHandle as gt, setNodeEventInterest as h, formatOutboundQueueVerdict as hn, DeviceRegistry as ht, createAddonContext as i, DECISIVE_HEAP_SPACES as in, setWorkerNativeCapsChangeListener as it, AGENT_CAP_FWD_ACTION as j, socketDirectionBytes as jn, HUB_RSS_BUDGET_ENV as jt, createUdsLogger as k, recordSocketFrame as kn, HUB_MAIN_HEAP_WATCH_LABEL as kt, setHubConnected as l, readHeapSpaces as ln, capActionSuffix as lt, registerEventBusService as m, createOutboundQueueGate as mn, serializeTypedArrays as mt, installManifestPythonDeps as n, formatArrayBufferCensus as nn, getWorkerNativeCapSnapshot as nt, getWorkerDeviceRegistry as o, emitHeapDiagnosticReport as on, validateProviderRegistrations as ot, getMoleculerEventStats as p, OUTBOUND_QUEUE_THRESHOLD_BYTES as pn, deserializeTypedArrays as pt, UdsLocalTransportServer as q, startHeapWatch as qt, adaptBrokerToCluster as r, isArrayBufferCensusReport as rn, mountNativeCapService as rt, getOrInitReadinessRegistry as s, formatHeapSpaces as sn, NATIVE_PROVIDER_SERVICE_INFIX as st, installManifestSystemDeps as t, createInspectorArrayBufferCensus as tn, getWorkerNativeCapProvider as tt, EVENT_TOPIC_PREFIX as u, selectReportedSpaces as un, capBareAction as ut, createKernelHwAccel as v, formatMeshQueue as vn, copyBundledNativeModules as vt, localProviderLink as w, diffSocketPlane as wn, HEAP_RECLAIM_MIN_INTERVAL_MS as wt, brokerTransportLink as x, SOCKET_QUEUE_TOP_N as xn, resolveNpmInvocation as xt, resolveHwAccel as y, EMPTY_SOCKET_PLANE_BASELINE as yn, installManifestNativeDeps as yt, UDS_NO_ROUTE_PREFIX as z, diffCapActionCensus as zn, createLoopDelayMeter as zt };
|