@camstack/system 1.2.219 → 1.2.221
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-DvVMHLfJ.js} +1 -1
- package/dist/{custom-action-registry-B6gOtUWA.mjs → custom-action-registry-DvtOs-tp.mjs} +1 -1
- package/dist/{dist-B17W9ngu.js → dist-0sWJNwme.js} +261 -4
- package/dist/{dist-Cyz5_i7z.mjs → dist-T9GvfhgV.mjs} +261 -4
- 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-Dh-XMfFx.js} +1 -1
- package/dist/{retired-settings-keys-BCYUrdsA.mjs → retired-settings-keys-DmVZAH9M.mjs} +1 -1
- package/package.json +1 -1
|
@@ -155,6 +155,166 @@ function formatEventPlane(window) {
|
|
|
155
155
|
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}`);
|
|
156
156
|
}
|
|
157
157
|
//#endregion
|
|
158
|
+
//#region src/kernel/transport/cap-action-census.ts
|
|
159
|
+
/** A process that has never read the census. */
|
|
160
|
+
var EMPTY_CAP_ACTION_READING = /* @__PURE__ */ new Map();
|
|
161
|
+
/**
|
|
162
|
+
* Bytes that crossed on a frame carrying no cap envelope.
|
|
163
|
+
*
|
|
164
|
+
* Named rather than omitted so the census's coverage is readable off its own
|
|
165
|
+
* output: `register`, `addon-call`, log frames and the event fan-out all land
|
|
166
|
+
* here. A census that reported only what it could name would look like a
|
|
167
|
+
* complete attribution of whatever share it happened to cover.
|
|
168
|
+
*/
|
|
169
|
+
var CAP_ACTION_UNLABELLED = "(unlabelled)";
|
|
170
|
+
/** Bytes of actions past {@link MAX_CAP_ACTIONS_PER_PEER} on one peer. */
|
|
171
|
+
var CAP_ACTION_OVERFLOW_LABEL = "(overflow)";
|
|
172
|
+
/**
|
|
173
|
+
* Distinct named actions kept per peer before the overflow bucket takes over.
|
|
174
|
+
*
|
|
175
|
+
* Sixty-four against a measured worst case of 14 (`pipeline-analytics`, the
|
|
176
|
+
* heaviest peer on this hub, 2026-09-11) — four times the observed shape, so a
|
|
177
|
+
* peer has to change character before it overflows, and a bound that is never
|
|
178
|
+
* reached in practice still forbids the unbounded growth a wire-supplied key
|
|
179
|
+
* makes possible in principle.
|
|
180
|
+
*/
|
|
181
|
+
var MAX_CAP_ACTIONS_PER_PEER = 64;
|
|
182
|
+
/**
|
|
183
|
+
* `<cap>.<method>` for a cap envelope, `null` for anything else.
|
|
184
|
+
*
|
|
185
|
+
* Both directions are labelled by the same function: a child's `cap-call-out`
|
|
186
|
+
* and the parent's `cap-call` carry `capName` + `method` in the same two keys,
|
|
187
|
+
* so one predicate covers the whole plane. Anything else — a `register`, an
|
|
188
|
+
* `addon-call`, a log or an event — is deliberately unlabelled; see
|
|
189
|
+
* {@link CAP_ACTION_UNLABELLED} for where its bytes go.
|
|
190
|
+
*
|
|
191
|
+
* Cost: two property reads, two `typeof`s and one concatenation of two short
|
|
192
|
+
* strings, called once per `req` frame (not per frame). Against the
|
|
193
|
+
* `encodeFrame` it sits beside — 1 948–1 975 ns on a 0.33 kB body — it is
|
|
194
|
+
* noise, and unlike `recordSocketFrame` it is not on the `evt` path at all.
|
|
195
|
+
*/
|
|
196
|
+
function capActionLabel(body) {
|
|
197
|
+
if (typeof body !== "object" || body === null) return null;
|
|
198
|
+
const env = body;
|
|
199
|
+
if (env.kind !== "cap-call" && env.kind !== "cap-call-out") return null;
|
|
200
|
+
if (typeof env.capName !== "string" || typeof env.method !== "string") return null;
|
|
201
|
+
return `${env.capName}.${env.method}`;
|
|
202
|
+
}
|
|
203
|
+
function emptyCounters() {
|
|
204
|
+
return {
|
|
205
|
+
calls: 0,
|
|
206
|
+
reqBytes: 0,
|
|
207
|
+
resBytes: 0,
|
|
208
|
+
raw: 0,
|
|
209
|
+
decoded: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function createCapActionCensus() {
|
|
213
|
+
const peers = /* @__PURE__ */ new Map();
|
|
214
|
+
/**
|
|
215
|
+
* The per-peer slot for `action`, creating it under the bound.
|
|
216
|
+
*
|
|
217
|
+
* Past {@link MAX_CAP_ACTIONS_PER_PEER} a NEW label is folded into the
|
|
218
|
+
* overflow bucket; labels already present keep their own slot, so a peer
|
|
219
|
+
* that overflows does not lose the attribution it had built.
|
|
220
|
+
*/
|
|
221
|
+
const slot = (peerId, action) => {
|
|
222
|
+
let byAction = peers.get(peerId);
|
|
223
|
+
if (byAction === void 0) {
|
|
224
|
+
byAction = /* @__PURE__ */ new Map();
|
|
225
|
+
peers.set(peerId, byAction);
|
|
226
|
+
}
|
|
227
|
+
const existing = byAction.get(action);
|
|
228
|
+
if (existing !== void 0) return existing;
|
|
229
|
+
if (byAction.size >= 64) {
|
|
230
|
+
let overflow = byAction.get(CAP_ACTION_OVERFLOW_LABEL);
|
|
231
|
+
if (overflow === void 0) {
|
|
232
|
+
overflow = emptyCounters();
|
|
233
|
+
byAction.set(CAP_ACTION_OVERFLOW_LABEL, overflow);
|
|
234
|
+
}
|
|
235
|
+
return overflow;
|
|
236
|
+
}
|
|
237
|
+
const fresh = emptyCounters();
|
|
238
|
+
byAction.set(action, fresh);
|
|
239
|
+
return fresh;
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
record(peerId, action, reqBytes, resBytes) {
|
|
243
|
+
const c = slot(peerId, action);
|
|
244
|
+
c.calls += 1;
|
|
245
|
+
c.reqBytes += reqBytes;
|
|
246
|
+
c.resBytes += resBytes;
|
|
247
|
+
},
|
|
248
|
+
recordForward(peerId, action, kind) {
|
|
249
|
+
const c = slot(peerId, action);
|
|
250
|
+
if (kind === "raw") c.raw += 1;
|
|
251
|
+
else c.decoded += 1;
|
|
252
|
+
},
|
|
253
|
+
recordUnlabelled(peerId, bytes) {
|
|
254
|
+
const c = slot(peerId, CAP_ACTION_UNLABELLED);
|
|
255
|
+
c.resBytes += bytes;
|
|
256
|
+
},
|
|
257
|
+
forget(peerId) {
|
|
258
|
+
peers.delete(peerId);
|
|
259
|
+
},
|
|
260
|
+
read() {
|
|
261
|
+
const out = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const [peerId, byAction] of peers) {
|
|
263
|
+
const copy = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const [action, c] of byAction) copy.set(action, {
|
|
265
|
+
calls: c.calls,
|
|
266
|
+
reqBytes: c.reqBytes,
|
|
267
|
+
resBytes: c.resBytes,
|
|
268
|
+
raw: c.raw,
|
|
269
|
+
decoded: c.decoded
|
|
270
|
+
});
|
|
271
|
+
out.set(peerId, copy);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function sampleOf(reading, peerId, action) {
|
|
278
|
+
return reading.get(peerId)?.get(action);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Subtract the previous reading from the current one.
|
|
282
|
+
*
|
|
283
|
+
* A peer whose runner RESPAWNED reconnects under the same id with counters
|
|
284
|
+
* that went backwards. Clamped at 0, exactly as `diffSocketPlane` clamps for
|
|
285
|
+
* the same reason: its traffic in that window is genuinely unknown, and a
|
|
286
|
+
* negative number on a rate reads as a broken meter.
|
|
287
|
+
*/
|
|
288
|
+
function diffCapActionCensus(before, current, topN) {
|
|
289
|
+
const deltas = [];
|
|
290
|
+
let labelledBytes = 0;
|
|
291
|
+
let totalBytes = 0;
|
|
292
|
+
for (const [peerId, byAction] of current) for (const [action, now] of byAction) {
|
|
293
|
+
const then = sampleOf(before, peerId, action);
|
|
294
|
+
const reqBytes = Math.max(0, now.reqBytes - (then?.reqBytes ?? 0));
|
|
295
|
+
const resBytes = Math.max(0, now.resBytes - (then?.resBytes ?? 0));
|
|
296
|
+
const bytes = reqBytes + resBytes;
|
|
297
|
+
totalBytes += bytes;
|
|
298
|
+
if (action === "(unlabelled)" || action === "(overflow)") continue;
|
|
299
|
+
labelledBytes += bytes;
|
|
300
|
+
if (bytes === 0) continue;
|
|
301
|
+
deltas.push({
|
|
302
|
+
peerId,
|
|
303
|
+
action,
|
|
304
|
+
calls: Math.max(0, now.calls - (then?.calls ?? 0)),
|
|
305
|
+
reqBytes,
|
|
306
|
+
resBytes,
|
|
307
|
+
raw: Math.max(0, now.raw - (then?.raw ?? 0)),
|
|
308
|
+
decoded: Math.max(0, now.decoded - (then?.decoded ?? 0))
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
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),
|
|
313
|
+
labelledBytes,
|
|
314
|
+
totalBytes
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
//#endregion
|
|
158
318
|
//#region src/kernel/transport/socket-traffic.ts
|
|
159
319
|
function createSocketDirectionCounters() {
|
|
160
320
|
return {
|
|
@@ -316,10 +476,12 @@ function createSocketPlaneReader(sources) {
|
|
|
316
476
|
});
|
|
317
477
|
}
|
|
318
478
|
const forward = registry.readForwardCensus?.();
|
|
479
|
+
const actions = registry.readActionCensus?.();
|
|
319
480
|
return {
|
|
320
481
|
peers,
|
|
321
482
|
peersConnected: listed.length,
|
|
322
|
-
...forward === void 0 ? {} : { forward }
|
|
483
|
+
...forward === void 0 ? {} : { forward },
|
|
484
|
+
...actions === void 0 ? {} : { actions }
|
|
323
485
|
};
|
|
324
486
|
} catch {
|
|
325
487
|
return;
|
|
@@ -422,6 +584,7 @@ function diffSocketPlane(baseline, current, topN = 5) {
|
|
|
422
584
|
if (peerMessages(delta) > 0) deltas.push(delta);
|
|
423
585
|
}
|
|
424
586
|
const forward = diffForward(current.forward, baseline.forward);
|
|
587
|
+
const actions = current.actions === void 0 ? void 0 : diffCapActionCensus(baseline.actions ?? EMPTY_CAP_ACTION_READING, current.actions, 5);
|
|
425
588
|
return {
|
|
426
589
|
peersConnected: current.peersConnected,
|
|
427
590
|
peersMeasured: current.peers.length,
|
|
@@ -430,7 +593,8 @@ function diffSocketPlane(baseline, current, topN = 5) {
|
|
|
430
593
|
top: deltas.toSorted((a, b) => peerMessages(b) - peerMessages(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
431
594
|
topBytes: deltas.toSorted((a, b) => peerBytes(b) - peerBytes(a) || a.peerId.localeCompare(b.peerId)).slice(0, topN),
|
|
432
595
|
queued: readSocketQueues(current),
|
|
433
|
-
...forward === void 0 ? {} : { forward }
|
|
596
|
+
...forward === void 0 ? {} : { forward },
|
|
597
|
+
...actions === void 0 ? {} : { actions }
|
|
434
598
|
};
|
|
435
599
|
}
|
|
436
600
|
function createSocketPlaneMeter(reader, topN = 5) {
|
|
@@ -441,7 +605,8 @@ function createSocketPlaneMeter(reader, topN = 5) {
|
|
|
441
605
|
const window = diffSocketPlane(baseline, current, topN);
|
|
442
606
|
baseline = {
|
|
443
607
|
peers: new Map(current.peers.map((p) => [p.peerId, p])),
|
|
444
|
-
...current.forward === void 0 ? {} : { forward: current.forward }
|
|
608
|
+
...current.forward === void 0 ? {} : { forward: current.forward },
|
|
609
|
+
...current.actions === void 0 ? {} : { actions: current.actions }
|
|
445
610
|
};
|
|
446
611
|
return window;
|
|
447
612
|
} };
|
|
@@ -478,6 +643,24 @@ function formatPeer(delta) {
|
|
|
478
643
|
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)}`;
|
|
479
644
|
}
|
|
480
645
|
/**
|
|
646
|
+
* One named action: `<peer>!<cap>.<method>:<kB>kB/<calls>:r<raw>/d<decoded>`.
|
|
647
|
+
*
|
|
648
|
+
* `!` separates the peer from the action because `.` already separates cap
|
|
649
|
+
* from method and a single delimiter for both would make the token ambiguous
|
|
650
|
+
* to split.
|
|
651
|
+
*
|
|
652
|
+
* The raw/decoded pair is the half that did not exist anywhere before D456.
|
|
653
|
+
* `socketFwd=` answers it for the whole plane — 30 349/5 051 on this hub — and
|
|
654
|
+
* a plane-wide ratio cannot tell a method that is entirely forwarded from one
|
|
655
|
+
* that is entirely decoded, which is the difference between bytes hub-main
|
|
656
|
+
* copies and bytes hub-main reads. The census pays for the two counters it
|
|
657
|
+
* takes; the conclusion they buy is which of the line's megabytes are actually
|
|
658
|
+
* expensive.
|
|
659
|
+
*/
|
|
660
|
+
function formatAction(entry) {
|
|
661
|
+
return `${entry.peerId}!${entry.action}:${kb$1(entry.reqBytes + entry.resBytes)}kB/${entry.calls}:r${entry.raw}/d${entry.decoded}`;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
481
664
|
* The suffix appended to a `[mem]` line — leading space included, empty when
|
|
482
665
|
* this process has no local socket plane.
|
|
483
666
|
*
|
|
@@ -503,7 +686,7 @@ function formatSocketPlane(window) {
|
|
|
503
686
|
const named = new Set(window.top.map((p) => p.peerId));
|
|
504
687
|
const topBytes = window.topBytes.filter((p) => !named.has(p.peerId)).map(formatPeer).join(",");
|
|
505
688
|
const queuedTop = window.queued.top.map((p) => `${p.peerId}:${kb$1(p.bytes)}`).join(",");
|
|
506
|
-
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}`);
|
|
689
|
+
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(",")}`));
|
|
507
690
|
}
|
|
508
691
|
//#endregion
|
|
509
692
|
//#region src/kernel/moleculer/mesh-queue-report.ts
|
|
@@ -1139,6 +1322,125 @@ function formatArrayBufferCensus(label, report, context) {
|
|
|
1139
1322
|
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}`;
|
|
1140
1323
|
}
|
|
1141
1324
|
//#endregion
|
|
1325
|
+
//#region src/kernel/gc-census.ts
|
|
1326
|
+
function countsOf(entries, kinds) {
|
|
1327
|
+
let majors = 0;
|
|
1328
|
+
let minors = 0;
|
|
1329
|
+
let incrementals = 0;
|
|
1330
|
+
let pauseMs = 0;
|
|
1331
|
+
for (const e of entries) {
|
|
1332
|
+
if (e.kind === kinds.major) majors++;
|
|
1333
|
+
else if (e.kind === kinds.minor) minors++;
|
|
1334
|
+
else if (e.kind === kinds.incremental) incrementals++;
|
|
1335
|
+
pauseMs += e.durationMs;
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
majors,
|
|
1339
|
+
minors,
|
|
1340
|
+
incrementals,
|
|
1341
|
+
pauseMs,
|
|
1342
|
+
windowMs: 0
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Build a census over an injected observer.
|
|
1347
|
+
*
|
|
1348
|
+
* Returns `undefined` when the observer cannot be installed — the caller then
|
|
1349
|
+
* prints nothing, which is the honest reading. It must never degrade into a
|
|
1350
|
+
* census that reports zero collections: a process whose GC is not being
|
|
1351
|
+
* observed and a process that is not collecting look identical on the line, and
|
|
1352
|
+
* they are the two answers furthest apart.
|
|
1353
|
+
*/
|
|
1354
|
+
function createGcCensus(install, kinds, startedAt) {
|
|
1355
|
+
let pending = [];
|
|
1356
|
+
const stopObserving = install((entry) => {
|
|
1357
|
+
pending.push(entry);
|
|
1358
|
+
});
|
|
1359
|
+
if (stopObserving === void 0) return void 0;
|
|
1360
|
+
let windowStart = startedAt;
|
|
1361
|
+
return {
|
|
1362
|
+
read: (at) => {
|
|
1363
|
+
const windowMs = at - windowStart;
|
|
1364
|
+
if (windowMs < 1e3) return null;
|
|
1365
|
+
const taken = pending;
|
|
1366
|
+
pending = [];
|
|
1367
|
+
windowStart = at;
|
|
1368
|
+
return {
|
|
1369
|
+
...countsOf(taken, kinds),
|
|
1370
|
+
windowMs
|
|
1371
|
+
};
|
|
1372
|
+
},
|
|
1373
|
+
stop: () => {
|
|
1374
|
+
stopObserving();
|
|
1375
|
+
pending = [];
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* The observer this process actually gets.
|
|
1381
|
+
*
|
|
1382
|
+
* Everything that can fail is inside the try: a runtime without
|
|
1383
|
+
* `perf_hooks`, without the `gc` entry type, or without the kind constants
|
|
1384
|
+
* yields `undefined` from `install`, and {@link createGcCensus} then declines
|
|
1385
|
+
* to exist rather than reporting zeros.
|
|
1386
|
+
*/
|
|
1387
|
+
function nodeGcObserver() {
|
|
1388
|
+
return {
|
|
1389
|
+
kinds: readKinds({
|
|
1390
|
+
major: 4,
|
|
1391
|
+
minor: 1,
|
|
1392
|
+
incremental: 8
|
|
1393
|
+
}),
|
|
1394
|
+
install: (observe) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const observer = new (process.getBuiltinModule("node:perf_hooks")).PerformanceObserver((list) => {
|
|
1397
|
+
for (const entry of list.getEntries()) {
|
|
1398
|
+
const detail = "detail" in entry ? entry.detail : void 0;
|
|
1399
|
+
observe({
|
|
1400
|
+
kind: typeof detail === "object" && detail !== null && "kind" in detail && typeof detail.kind === "number" ? detail.kind : -1,
|
|
1401
|
+
durationMs: entry.duration
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
1406
|
+
return () => {
|
|
1407
|
+
observer.disconnect();
|
|
1408
|
+
};
|
|
1409
|
+
} catch {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
function readKinds(fallback) {
|
|
1416
|
+
try {
|
|
1417
|
+
const c = process.getBuiltinModule("node:perf_hooks").constants;
|
|
1418
|
+
return {
|
|
1419
|
+
major: c.NODE_PERFORMANCE_GC_MAJOR,
|
|
1420
|
+
minor: c.NODE_PERFORMANCE_GC_MINOR,
|
|
1421
|
+
incremental: c.NODE_PERFORMANCE_GC_INCREMENTAL
|
|
1422
|
+
};
|
|
1423
|
+
} catch {
|
|
1424
|
+
return fallback;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* The two fields, APPENDED LAST on the `[mem]` line.
|
|
1429
|
+
*
|
|
1430
|
+
* D446's rule: no existing field moves, so every Loki prefix written since
|
|
1431
|
+
* 2026-08-29 still matches. `gc=` prints even when nothing was collected — a
|
|
1432
|
+
* window with no collections is a reading, and it is the reading that says a
|
|
1433
|
+
* process is idle rather than unobserved. `gcPauseMs=` is the MAIN-THREAD
|
|
1434
|
+
* pause only; the concurrent marking that cost hub-main 70 % of its CPU is on
|
|
1435
|
+
* the platform workers and no in-process counter sees it, which is why the
|
|
1436
|
+
* field is named for the thread it describes (D456, part 4).
|
|
1437
|
+
*/
|
|
1438
|
+
function formatGcCensus(window) {
|
|
1439
|
+
if (window === null || window === void 0) return "";
|
|
1440
|
+
const perMin = (n) => window.windowMs <= 0 ? "?" : (n / (window.windowMs / 6e4)).toFixed(1);
|
|
1441
|
+
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)}`;
|
|
1442
|
+
}
|
|
1443
|
+
//#endregion
|
|
1142
1444
|
//#region src/kernel/heap-watch.ts
|
|
1143
1445
|
/**
|
|
1144
1446
|
* heap-watch — hub-main's own memory, on the record, and returned to the OS.
|
|
@@ -1735,6 +2037,8 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1735
2037
|
const socketPlane = probes?.socketPlane === void 0 ? void 0 : createSocketPlaneMeter(probes.socketPlane);
|
|
1736
2038
|
const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
|
|
1737
2039
|
const now = reclaimOptions?.now ?? (() => Date.now());
|
|
2040
|
+
const gcObserver = probes?.gcObserver ?? nodeGcObserver();
|
|
2041
|
+
const gcCensus = createGcCensus(gcObserver.install, gcObserver.kinds, now());
|
|
1738
2042
|
const triggerMb = reclaimOptions?.triggerMb ?? 1024;
|
|
1739
2043
|
const minIntervalMs = reclaimOptions?.minIntervalMs ?? 12e4;
|
|
1740
2044
|
const steadyStateIntervalMs = reclaimOptions?.steadyStateIntervalMs ?? 6e5;
|
|
@@ -1837,7 +2141,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1837
2141
|
const due = at - lastLoggedAt >= intervalMs;
|
|
1838
2142
|
if (mode === "escalated" || due) {
|
|
1839
2143
|
lastLoggedAt = at;
|
|
1840
|
-
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()));
|
|
2144
|
+
const line = format(label, sample, loopDelay?.read(), budgetMb, `${formatHeapSpaces(readSpaces())}${formatEventPlane(eventPlane?.read())}` + formatSocketPlane(socketPlane?.read()) + formatMeshQueue(meshQueue?.()) + formatGcCensus(gcCensus?.read(at)));
|
|
1841
2145
|
if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
|
|
1842
2146
|
else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
|
|
1843
2147
|
else sink.info(line);
|
|
@@ -1894,6 +2198,7 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1894
2198
|
stopped = true;
|
|
1895
2199
|
clearInterval(timer);
|
|
1896
2200
|
loopDelay?.stop();
|
|
2201
|
+
gcCensus?.stop();
|
|
1897
2202
|
};
|
|
1898
2203
|
}
|
|
1899
2204
|
/**
|
|
@@ -5915,6 +6220,8 @@ var SocketChannel = class {
|
|
|
5915
6220
|
txCounters = createSocketDirectionCounters();
|
|
5916
6221
|
/** Frames read from the peer, cumulative, split by kind. */
|
|
5917
6222
|
rxCounters = createSocketDirectionCounters();
|
|
6223
|
+
/** See {@link CapActionChannelObserver}. Absent = no per-action census. */
|
|
6224
|
+
actionObserver = null;
|
|
5918
6225
|
constructor(socket) {
|
|
5919
6226
|
this.socket = socket;
|
|
5920
6227
|
socket.on("data", (chunk) => this.onData(chunk));
|
|
@@ -5945,16 +6252,26 @@ var SocketChannel = class {
|
|
|
5945
6252
|
}, timeoutMs);
|
|
5946
6253
|
timer.unref?.();
|
|
5947
6254
|
}
|
|
6255
|
+
const label = this.labelSafely(body);
|
|
5948
6256
|
this.pending.set(id, {
|
|
5949
6257
|
resolve,
|
|
5950
6258
|
reject,
|
|
5951
|
-
timer
|
|
6259
|
+
timer,
|
|
6260
|
+
action: null
|
|
5952
6261
|
});
|
|
5953
|
-
this.send({
|
|
6262
|
+
const reqBytes = this.send({
|
|
5954
6263
|
k: "req",
|
|
5955
6264
|
id,
|
|
5956
6265
|
body
|
|
5957
6266
|
}, opts?.payload);
|
|
6267
|
+
const entry = this.pending.get(id);
|
|
6268
|
+
if (entry !== void 0 && label !== void 0) this.pending.set(id, {
|
|
6269
|
+
...entry,
|
|
6270
|
+
action: {
|
|
6271
|
+
label,
|
|
6272
|
+
reqBytes
|
|
6273
|
+
}
|
|
6274
|
+
});
|
|
5958
6275
|
});
|
|
5959
6276
|
}
|
|
5960
6277
|
emit(body) {
|
|
@@ -5964,6 +6281,36 @@ var SocketChannel = class {
|
|
|
5964
6281
|
body
|
|
5965
6282
|
});
|
|
5966
6283
|
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Wire the per-action census. Only one observer is active at a time; an
|
|
6286
|
+
* unwired channel records nothing at all (never zeros).
|
|
6287
|
+
*/
|
|
6288
|
+
observeActions(observer) {
|
|
6289
|
+
this.actionObserver = observer;
|
|
6290
|
+
}
|
|
6291
|
+
/**
|
|
6292
|
+
* The label for one envelope, or `undefined` when there is nothing to
|
|
6293
|
+
* record. Wrapped: a broken labeller must never fail the call it watches —
|
|
6294
|
+
* the same discipline `LocalChildRegistry.recordCapUsage` applies, and for
|
|
6295
|
+
* the same reason.
|
|
6296
|
+
*/
|
|
6297
|
+
labelSafely(body) {
|
|
6298
|
+
const obs = this.actionObserver;
|
|
6299
|
+
if (obs === null) return void 0;
|
|
6300
|
+
try {
|
|
6301
|
+
return obs.label(body);
|
|
6302
|
+
} catch {
|
|
6303
|
+
return null;
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
6306
|
+
/** Settle one request/response pair on the census. Never throws. */
|
|
6307
|
+
recordAction(label, reqBytes, resBytes) {
|
|
6308
|
+
const obs = this.actionObserver;
|
|
6309
|
+
if (obs === null) return;
|
|
6310
|
+
try {
|
|
6311
|
+
obs.record(label, reqBytes, resBytes);
|
|
6312
|
+
} catch {}
|
|
6313
|
+
}
|
|
5967
6314
|
/** Replace the request handler. Only one handler is active at a time. */
|
|
5968
6315
|
onRequest(handler) {
|
|
5969
6316
|
this.requestHandler = handler;
|
|
@@ -6011,21 +6358,27 @@ var SocketChannel = class {
|
|
|
6011
6358
|
* are never copied into the head. A forwarded payload is a view into the
|
|
6012
6359
|
* frame it arrived on, and that frame's buffer is never mutated, so handing
|
|
6013
6360
|
* the view to the socket is safe.
|
|
6361
|
+
*
|
|
6362
|
+
* Returns the on-the-wire bytes it charged — the same number the per-kind
|
|
6363
|
+
* counter got, so the per-action census can never disagree with the per-peer
|
|
6364
|
+
* one. `0` when the channel is closed and nothing was written.
|
|
6014
6365
|
*/
|
|
6015
6366
|
send(frame, payload) {
|
|
6016
|
-
if (this.closed) return;
|
|
6367
|
+
if (this.closed) return 0;
|
|
6017
6368
|
if (payload === void 0) {
|
|
6018
6369
|
const encoded = encodeFrame(frame);
|
|
6019
6370
|
recordSocketFrame(this.txCounters, frame.k, encoded.byteLength);
|
|
6020
6371
|
this.socket.write(encoded);
|
|
6021
|
-
return;
|
|
6372
|
+
return encoded.byteLength;
|
|
6022
6373
|
}
|
|
6023
6374
|
const head = encodeFrameHead(frame, payload);
|
|
6024
|
-
|
|
6375
|
+
const bytes = head.byteLength + payload.byteLength;
|
|
6376
|
+
recordSocketFrame(this.txCounters, frame.k, bytes);
|
|
6025
6377
|
this.socket.cork();
|
|
6026
6378
|
this.socket.write(head);
|
|
6027
6379
|
this.socket.write(payload);
|
|
6028
6380
|
this.socket.uncork();
|
|
6381
|
+
return bytes;
|
|
6029
6382
|
}
|
|
6030
6383
|
onData(chunk) {
|
|
6031
6384
|
const frames = this.decoder.push(chunk);
|
|
@@ -6044,28 +6397,31 @@ var SocketChannel = class {
|
|
|
6044
6397
|
async handleFrame(frame, wireBytes) {
|
|
6045
6398
|
recordSocketFrame(this.rxCounters, frame.k, wireBytes);
|
|
6046
6399
|
if (frame.k === "req") {
|
|
6400
|
+
const label = this.labelSafely(frame.body);
|
|
6401
|
+
let resBytes = 0;
|
|
6047
6402
|
try {
|
|
6048
6403
|
const result = await this.requestHandler(frame.body, frame.pl);
|
|
6049
|
-
if (result instanceof PayloadReply) this.send({
|
|
6404
|
+
if (result instanceof PayloadReply) resBytes = this.send({
|
|
6050
6405
|
k: "res",
|
|
6051
6406
|
id: frame.id,
|
|
6052
6407
|
ok: true,
|
|
6053
6408
|
body: result.body
|
|
6054
6409
|
}, result.payload);
|
|
6055
|
-
else this.send({
|
|
6410
|
+
else resBytes = this.send({
|
|
6056
6411
|
k: "res",
|
|
6057
6412
|
id: frame.id,
|
|
6058
6413
|
ok: true,
|
|
6059
6414
|
body: result
|
|
6060
6415
|
});
|
|
6061
6416
|
} catch (err) {
|
|
6062
|
-
this.send({
|
|
6417
|
+
resBytes = this.send({
|
|
6063
6418
|
k: "res",
|
|
6064
6419
|
id: frame.id,
|
|
6065
6420
|
ok: false,
|
|
6066
6421
|
err: err instanceof Error ? err.message : String(err)
|
|
6067
6422
|
});
|
|
6068
6423
|
}
|
|
6424
|
+
if (label !== void 0) this.recordAction(label, wireBytes, resBytes);
|
|
6069
6425
|
return;
|
|
6070
6426
|
}
|
|
6071
6427
|
if (frame.k === "res") {
|
|
@@ -6073,6 +6429,7 @@ var SocketChannel = class {
|
|
|
6073
6429
|
if (!p) return;
|
|
6074
6430
|
this.pending.delete(frame.id);
|
|
6075
6431
|
if (p.timer !== null) clearTimeout(p.timer);
|
|
6432
|
+
if (p.action !== null) this.recordAction(p.action.label, p.action.reqBytes, wireBytes);
|
|
6076
6433
|
if (!frame.ok) p.reject(new Error(frame.err));
|
|
6077
6434
|
else if (frame.pl !== void 0) p.resolve(new PayloadReply(frame.body, frame.pl));
|
|
6078
6435
|
else p.resolve(frame.body);
|
|
@@ -6626,6 +6983,64 @@ function deliverShared(state, event) {
|
|
|
6626
6983
|
}
|
|
6627
6984
|
}
|
|
6628
6985
|
//#endregion
|
|
6986
|
+
//#region src/kernel/transport/cap-child-index.ts
|
|
6987
|
+
/** Separator that cannot occur in a kebab cap name, so the key is unambiguous. */
|
|
6988
|
+
var DEVICE_KEY_SEPARATOR = "\0";
|
|
6989
|
+
var NO_CANDIDATES = Object.freeze([]);
|
|
6990
|
+
var MapCapChildIndex = class {
|
|
6991
|
+
deviceOwners;
|
|
6992
|
+
singletons;
|
|
6993
|
+
constructor(deviceOwners, singletons) {
|
|
6994
|
+
this.deviceOwners = deviceOwners;
|
|
6995
|
+
this.singletons = singletons;
|
|
6996
|
+
}
|
|
6997
|
+
deviceOwner(capName, deviceId) {
|
|
6998
|
+
return this.deviceOwners.get(`${capName}${DEVICE_KEY_SEPARATOR}${deviceId}`) ?? null;
|
|
6999
|
+
}
|
|
7000
|
+
singletonCandidates(capName) {
|
|
7001
|
+
return this.singletons.get(capName) ?? NO_CANDIDATES;
|
|
7002
|
+
}
|
|
7003
|
+
};
|
|
7004
|
+
/**
|
|
7005
|
+
* Build the `(cap, device?) → child` index from the connected children.
|
|
7006
|
+
*
|
|
7007
|
+
* A PURE function of what it is handed — it captures nothing, subscribes to
|
|
7008
|
+
* nothing and cannot be updated in place. That is the whole safety argument
|
|
7009
|
+
* (D3, D457): the only way to change what it says is to build a new one from
|
|
7010
|
+
* the `children` map, so an index can never name a child that map has already
|
|
7011
|
+
* dropped. An incremental index — add on register, remove on close — would be
|
|
7012
|
+
* a second registry that can disagree with the first, and a stale one routes
|
|
7013
|
+
* to a dead runner, which is strictly worse than a slow scan.
|
|
7014
|
+
*
|
|
7015
|
+
* Cost: O(children × descriptors), paid once per mutation of the children map
|
|
7016
|
+
* (a register, a re-register, a disconnect) rather than once per cap call.
|
|
7017
|
+
*
|
|
7018
|
+
* The two tiers stay separate, exactly as the scan had them: a device lookup
|
|
7019
|
+
* is never answered from a deviceId-less descriptor. `resolveChildId` owns
|
|
7020
|
+
* the fallback from tier 1 to tier 2, and folding it in here would make it
|
|
7021
|
+
* unreachable.
|
|
7022
|
+
*/
|
|
7023
|
+
function buildCapChildIndex(entries) {
|
|
7024
|
+
const deviceOwners = /* @__PURE__ */ new Map();
|
|
7025
|
+
const singletons = /* @__PURE__ */ new Map();
|
|
7026
|
+
for (const entry of entries) {
|
|
7027
|
+
const namedForThisChild = /* @__PURE__ */ new Set();
|
|
7028
|
+
for (const cap of entry.caps) {
|
|
7029
|
+
if (cap.deviceId === void 0) {
|
|
7030
|
+
if (namedForThisChild.has(cap.capName)) continue;
|
|
7031
|
+
namedForThisChild.add(cap.capName);
|
|
7032
|
+
const candidates = singletons.get(cap.capName);
|
|
7033
|
+
if (candidates === void 0) singletons.set(cap.capName, [entry.childId]);
|
|
7034
|
+
else candidates.push(entry.childId);
|
|
7035
|
+
continue;
|
|
7036
|
+
}
|
|
7037
|
+
const key = `${cap.capName}${DEVICE_KEY_SEPARATOR}${cap.deviceId}`;
|
|
7038
|
+
if (!deviceOwners.has(key)) deviceOwners.set(key, entry.childId);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
return new MapCapChildIndex(deviceOwners, singletons);
|
|
7042
|
+
}
|
|
7043
|
+
//#endregion
|
|
6629
7044
|
//#region src/kernel/transport/cap-routing-hints.ts
|
|
6630
7045
|
/** Lift {@link CapRoutingHints} from a call's args. Never throws. */
|
|
6631
7046
|
function liftRoutingHints(args) {
|
|
@@ -6637,8 +7052,6 @@ function liftRoutingHints(args) {
|
|
|
6637
7052
|
...typeof addonId === "string" && addonId.length > 0 ? { addonId } : {}
|
|
6638
7053
|
};
|
|
6639
7054
|
}
|
|
6640
|
-
//#endregion
|
|
6641
|
-
//#region src/kernel/transport/local-child-registry.ts
|
|
6642
7055
|
function readFanoutMode() {
|
|
6643
7056
|
const raw = process.env.CAMSTACK_UDS_EVENT_FANOUT;
|
|
6644
7057
|
if (raw === "shadow" || raw === "broadcast") return raw;
|
|
@@ -6679,6 +7092,14 @@ var LocalChildRegistry = class {
|
|
|
6679
7092
|
/** See {@link ForwardCensus}. Mutable counters on the hot path, by design. */
|
|
6680
7093
|
forwardedRaw = 0;
|
|
6681
7094
|
forwardedDecoded = 0;
|
|
7095
|
+
/**
|
|
7096
|
+
* The per-ACTION byte census (D456). `ForwardCensus` above is the same
|
|
7097
|
+
* question asked of the whole plane; this one asks it per `<cap>.<method>`,
|
|
7098
|
+
* beside the bytes that method actually moved. Both are kept: `socketFwd=`
|
|
7099
|
+
* says whether the negotiation is working at all, and a `raw=0` on ONE
|
|
7100
|
+
* method with `raw>0` on its neighbours is a different fault.
|
|
7101
|
+
*/
|
|
7102
|
+
actionCensus = createCapActionCensus();
|
|
6682
7103
|
/** See {@link LocalChildRegistryOptions.capTimeoutMs}. */
|
|
6683
7104
|
capTimeoutMs;
|
|
6684
7105
|
/** See {@link LocalChildRegistryOptions.capUsageObserver}. */
|
|
@@ -6692,6 +7113,12 @@ var LocalChildRegistry = class {
|
|
|
6692
7113
|
/** Last pattern-set string logged per child, to dedup the INFO line. */
|
|
6693
7114
|
loggedPatternSet = /* @__PURE__ */ new Map();
|
|
6694
7115
|
/**
|
|
7116
|
+
* Derived `(cap, device?) → child` index over {@link children}, or `null`
|
|
7117
|
+
* when it must be rebuilt. See {@link capIndex} for why it is a cache and
|
|
7118
|
+
* never a registry (D3, D457).
|
|
7119
|
+
*/
|
|
7120
|
+
capChildIndex = null;
|
|
7121
|
+
/**
|
|
6695
7122
|
* Accepts either a plain positional `server` argument (backward-compatible)
|
|
6696
7123
|
* or a full `LocalChildRegistryOptions` object.
|
|
6697
7124
|
*
|
|
@@ -6772,6 +7199,21 @@ var LocalChildRegistry = class {
|
|
|
6772
7199
|
};
|
|
6773
7200
|
}
|
|
6774
7201
|
/**
|
|
7202
|
+
* Cumulative per-action byte census: peer → `<cap>.<method>` → bytes, calls
|
|
7203
|
+
* and the raw/decoded split. A COPY (`cap-action-census.ts` explains why it
|
|
7204
|
+
* must be one); the heartbeat subtracts the previous reading.
|
|
7205
|
+
*
|
|
7206
|
+
* This is the instrument D454 closed by asking for. `socketTopBytes=` names
|
|
7207
|
+
* the peer that moves the bytes and `txkB=` says they are responses; neither
|
|
7208
|
+
* can say WHICH method originates them, and after raw-forward the bytes that
|
|
7209
|
+
* cross are no longer the proxy for what hub-main pays — 85.7 % of sibling
|
|
7210
|
+
* calls cross unread. What costs is what hub-main LOOKS at, and that is the
|
|
7211
|
+
* `decoded` half of an entry, next to the bytes it carried.
|
|
7212
|
+
*/
|
|
7213
|
+
readActionCensus() {
|
|
7214
|
+
return this.actionCensus.read();
|
|
7215
|
+
}
|
|
7216
|
+
/**
|
|
6775
7217
|
* The regime the counters above were produced under, read once from
|
|
6776
7218
|
* `CAMSTACK_UDS_EVENT_FANOUT` at construction.
|
|
6777
7219
|
*
|
|
@@ -6885,11 +7327,12 @@ var LocalChildRegistry = class {
|
|
|
6885
7327
|
* compete for the same capName.
|
|
6886
7328
|
*/
|
|
6887
7329
|
resolveChildId(capName, deviceId) {
|
|
7330
|
+
const index = this.capIndex();
|
|
6888
7331
|
if (deviceId !== void 0) {
|
|
6889
|
-
const deviceOwner =
|
|
7332
|
+
const deviceOwner = index.deviceOwner(capName, deviceId);
|
|
6890
7333
|
if (deviceOwner !== null) return deviceOwner;
|
|
6891
7334
|
}
|
|
6892
|
-
const candidates =
|
|
7335
|
+
const candidates = index.singletonCandidates(capName);
|
|
6893
7336
|
if (candidates.length === 0) return null;
|
|
6894
7337
|
if (candidates.length === 1) return candidates[0];
|
|
6895
7338
|
const preferredAddonId = this.getActiveSingletonAddonId?.(capName) ?? null;
|
|
@@ -6922,16 +7365,37 @@ var LocalChildRegistry = class {
|
|
|
6922
7365
|
});
|
|
6923
7366
|
} catch {}
|
|
6924
7367
|
}
|
|
6925
|
-
/**
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7368
|
+
/**
|
|
7369
|
+
* The cap index over the CURRENT children, rebuilt on first use after any
|
|
7370
|
+
* mutation of {@link children}.
|
|
7371
|
+
*
|
|
7372
|
+
* This replaced a linear walk of every child's cap manifest per
|
|
7373
|
+
* `cap-call-out`, measured at **7.1 % of hub-main's busy main thread**
|
|
7374
|
+
* across 39 children (D456). Why it did not exist before is the whole
|
|
7375
|
+
* design: what it indexes moves — a runner is replaced on every update, a
|
|
7376
|
+
* crash respawns one, a child re-registers with a replacement manifest after
|
|
7377
|
+
* init — and an index that names a dead child routes work into a socket
|
|
7378
|
+
* nobody is reading, which is strictly worse than a slow scan.
|
|
7379
|
+
*
|
|
7380
|
+
* So it is not maintained; it is DISCARDED. `buildCapChildIndex` is a pure
|
|
7381
|
+
* function of `children.values()`, the only two writers of that map
|
|
7382
|
+
* ({@link invalidateCapIndex}'s call sites) drop the index in the same
|
|
7383
|
+
* statement, and nothing else can construct one. There is no add-on-register
|
|
7384
|
+
* / remove-on-close path that could disagree with the map — i.e. no shadow
|
|
7385
|
+
* registry (D3): the map remains the single authority and this holds no fact
|
|
7386
|
+
* it does not.
|
|
7387
|
+
*
|
|
7388
|
+
* The operator's active-singleton preference is deliberately NOT indexed —
|
|
7389
|
+
* it changes without any child connecting or leaving, so `resolveChildId`
|
|
7390
|
+
* reads it live and the index only supplies the candidate list.
|
|
7391
|
+
*/
|
|
7392
|
+
capIndex() {
|
|
7393
|
+
if (this.capChildIndex === null) this.capChildIndex = buildCapChildIndex(this.children.values());
|
|
7394
|
+
return this.capChildIndex;
|
|
6929
7395
|
}
|
|
6930
|
-
/**
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
for (const entry of this.children.values()) if (entry.caps.some(predicate)) out.push(entry.childId);
|
|
6934
|
-
return out;
|
|
7396
|
+
/** Drop the derived index. MUST follow every write to {@link children}. */
|
|
7397
|
+
invalidateCapIndex() {
|
|
7398
|
+
this.capChildIndex = null;
|
|
6935
7399
|
}
|
|
6936
7400
|
/**
|
|
6937
7401
|
* Does the named child currently provide `(capName, deviceId?)`?
|
|
@@ -7127,6 +7591,14 @@ var LocalChildRegistry = class {
|
|
|
7127
7591
|
onConnection(channel) {
|
|
7128
7592
|
let childId = null;
|
|
7129
7593
|
const incarnation = this.nextIncarnation++;
|
|
7594
|
+
channel.observeActions?.({
|
|
7595
|
+
label: capActionLabel,
|
|
7596
|
+
record: (label, reqBytes, resBytes) => {
|
|
7597
|
+
const peerId = childId ?? "(pre-register)";
|
|
7598
|
+
if (label === null) this.actionCensus.recordUnlabelled(peerId, reqBytes + resBytes);
|
|
7599
|
+
else this.actionCensus.record(peerId, label, reqBytes, resBytes);
|
|
7600
|
+
}
|
|
7601
|
+
});
|
|
7130
7602
|
channel.onEvent((body) => {
|
|
7131
7603
|
const msg = body;
|
|
7132
7604
|
if (msg.kind === "event") {
|
|
@@ -7169,6 +7641,7 @@ var LocalChildRegistry = class {
|
|
|
7169
7641
|
incarnation,
|
|
7170
7642
|
rawForward: msg.rawForward === true
|
|
7171
7643
|
});
|
|
7644
|
+
this.invalidateCapIndex();
|
|
7172
7645
|
this.logPatternSet(msg.childId, eventPatterns);
|
|
7173
7646
|
this.registeredHandler({
|
|
7174
7647
|
childId: msg.childId,
|
|
@@ -7200,6 +7673,8 @@ var LocalChildRegistry = class {
|
|
|
7200
7673
|
const addonPinned = pinnedNodeId === void 0 && this.isAddonPinnedCall?.(out.capName, out.method, hints) === true;
|
|
7201
7674
|
const target = !(out.native === true) && !aggregated && !addonPinned && (pinnedNodeId === void 0 || pinTargetsThisNode) ? this.resolveChildId(out.capName, out.deviceId) : null;
|
|
7202
7675
|
this.recordCapUsage(childId, target, out.capName, out.method);
|
|
7676
|
+
const actionPeer = childId ?? "(pre-register)";
|
|
7677
|
+
const actionLabel = `${out.capName}.${out.method}`;
|
|
7203
7678
|
if (target !== null) {
|
|
7204
7679
|
if (!this.egressRoutedCaps.has(out.capName)) {
|
|
7205
7680
|
this.egressRoutedCaps.add(out.capName);
|
|
@@ -7208,6 +7683,7 @@ var LocalChildRegistry = class {
|
|
|
7208
7683
|
const entry = this.children.get(target);
|
|
7209
7684
|
if (payload !== void 0 && entry !== void 0 && entry.rawForward) {
|
|
7210
7685
|
this.forwardedRaw += 1;
|
|
7686
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "raw");
|
|
7211
7687
|
const forward = {
|
|
7212
7688
|
kind: "cap-call",
|
|
7213
7689
|
capName: out.capName,
|
|
@@ -7222,9 +7698,12 @@ var LocalChildRegistry = class {
|
|
|
7222
7698
|
});
|
|
7223
7699
|
}
|
|
7224
7700
|
this.forwardedDecoded += 1;
|
|
7225
|
-
|
|
7701
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7702
|
+
const input = buildInput();
|
|
7703
|
+
return entry !== void 0 ? this.callCapOnChild(target, input) : this.callCap(input);
|
|
7226
7704
|
}
|
|
7227
7705
|
this.forwardedDecoded += 1;
|
|
7706
|
+
this.actionCensus.recordForward(actionPeer, actionLabel, "decoded");
|
|
7228
7707
|
if (this.onUnownedCall !== void 0) return this.onUnownedCall(buildInput());
|
|
7229
7708
|
throw new Error(`${UDS_NO_ROUTE_PREFIX}: cap-call-out has no local provider for "${out.capName}" and no fallback`);
|
|
7230
7709
|
}
|
|
@@ -7247,7 +7726,9 @@ var LocalChildRegistry = class {
|
|
|
7247
7726
|
return;
|
|
7248
7727
|
}
|
|
7249
7728
|
this.children.delete(childId);
|
|
7729
|
+
this.invalidateCapIndex();
|
|
7250
7730
|
this.childEventStats.delete(childId);
|
|
7731
|
+
this.actionCensus.forget(childId);
|
|
7251
7732
|
this.loggedPatternSet.delete(childId);
|
|
7252
7733
|
this.goneHandler(childId, incarnation);
|
|
7253
7734
|
});
|
|
@@ -10747,6 +11228,18 @@ Object.defineProperty(exports, "AddonDepsManager", {
|
|
|
10747
11228
|
return AddonDepsManager;
|
|
10748
11229
|
}
|
|
10749
11230
|
});
|
|
11231
|
+
Object.defineProperty(exports, "CAP_ACTION_OVERFLOW_LABEL", {
|
|
11232
|
+
enumerable: true,
|
|
11233
|
+
get: function() {
|
|
11234
|
+
return CAP_ACTION_OVERFLOW_LABEL;
|
|
11235
|
+
}
|
|
11236
|
+
});
|
|
11237
|
+
Object.defineProperty(exports, "CAP_ACTION_UNLABELLED", {
|
|
11238
|
+
enumerable: true,
|
|
11239
|
+
get: function() {
|
|
11240
|
+
return CAP_ACTION_UNLABELLED;
|
|
11241
|
+
}
|
|
11242
|
+
});
|
|
10750
11243
|
Object.defineProperty(exports, "CapRouteError", {
|
|
10751
11244
|
enumerable: true,
|
|
10752
11245
|
get: function() {
|
|
@@ -10789,6 +11282,12 @@ Object.defineProperty(exports, "DeviceRegistry", {
|
|
|
10789
11282
|
return DeviceRegistry;
|
|
10790
11283
|
}
|
|
10791
11284
|
});
|
|
11285
|
+
Object.defineProperty(exports, "EMPTY_CAP_ACTION_READING", {
|
|
11286
|
+
enumerable: true,
|
|
11287
|
+
get: function() {
|
|
11288
|
+
return EMPTY_CAP_ACTION_READING;
|
|
11289
|
+
}
|
|
11290
|
+
});
|
|
10792
11291
|
Object.defineProperty(exports, "EMPTY_SOCKET_DIRECTION", {
|
|
10793
11292
|
enumerable: true,
|
|
10794
11293
|
get: function() {
|
|
@@ -10885,6 +11384,12 @@ Object.defineProperty(exports, "LocalChildRegistry", {
|
|
|
10885
11384
|
return LocalChildRegistry;
|
|
10886
11385
|
}
|
|
10887
11386
|
});
|
|
11387
|
+
Object.defineProperty(exports, "MAX_CAP_ACTIONS_PER_PEER", {
|
|
11388
|
+
enumerable: true,
|
|
11389
|
+
get: function() {
|
|
11390
|
+
return MAX_CAP_ACTIONS_PER_PEER;
|
|
11391
|
+
}
|
|
11392
|
+
});
|
|
10888
11393
|
Object.defineProperty(exports, "MESH_QUEUE_TOP_N", {
|
|
10889
11394
|
enumerable: true,
|
|
10890
11395
|
get: function() {
|
|
@@ -11041,6 +11546,12 @@ Object.defineProperty(exports, "callWithServiceDiscovery", {
|
|
|
11041
11546
|
return callWithServiceDiscovery;
|
|
11042
11547
|
}
|
|
11043
11548
|
});
|
|
11549
|
+
Object.defineProperty(exports, "capActionLabel", {
|
|
11550
|
+
enumerable: true,
|
|
11551
|
+
get: function() {
|
|
11552
|
+
return capActionLabel;
|
|
11553
|
+
}
|
|
11554
|
+
});
|
|
11044
11555
|
Object.defineProperty(exports, "capActionName", {
|
|
11045
11556
|
enumerable: true,
|
|
11046
11557
|
get: function() {
|
|
@@ -11113,6 +11624,12 @@ Object.defineProperty(exports, "createBrokerDeviceManagerApi", {
|
|
|
11113
11624
|
return createBrokerDeviceManagerApi;
|
|
11114
11625
|
}
|
|
11115
11626
|
});
|
|
11627
|
+
Object.defineProperty(exports, "createCapActionCensus", {
|
|
11628
|
+
enumerable: true,
|
|
11629
|
+
get: function() {
|
|
11630
|
+
return createCapActionCensus;
|
|
11631
|
+
}
|
|
11632
|
+
});
|
|
11116
11633
|
Object.defineProperty(exports, "createDeferredHeapWatchSink", {
|
|
11117
11634
|
enumerable: true,
|
|
11118
11635
|
get: function() {
|
|
@@ -11239,6 +11756,12 @@ Object.defineProperty(exports, "deserializeTypedArrays", {
|
|
|
11239
11756
|
return deserializeTypedArrays;
|
|
11240
11757
|
}
|
|
11241
11758
|
});
|
|
11759
|
+
Object.defineProperty(exports, "diffCapActionCensus", {
|
|
11760
|
+
enumerable: true,
|
|
11761
|
+
get: function() {
|
|
11762
|
+
return diffCapActionCensus;
|
|
11763
|
+
}
|
|
11764
|
+
});
|
|
11242
11765
|
Object.defineProperty(exports, "diffEventPlane", {
|
|
11243
11766
|
enumerable: true,
|
|
11244
11767
|
get: function() {
|