@camstack/system 1.2.217 → 1.2.218
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 +9 -6
- package/dist/addon-runner.mjs +9 -6
- 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.d.ts +0 -7
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -8
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -8
- 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-jY0NOZK8.js → custom-action-registry-BDwM5qAK.js} +19 -0
- package/dist/{custom-action-registry-F__gp_VX.mjs → custom-action-registry-CsONUlhC.mjs} +19 -0
- package/dist/{dist-C85OOASj.js → dist-BOlMCOeT.js} +116 -1
- package/dist/{dist-DtDKMSeG.mjs → dist-DigmGsQS.mjs} +111 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +11 -3
- package/dist/index.mjs +4 -4
- package/dist/kernel/array-buffer-census.d.ts +122 -0
- package/dist/kernel/custom-action-registry.d.ts +10 -1
- package/dist/kernel/heap-watch.d.ts +15 -0
- package/dist/kernel/transport/child-cap-protocol.d.ts +15 -1
- package/dist/kernel/transport/local-child-client.d.ts +18 -2
- package/dist/{manifest-system-deps-DvY1bY_n.js → manifest-system-deps-Bo92lg5u.js} +350 -14
- package/dist/{manifest-system-deps-726RBOX7.mjs → manifest-system-deps-D--wyMD9.mjs} +303 -15
- package/dist/{retired-settings-keys-Cx-lYIbU.mjs → retired-settings-keys-vK_U-CG2.mjs} +1 -1
- package/dist/{retired-settings-keys-Dfj9LA-i.js → retired-settings-keys-xEAlYnv7.js} +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CustomActionCaller, IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
|
|
1
|
+
import { ChildCustomActionCatalogs, CustomActionCaller, IReadinessRegistryRecord, LogLevel, LogTags, SystemEvent } from '@camstack/types';
|
|
2
2
|
/**
|
|
3
3
|
* Routing descriptor a child sends so the parent can route cap calls to it.
|
|
4
4
|
* The provider implementation never crosses the wire — only these keys do.
|
|
@@ -35,6 +35,18 @@ export interface RegisterMessage {
|
|
|
35
35
|
* `EventSubUpdateMessage` for post-boot updates.
|
|
36
36
|
*/
|
|
37
37
|
readonly eventPatterns?: readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* Custom-action catalogs of the addons this child hosts, keyed by addon id,
|
|
40
|
+
* with the Zod schemas stripped — see `CustomActionDescriptor`. Carried IN
|
|
41
|
+
* the register frame for the same reason as `eventPatterns`: the parent
|
|
42
|
+
* learns it race-free, at the post-init re-register, FROM THE PROCESS THAT
|
|
43
|
+
* OWNS THE ADDON. It is never learned by evaluating the addon's bundle in the
|
|
44
|
+
* parent (D444: ten 9 MB evaluations in hub-main, none of which produced a
|
|
45
|
+
* catalog, and every one pinned the evicted graph's module-scope state).
|
|
46
|
+
* OPTIONAL: absent from the pre-init register (the catalogs do not exist yet)
|
|
47
|
+
* and from a legacy child; absent means "not described yet", never "none".
|
|
48
|
+
*/
|
|
49
|
+
readonly customActions?: ChildCustomActionCatalogs;
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
40
52
|
* Child → parent, fire-and-forget: the child's category-pattern subscription
|
|
@@ -248,6 +260,8 @@ export interface RegisteredChild {
|
|
|
248
260
|
* one. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
|
|
249
261
|
*/
|
|
250
262
|
readonly incarnation: number;
|
|
263
|
+
/** See {@link RegisterMessage.customActions}. Absent = not described yet. */
|
|
264
|
+
readonly customActions?: ChildCustomActionCatalogs;
|
|
251
265
|
}
|
|
252
266
|
/** Arguments to route a cap method call (the `cap-call` message minus its discriminant). */
|
|
253
267
|
export type CapCallInput = Omit<CapCallMessage, 'kind'>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
|
|
1
|
+
import { ChildCustomActionCatalogs, IReadinessRegistryRecord, SystemEvent } from '@camstack/types';
|
|
2
2
|
import { AddonCallInput, CapCallInput, ChildCapDescriptor, ChildLogMessage } from './child-cap-protocol.js';
|
|
3
3
|
export interface LocalChildClientOptions {
|
|
4
4
|
/** Parent node id — selects the UDS endpoint this child connects to. */
|
|
@@ -56,6 +56,15 @@ export declare class LocalChildClient {
|
|
|
56
56
|
* subscriptions do), the register carries the real union — even if empty.
|
|
57
57
|
*/
|
|
58
58
|
private hasDeclaredEventPatterns;
|
|
59
|
+
/**
|
|
60
|
+
* The custom-action catalogs of every hosted addon, once the runner's init
|
|
61
|
+
* loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
|
|
62
|
+
* until then, and OMITTED from the register frame while null: the parent
|
|
63
|
+
* reads absence as "not described yet", and a pre-init register must never
|
|
64
|
+
* look like "this child declares no actions". Kept on the client so every
|
|
65
|
+
* later re-register (native-cap change, reconnect) re-carries it unchanged.
|
|
66
|
+
*/
|
|
67
|
+
private latestCustomActions;
|
|
59
68
|
/** Events and logs queued while the channel is not yet open. */
|
|
60
69
|
private readonly pendingEmits;
|
|
61
70
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
@@ -138,7 +147,14 @@ export declare class LocalChildClient {
|
|
|
138
147
|
* it (device-restore can race ahead of the UDS connect). After `start()`,
|
|
139
148
|
* the set is sent immediately.
|
|
140
149
|
*/
|
|
141
|
-
updateCaps(caps: readonly ChildCapDescriptor[]): Promise<void>;
|
|
150
|
+
updateCaps(caps: readonly ChildCapDescriptor[], customActions?: ChildCustomActionCatalogs): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* The register frame: caps, plus everything a re-register must atomically
|
|
153
|
+
* re-carry so no separate re-sync can be missed — the subscription union
|
|
154
|
+
* (omitted if nothing ever declared, which keeps the fail-open shape) and the
|
|
155
|
+
* custom-action catalogs (omitted until the init loop produced them).
|
|
156
|
+
*/
|
|
157
|
+
private registerFrame;
|
|
142
158
|
/**
|
|
143
159
|
* Fire-and-forget: send a system event to the parent for forwarding to the
|
|
144
160
|
* hub event bus. Safe to call before `start()` — events are buffered and
|
|
@@ -15,6 +15,7 @@ node_os = require_chunk.__toESM(node_os);
|
|
|
15
15
|
let node_http = require("node:http");
|
|
16
16
|
let node_v8 = require("node:v8");
|
|
17
17
|
node_v8 = require_chunk.__toESM(node_v8);
|
|
18
|
+
let node_inspector_promises = require("node:inspector/promises");
|
|
18
19
|
let node_perf_hooks = require("node:perf_hooks");
|
|
19
20
|
let node_vm = require("node:vm");
|
|
20
21
|
node_vm = require_chunk.__toESM(node_vm);
|
|
@@ -514,7 +515,7 @@ var DECISIVE_HEAP_SPACES = ["old_space", "large_object_space"];
|
|
|
514
515
|
var HEAP_SPACE_REPORT_MIN_MB = 32;
|
|
515
516
|
var BYTES_PER_MB = 1048576;
|
|
516
517
|
/** Whole MB, matching the rest of the `[mem]` line. */
|
|
517
|
-
function mb(bytes) {
|
|
518
|
+
function mb$1(bytes) {
|
|
518
519
|
return Math.round(bytes / BYTES_PER_MB);
|
|
519
520
|
}
|
|
520
521
|
/** Tenths of an MB — the resolution the SIGUSR2 report has always printed. */
|
|
@@ -561,7 +562,7 @@ function heapSpaceField(v8Name) {
|
|
|
561
562
|
*/
|
|
562
563
|
function selectReportedSpaces(spaces, minMb = 32) {
|
|
563
564
|
const decisive = DECISIVE_HEAP_SPACES.map((name) => spaces.find((space) => space.name === name)).filter((space) => space !== void 0);
|
|
564
|
-
const rest = spaces.filter((space) => !DECISIVE_HEAP_SPACES.includes(space.name)).filter((space) => mb(space.usedBytes) >= minMb).toSorted((a, b) => b.usedBytes - a.usedBytes || a.name.localeCompare(b.name));
|
|
565
|
+
const rest = spaces.filter((space) => !DECISIVE_HEAP_SPACES.includes(space.name)).filter((space) => mb$1(space.usedBytes) >= minMb).toSorted((a, b) => b.usedBytes - a.usedBytes || a.name.localeCompare(b.name));
|
|
565
566
|
return [...decisive, ...rest];
|
|
566
567
|
}
|
|
567
568
|
/**
|
|
@@ -576,7 +577,7 @@ function selectReportedSpaces(spaces, minMb = 32) {
|
|
|
576
577
|
function formatHeapSpaces(spaces, minMb = 32) {
|
|
577
578
|
const selected = selectReportedSpaces(spaces, minMb);
|
|
578
579
|
if (selected.length === 0) return "";
|
|
579
|
-
return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb(s.usedBytes)}MB`).join(" ")}`;
|
|
580
|
+
return ` ${selected.map((s) => `${heapSpaceField(s.name)}=${mb$1(s.usedBytes)}MB`).join(" ")}`;
|
|
580
581
|
}
|
|
581
582
|
var defaultDeps = {
|
|
582
583
|
readMemory: () => process.memoryUsage(),
|
|
@@ -606,6 +607,251 @@ function emitHeapDiagnosticReport(request, deps = defaultDeps) {
|
|
|
606
607
|
}
|
|
607
608
|
}
|
|
608
609
|
//#endregion
|
|
610
|
+
//#region src/kernel/array-buffer-census.ts
|
|
611
|
+
/**
|
|
612
|
+
* array-buffer-census — WHAT SHAPE the live ArrayBuffers have, taken by the
|
|
613
|
+
* heartbeat itself when they grow, because nobody can be awake for it.
|
|
614
|
+
*
|
|
615
|
+
* ## The episodes this exists for
|
|
616
|
+
*
|
|
617
|
+
* hub-main, 2026-09-10 (CEST): `arrayBuffers` climbed linearly and released in
|
|
618
|
+
* one instant, twice, with not one line from the process at either boundary —
|
|
619
|
+
* 17:26→18:31 at ~20 MB/min to 1 264 MB, released in one heartbeat; 22:20→23:36
|
|
620
|
+
* at 60–85 MB/min to **4 968 MB**, released between two probes at 23:38. Both
|
|
621
|
+
* were LIVE: a forced compacting GC at 23:36:40 returned 4 968→4 954 MB. The
|
|
622
|
+
* measurement that names their shape had been prepared (D443) and was never
|
|
623
|
+
* run, because it needed a human to run it during the episode, and the
|
|
624
|
+
* episode does not wait. Two hours after the second release the process died
|
|
625
|
+
* on `Ineffective mark-compacts near heap limit`.
|
|
626
|
+
*
|
|
627
|
+
* ## What this is, and what it is NOT
|
|
628
|
+
*
|
|
629
|
+
* `Runtime.queryObjects(ArrayBuffer.prototype)` over an IN-PROCESS inspector
|
|
630
|
+
* session, followed by `Runtime.callFunctionOn` computing a byteLength
|
|
631
|
+
* histogram over the result array, returned BY VALUE. It walks the heap in
|
|
632
|
+
* memory and returns a few hundred bytes of JSON. Nothing is serialised,
|
|
633
|
+
* nothing is written, no file exists afterwards.
|
|
634
|
+
*
|
|
635
|
+
* It is NOT a heap snapshot. `scripts/check-no-heap-dump-command.ts` forbids
|
|
636
|
+
* `writeHeapSnapshot` because a multi-GB serialisation of the heap took the
|
|
637
|
+
* whole host down twice; this module contains no such call and must never
|
|
638
|
+
* grow one. The two look alike only in that both need a GC first.
|
|
639
|
+
*
|
|
640
|
+
* ## What it costs, and why the line says so
|
|
641
|
+
*
|
|
642
|
+
* `queryObjects` forces ONE full garbage collection before it walks — the same
|
|
643
|
+
* stop-the-world the reclaim pass already pays every 10 minutes on this
|
|
644
|
+
* process (~1 s at a 2.5 GB heap). The cost is printed on the line it
|
|
645
|
+
* produces, because a pause with no owner in the log is exactly the kind of
|
|
646
|
+
* stall D181 warns about on the process that mediates every cross-runner call.
|
|
647
|
+
* The gate ({@link createArrayBufferCensusGate}) bounds it: a threshold, held
|
|
648
|
+
* for a sustain window so a burst that dies in the next scavenge never pays,
|
|
649
|
+
* and a generous cooldown so an hour-long episode buys a handful of readings,
|
|
650
|
+
* not one per probe.
|
|
651
|
+
*
|
|
652
|
+
* ## Why the byteLength histogram, and not "who holds them"
|
|
653
|
+
*
|
|
654
|
+
* `queryObjects` returns the objects, not their retainers; a retainer walk is
|
|
655
|
+
* a heap snapshot by another name. But the SIZE distribution already
|
|
656
|
+
* discriminates the candidates that were argued about for a night: 65 536-byte
|
|
657
|
+
* buffers are socket chunks, 8 192-byte ones are Buffer-pool slabs, a few
|
|
658
|
+
* hundred equal multi-megabyte ones are frames or JPEGs, and the count says
|
|
659
|
+
* whether it is thousands of small or dozens of huge. The exact top sizes are
|
|
660
|
+
* printed as well as power-of-two buckets, because "1 462 272 B × 880" names
|
|
661
|
+
* a producer where "≤2 MB: 1.2 GB" does not.
|
|
662
|
+
*/
|
|
663
|
+
/**
|
|
664
|
+
* Take the census once `arrayBuffers` has been at or above this for
|
|
665
|
+
* {@link ARRAY_BUFFER_CENSUS_SUSTAIN_MS}.
|
|
666
|
+
*
|
|
667
|
+
* 512 MB. The two 2026-09-10 episodes crossed it 25 and 8 minutes in; the
|
|
668
|
+
* routine reading in six hours of 2-second samples around them was 18–100 MB
|
|
669
|
+
* with single-sample spikes to 479 MB, and the process's floor after every
|
|
670
|
+
* compaction is 12–18 MB. A dead-buffer sawtooth between major GCs (measured
|
|
671
|
+
* up to 3.65 GB on 2026-08-08 when the heap had 4 GB of headroom) cannot reach
|
|
672
|
+
* it while GC runs every few seconds — and if one did, the census would SAY
|
|
673
|
+
* so: it reports the live total after its own GC next to the raw reading.
|
|
674
|
+
*/
|
|
675
|
+
var ARRAY_BUFFER_CENSUS_THRESHOLD_MB = 512;
|
|
676
|
+
/**
|
|
677
|
+
* How long the reading must stay above the threshold before a census is taken.
|
|
678
|
+
*
|
|
679
|
+
* 60 s = 30 probes at the 2 s cadence. A burst that a scavenge or the next
|
|
680
|
+
* major GC returns never pays a forced GC; an episode climbing at the slowest
|
|
681
|
+
* rate measured (20 MB/min) is still 20 MB higher by the time it is measured,
|
|
682
|
+
* which is nothing against the threshold.
|
|
683
|
+
*/
|
|
684
|
+
var ARRAY_BUFFER_CENSUS_SUSTAIN_MS = 6e4;
|
|
685
|
+
/**
|
|
686
|
+
* Minimum spacing between two censuses.
|
|
687
|
+
*
|
|
688
|
+
* 30 minutes: the second episode ran 76 minutes, so it would have produced
|
|
689
|
+
* three readings — enough to see the shape change (or not) as it grew — at a
|
|
690
|
+
* cost the router already pays six times an hour for the reclaim pass.
|
|
691
|
+
*/
|
|
692
|
+
var ARRAY_BUFFER_CENSUS_COOLDOWN_MS = 30 * 6e4;
|
|
693
|
+
/**
|
|
694
|
+
* A drop of at least this share of the threshold between two consecutive
|
|
695
|
+
* probes, from a reading that had been SUSTAINED above the threshold, is a
|
|
696
|
+
* RELEASE — the boundary both episodes crossed with no line, and the one line
|
|
697
|
+
* that lets the release be correlated with whatever else the process logged
|
|
698
|
+
* that second. Sustained, not merely above: a one-probe spike that the next
|
|
699
|
+
* scavenge sweeps (a 1.3 GB burst in 11 s was measured on 2026-08-29) is not
|
|
700
|
+
* an episode ending, and a WARN for every one of those would bury the line
|
|
701
|
+
* this exists to produce.
|
|
702
|
+
*/
|
|
703
|
+
var ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO = .5;
|
|
704
|
+
/**
|
|
705
|
+
* The pure decision: threshold, sustain, cooldown, release — testable without
|
|
706
|
+
* allocating a gigabyte or forcing a GC.
|
|
707
|
+
*/
|
|
708
|
+
function createArrayBufferCensusGate(options = {}) {
|
|
709
|
+
const thresholdMb = options.thresholdMb ?? 512;
|
|
710
|
+
const sustainMs = options.sustainMs ?? 6e4;
|
|
711
|
+
const cooldownMs = options.cooldownMs ?? 18e5;
|
|
712
|
+
let aboveSince = null;
|
|
713
|
+
let lastTakenAt = Number.NEGATIVE_INFINITY;
|
|
714
|
+
let previousMb = null;
|
|
715
|
+
let previousAt = null;
|
|
716
|
+
return { observe(arrayBuffersMb, at) {
|
|
717
|
+
const previous = previousMb;
|
|
718
|
+
const previousHeldForMs = previous !== null && previousAt !== null && aboveSince !== null ? previousAt - aboveSince : null;
|
|
719
|
+
previousMb = arrayBuffersMb;
|
|
720
|
+
previousAt = at;
|
|
721
|
+
if (previous !== null && previousHeldForMs !== null && previousHeldForMs >= sustainMs && previous - arrayBuffersMb >= thresholdMb * .5) {
|
|
722
|
+
aboveSince = null;
|
|
723
|
+
return {
|
|
724
|
+
kind: "released",
|
|
725
|
+
fromMb: previous,
|
|
726
|
+
toMb: arrayBuffersMb
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (arrayBuffersMb < thresholdMb) {
|
|
730
|
+
aboveSince = null;
|
|
731
|
+
return { kind: "hold" };
|
|
732
|
+
}
|
|
733
|
+
aboveSince ??= at;
|
|
734
|
+
const heldForMs = at - aboveSince;
|
|
735
|
+
if (heldForMs < sustainMs) return { kind: "hold" };
|
|
736
|
+
if (at - lastTakenAt < cooldownMs) return { kind: "hold" };
|
|
737
|
+
lastTakenAt = at;
|
|
738
|
+
return {
|
|
739
|
+
kind: "take",
|
|
740
|
+
reason: `arrayBuffers=${arrayBuffersMb}MB has been >=${thresholdMb}MB for ${heldForMs}ms`
|
|
741
|
+
};
|
|
742
|
+
} };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* The function evaluated INSIDE the inspector against the `queryObjects`
|
|
746
|
+
* result array (`this`). Kept as source text because that is what the
|
|
747
|
+
* protocol takes; kept small because it runs with the world stopped.
|
|
748
|
+
*/
|
|
749
|
+
var HISTOGRAM_FN = `function () {
|
|
750
|
+
var byBucket = new Map(), bySize = new Map()
|
|
751
|
+
var count = 0, bytes = 0, detached = 0
|
|
752
|
+
for (var i = 0; i < this.length; i++) {
|
|
753
|
+
var b = this[i].byteLength
|
|
754
|
+
count++
|
|
755
|
+
bytes += b
|
|
756
|
+
if (b === 0) { detached++; continue }
|
|
757
|
+
var bucket = 1
|
|
758
|
+
while (bucket < b) bucket *= 2
|
|
759
|
+
var e = byBucket.get(bucket); if (!e) { e = [0, 0]; byBucket.set(bucket, e) }
|
|
760
|
+
e[0]++; e[1] += b
|
|
761
|
+
var s = bySize.get(b); if (!s) { s = [0, 0]; bySize.set(b, s) }
|
|
762
|
+
s[0]++; s[1] += b
|
|
763
|
+
}
|
|
764
|
+
var buckets = []
|
|
765
|
+
byBucket.forEach(function (v, k) { buckets.push({ upToBytes: k, count: v[0], bytes: v[1] }) })
|
|
766
|
+
buckets.sort(function (a, b) { return b.bytes - a.bytes })
|
|
767
|
+
var topSizes = []
|
|
768
|
+
bySize.forEach(function (v, k) { topSizes.push({ byteLength: k, count: v[0], bytes: v[1] }) })
|
|
769
|
+
topSizes.sort(function (a, b) { return b.bytes - a.bytes })
|
|
770
|
+
return { count: count, bytes: bytes, detached: detached,
|
|
771
|
+
buckets: buckets.slice(0, 6), topSizes: topSizes.slice(0, 5) }
|
|
772
|
+
}`;
|
|
773
|
+
function isFiniteNumber(value) {
|
|
774
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
775
|
+
}
|
|
776
|
+
function isBucket(value) {
|
|
777
|
+
return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "upToBytes")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
|
|
778
|
+
}
|
|
779
|
+
function isExactSize(value) {
|
|
780
|
+
return typeof value === "object" && value !== null && isFiniteNumber(Reflect.get(value, "byteLength")) && isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes"));
|
|
781
|
+
}
|
|
782
|
+
/** The inspector answers `unknown`; the report is trusted only once it has this shape. */
|
|
783
|
+
function isArrayBufferCensusReport(value) {
|
|
784
|
+
if (typeof value !== "object" || value === null) return false;
|
|
785
|
+
const buckets = Reflect.get(value, "buckets");
|
|
786
|
+
const topSizes = Reflect.get(value, "topSizes");
|
|
787
|
+
return isFiniteNumber(Reflect.get(value, "count")) && isFiniteNumber(Reflect.get(value, "bytes")) && isFiniteNumber(Reflect.get(value, "detached")) && Array.isArray(buckets) && buckets.every(isBucket) && Array.isArray(topSizes) && topSizes.every(isExactSize);
|
|
788
|
+
}
|
|
789
|
+
function readObjectId(result, path) {
|
|
790
|
+
let cursor = result;
|
|
791
|
+
for (const key of path) {
|
|
792
|
+
if (typeof cursor !== "object" || cursor === null) throw new Error(`inspector reply has no ${path.join(".")}`);
|
|
793
|
+
cursor = Reflect.get(cursor, key);
|
|
794
|
+
}
|
|
795
|
+
if (typeof cursor !== "string") throw new Error(`inspector reply has no ${path.join(".")}`);
|
|
796
|
+
return cursor;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* The real census: an in-process inspector session, opened for the call and
|
|
800
|
+
* closed after it, so no session is left listening between censuses.
|
|
801
|
+
*
|
|
802
|
+
* Every remote object it creates is released before the session closes; a
|
|
803
|
+
* failure anywhere still disconnects. The session is in-process — nothing
|
|
804
|
+
* listens on a port, nothing outside this process can reach it.
|
|
805
|
+
*/
|
|
806
|
+
function createInspectorArrayBufferCensus() {
|
|
807
|
+
return async () => {
|
|
808
|
+
const session = new node_inspector_promises.Session();
|
|
809
|
+
session.connect();
|
|
810
|
+
try {
|
|
811
|
+
const prototypeObjectId = readObjectId(await session.post("Runtime.evaluate", { expression: "ArrayBuffer.prototype" }), ["result", "objectId"]);
|
|
812
|
+
try {
|
|
813
|
+
const objectId = readObjectId(await session.post("Runtime.queryObjects", { prototypeObjectId }), ["objects", "objectId"]);
|
|
814
|
+
try {
|
|
815
|
+
const evaluated = await session.post("Runtime.callFunctionOn", {
|
|
816
|
+
objectId,
|
|
817
|
+
functionDeclaration: HISTOGRAM_FN,
|
|
818
|
+
returnByValue: true
|
|
819
|
+
});
|
|
820
|
+
const value = typeof evaluated === "object" && evaluated !== null ? Reflect.get(Reflect.get(evaluated, "result") ?? {}, "value") : void 0;
|
|
821
|
+
if (!isArrayBufferCensusReport(value)) throw new Error("inspector returned a histogram of an unexpected shape");
|
|
822
|
+
return value;
|
|
823
|
+
} finally {
|
|
824
|
+
await session.post("Runtime.releaseObject", { objectId });
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
await session.post("Runtime.releaseObject", { objectId: prototypeObjectId });
|
|
828
|
+
}
|
|
829
|
+
} finally {
|
|
830
|
+
session.disconnect();
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
var MB$1 = 1048576;
|
|
835
|
+
function mb(bytes) {
|
|
836
|
+
return Math.round(bytes / MB$1);
|
|
837
|
+
}
|
|
838
|
+
function formatBucket(bucket) {
|
|
839
|
+
return `<=${bucket.upToBytes >= MB$1 ? `${Math.round(bucket.upToBytes / MB$1)}MB` : bucket.upToBytes >= 1024 ? `${Math.round(bucket.upToBytes / 1024)}KB` : `${bucket.upToBytes}B`}:${bucket.count}x/${mb(bucket.bytes)}MB`;
|
|
840
|
+
}
|
|
841
|
+
function formatExact(size) {
|
|
842
|
+
return `${size.byteLength}Bx${size.count}=${mb(size.bytes)}MB`;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The census line. `live` is what survived the census's own GC; `raw` is the
|
|
846
|
+
* reading that triggered it — when they disagree by a lot, the growth was dead
|
|
847
|
+
* buffers awaiting a sweep, not a retention, and the line says which.
|
|
848
|
+
*/
|
|
849
|
+
function formatArrayBufferCensus(label, report, context) {
|
|
850
|
+
const live = mb(report.bytes);
|
|
851
|
+
const verdict = live < context.rawMb * .5 ? "mostly DEAD buffers awaiting a sweep, not a retention" : "LIVE — something holds them";
|
|
852
|
+
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}`;
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
609
855
|
//#region src/kernel/heap-watch.ts
|
|
610
856
|
/**
|
|
611
857
|
* heap-watch — hub-main's own memory, on the record, and returned to the OS.
|
|
@@ -1291,8 +1537,35 @@ function startHeapWatch(label = HUB_MAIN_HEAP_WATCH_LABEL, sink = consoleSink, i
|
|
|
1291
1537
|
if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
|
|
1292
1538
|
checkRssBudget(sample, at);
|
|
1293
1539
|
maybeReclaim(sample);
|
|
1540
|
+
judgeArrayBuffers(sample, at);
|
|
1294
1541
|
} catch {}
|
|
1295
1542
|
};
|
|
1543
|
+
const censusProbe = probes?.arrayBufferCensus;
|
|
1544
|
+
const censusGate = censusProbe === void 0 ? void 0 : createArrayBufferCensusGate(censusProbe);
|
|
1545
|
+
let censusInFlight = false;
|
|
1546
|
+
const judgeArrayBuffers = (sample, at) => {
|
|
1547
|
+
if (censusProbe === void 0 || censusGate === void 0) return;
|
|
1548
|
+
const verdict = censusGate.observe(sample.arrayBuffersMb, at);
|
|
1549
|
+
if (verdict.kind === "released") {
|
|
1550
|
+
sink.warn(`[mem] ${label} arrayBuffers RELEASED ${verdict.fromMb}MB→${verdict.toMb}MB between two probes ${probeIntervalMs}ms apart — no owner logged this release; whatever held them let go in one instant (a socket that died, a queue that drained, a map that was cleared). Correlate with every line of this process in that window.`);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (verdict.kind !== "take" || censusInFlight) return;
|
|
1554
|
+
censusInFlight = true;
|
|
1555
|
+
const startedAt = now();
|
|
1556
|
+
const rawMb = sample.arrayBuffersMb;
|
|
1557
|
+
censusProbe.census().then((report) => {
|
|
1558
|
+
sink.warn(formatArrayBufferCensus(label, report, {
|
|
1559
|
+
rawMb,
|
|
1560
|
+
tookMs: now() - startedAt,
|
|
1561
|
+
reason: verdict.reason
|
|
1562
|
+
}));
|
|
1563
|
+
}).catch((error) => {
|
|
1564
|
+
sink.warn(`[mem] ${label} arrayBuffer census FAILED after ${now() - startedAt}ms (trigger: ${verdict.reason}) — ${error instanceof Error ? error.message : String(error)}`);
|
|
1565
|
+
}).finally(() => {
|
|
1566
|
+
censusInFlight = false;
|
|
1567
|
+
});
|
|
1568
|
+
};
|
|
1296
1569
|
const timer = setInterval(tick, probeIntervalMs);
|
|
1297
1570
|
timer.unref?.();
|
|
1298
1571
|
tick();
|
|
@@ -6871,7 +7144,8 @@ var LocalChildRegistry = class {
|
|
|
6871
7144
|
this.registeredHandler({
|
|
6872
7145
|
childId: msg.childId,
|
|
6873
7146
|
caps: msg.caps,
|
|
6874
|
-
incarnation
|
|
7147
|
+
incarnation,
|
|
7148
|
+
...msg.customActions !== void 0 ? { customActions: msg.customActions } : {}
|
|
6875
7149
|
});
|
|
6876
7150
|
return { ok: true };
|
|
6877
7151
|
}
|
|
@@ -6973,6 +7247,15 @@ var LocalChildClient = class {
|
|
|
6973
7247
|
* subscriptions do), the register carries the real union — even if empty.
|
|
6974
7248
|
*/
|
|
6975
7249
|
hasDeclaredEventPatterns = false;
|
|
7250
|
+
/**
|
|
7251
|
+
* The custom-action catalogs of every hosted addon, once the runner's init
|
|
7252
|
+
* loop has produced them (`updateCaps(caps, catalogs)` at post-init). `null`
|
|
7253
|
+
* until then, and OMITTED from the register frame while null: the parent
|
|
7254
|
+
* reads absence as "not described yet", and a pre-init register must never
|
|
7255
|
+
* look like "this child declares no actions". Kept on the client so every
|
|
7256
|
+
* later re-register (native-cap change, reconnect) re-carries it unchanged.
|
|
7257
|
+
*/
|
|
7258
|
+
latestCustomActions = null;
|
|
6976
7259
|
/** Events and logs queued while the channel is not yet open. */
|
|
6977
7260
|
pendingEmits = [];
|
|
6978
7261
|
/** Handler for parent→child events. Registered via `onEvent`. */
|
|
@@ -7112,12 +7395,7 @@ var LocalChildClient = class {
|
|
|
7112
7395
|
}
|
|
7113
7396
|
if (msg.kind === "set-log-level") this.setLogLevelHandler?.(msg.level);
|
|
7114
7397
|
});
|
|
7115
|
-
const register =
|
|
7116
|
-
kind: "register",
|
|
7117
|
-
childId: this.options.childId,
|
|
7118
|
-
caps: this.latestCaps,
|
|
7119
|
-
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
7120
|
-
};
|
|
7398
|
+
const register = this.registerFrame(this.latestCaps);
|
|
7121
7399
|
try {
|
|
7122
7400
|
await channel.request(register);
|
|
7123
7401
|
} catch (err) {
|
|
@@ -7143,16 +7421,26 @@ var LocalChildClient = class {
|
|
|
7143
7421
|
* it (device-restore can race ahead of the UDS connect). After `start()`,
|
|
7144
7422
|
* the set is sent immediately.
|
|
7145
7423
|
*/
|
|
7146
|
-
async updateCaps(caps) {
|
|
7424
|
+
async updateCaps(caps, customActions) {
|
|
7147
7425
|
this.latestCaps = caps;
|
|
7426
|
+
if (customActions !== void 0) this.latestCustomActions = customActions;
|
|
7148
7427
|
if (this.channel === null) return;
|
|
7149
|
-
|
|
7428
|
+
await this.channel.request(this.registerFrame(caps));
|
|
7429
|
+
}
|
|
7430
|
+
/**
|
|
7431
|
+
* The register frame: caps, plus everything a re-register must atomically
|
|
7432
|
+
* re-carry so no separate re-sync can be missed — the subscription union
|
|
7433
|
+
* (omitted if nothing ever declared, which keeps the fail-open shape) and the
|
|
7434
|
+
* custom-action catalogs (omitted until the init loop produced them).
|
|
7435
|
+
*/
|
|
7436
|
+
registerFrame(caps) {
|
|
7437
|
+
return {
|
|
7150
7438
|
kind: "register",
|
|
7151
7439
|
childId: this.options.childId,
|
|
7152
7440
|
caps,
|
|
7153
|
-
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {}
|
|
7441
|
+
...this.hasDeclaredEventPatterns ? { eventPatterns: this.latestEventPatterns } : {},
|
|
7442
|
+
...this.latestCustomActions !== null ? { customActions: this.latestCustomActions } : {}
|
|
7154
7443
|
};
|
|
7155
|
-
await this.channel.request(register);
|
|
7156
7444
|
}
|
|
7157
7445
|
/**
|
|
7158
7446
|
* Fire-and-forget: send a system event to the parent for forwarding to the
|
|
@@ -9901,6 +10189,30 @@ Object.defineProperty(exports, "AGENT_CAP_FWD_SERVICE", {
|
|
|
9901
10189
|
return AGENT_CAP_FWD_SERVICE;
|
|
9902
10190
|
}
|
|
9903
10191
|
});
|
|
10192
|
+
Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_COOLDOWN_MS", {
|
|
10193
|
+
enumerable: true,
|
|
10194
|
+
get: function() {
|
|
10195
|
+
return ARRAY_BUFFER_CENSUS_COOLDOWN_MS;
|
|
10196
|
+
}
|
|
10197
|
+
});
|
|
10198
|
+
Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_SUSTAIN_MS", {
|
|
10199
|
+
enumerable: true,
|
|
10200
|
+
get: function() {
|
|
10201
|
+
return ARRAY_BUFFER_CENSUS_SUSTAIN_MS;
|
|
10202
|
+
}
|
|
10203
|
+
});
|
|
10204
|
+
Object.defineProperty(exports, "ARRAY_BUFFER_CENSUS_THRESHOLD_MB", {
|
|
10205
|
+
enumerable: true,
|
|
10206
|
+
get: function() {
|
|
10207
|
+
return ARRAY_BUFFER_CENSUS_THRESHOLD_MB;
|
|
10208
|
+
}
|
|
10209
|
+
});
|
|
10210
|
+
Object.defineProperty(exports, "ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO", {
|
|
10211
|
+
enumerable: true,
|
|
10212
|
+
get: function() {
|
|
10213
|
+
return ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO;
|
|
10214
|
+
}
|
|
10215
|
+
});
|
|
9904
10216
|
Object.defineProperty(exports, "AddonDepsManager", {
|
|
9905
10217
|
enumerable: true,
|
|
9906
10218
|
get: function() {
|
|
@@ -10231,6 +10543,12 @@ Object.defineProperty(exports, "createAddonService", {
|
|
|
10231
10543
|
return createAddonService;
|
|
10232
10544
|
}
|
|
10233
10545
|
});
|
|
10546
|
+
Object.defineProperty(exports, "createArrayBufferCensusGate", {
|
|
10547
|
+
enumerable: true,
|
|
10548
|
+
get: function() {
|
|
10549
|
+
return createArrayBufferCensusGate;
|
|
10550
|
+
}
|
|
10551
|
+
});
|
|
10234
10552
|
Object.defineProperty(exports, "createBrokerDeviceManagerApi", {
|
|
10235
10553
|
enumerable: true,
|
|
10236
10554
|
get: function() {
|
|
@@ -10255,6 +10573,12 @@ Object.defineProperty(exports, "createEventPlaneReader", {
|
|
|
10255
10573
|
return createEventPlaneReader;
|
|
10256
10574
|
}
|
|
10257
10575
|
});
|
|
10576
|
+
Object.defineProperty(exports, "createInspectorArrayBufferCensus", {
|
|
10577
|
+
enumerable: true,
|
|
10578
|
+
get: function() {
|
|
10579
|
+
return createInspectorArrayBufferCensus;
|
|
10580
|
+
}
|
|
10581
|
+
});
|
|
10258
10582
|
Object.defineProperty(exports, "createKernelHwAccel", {
|
|
10259
10583
|
enumerable: true,
|
|
10260
10584
|
get: function() {
|
|
@@ -10369,6 +10693,12 @@ Object.defineProperty(exports, "encodeFrame", {
|
|
|
10369
10693
|
return encodeFrame;
|
|
10370
10694
|
}
|
|
10371
10695
|
});
|
|
10696
|
+
Object.defineProperty(exports, "formatArrayBufferCensus", {
|
|
10697
|
+
enumerable: true,
|
|
10698
|
+
get: function() {
|
|
10699
|
+
return formatArrayBufferCensus;
|
|
10700
|
+
}
|
|
10701
|
+
});
|
|
10372
10702
|
Object.defineProperty(exports, "formatEventPlane", {
|
|
10373
10703
|
enumerable: true,
|
|
10374
10704
|
get: function() {
|
|
@@ -10471,6 +10801,12 @@ Object.defineProperty(exports, "ipcParentLink", {
|
|
|
10471
10801
|
return ipcParentLink;
|
|
10472
10802
|
}
|
|
10473
10803
|
});
|
|
10804
|
+
Object.defineProperty(exports, "isArrayBufferCensusReport", {
|
|
10805
|
+
enumerable: true,
|
|
10806
|
+
get: function() {
|
|
10807
|
+
return isArrayBufferCensusReport;
|
|
10808
|
+
}
|
|
10809
|
+
});
|
|
10474
10810
|
Object.defineProperty(exports, "localEndpointPath", {
|
|
10475
10811
|
enumerable: true,
|
|
10476
10812
|
get: function() {
|