@camstack/system 1.2.102 → 1.2.104
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 +15 -1
- package/dist/addon-runner.mjs +15 -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 +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- 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 +150 -22
- package/dist/builtins/snapshot/index.mjs +150 -22
- package/dist/builtins/snapshot/snapshot-cache.d.ts +63 -0
- package/dist/builtins/snapshot/snapshot-resize.d.ts +33 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts +6 -0
- 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 +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- 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/{dist-3wh5LCad.mjs → dist-CAzHQ03i.mjs} +144 -21
- package/dist/{dist-9sC0wZw6.js → dist-DR1VmGz6.js} +144 -21
- package/dist/index.d.ts +2 -2
- package/dist/index.js +243 -328
- package/dist/index.mjs +233 -319
- package/dist/kernel/config-schema.d.ts +14 -10
- package/dist/kernel/heap-watch.d.ts +106 -7
- package/dist/kernel/moleculer/process-service.d.ts +34 -0
- package/dist/logging/log-manager.d.ts +7 -5
- package/dist/logging/partitioned-log-buffer.d.ts +95 -16
- package/dist/{manifest-python-deps-Bt_J04b0.mjs → manifest-python-deps-5ei2xVUh.mjs} +355 -1
- package/dist/{manifest-python-deps-BtVjvfvj.js → manifest-python-deps-GejnH--L.js} +422 -0
- package/package.json +1 -1
|
@@ -85,18 +85,22 @@ export type AppConfig = BootstrapConfig & {
|
|
|
85
85
|
*/
|
|
86
86
|
perAddonLogBufferSize?: number;
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
89
|
-
* default) means the per-addon rings are the only bound — the historical
|
|
90
|
-
* behaviour, unchanged unless you set this.
|
|
88
|
+
* Ceiling on retained entries across every addon bucket.
|
|
91
89
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* UNSET (the normal case) takes `DEFAULT_MAX_TOTAL_LOG_ENTRIES` — the buffer is
|
|
91
|
+
* bounded in aggregate out of the box. It has to be: `perAddonLogBufferSize ×
|
|
92
|
+
* addons` grows with the roster, and hub-main ingests from every local runner
|
|
93
|
+
* AND every agent, so the measured ~60-bucket hub retained ~300k entries
|
|
94
|
+
* (~92MB at ~306 B/entry) with no aggregate bound at all.
|
|
94
95
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* timings.
|
|
96
|
+
* Set a number to tune it. Set an explicit `null` to opt out entirely — the
|
|
97
|
+
* per-addon rings then become the only bound again.
|
|
98
|
+
*
|
|
99
|
+
* It is met by discarding entries at or below `logBufferPruneLevel` FIRST, so a
|
|
100
|
+
* live addon's `warn`/`error` are never traded for lines about frame timings.
|
|
101
|
+
* Only when that cannot reach the target are whole least-recently-WRITTEN
|
|
102
|
+
* buckets dropped — a bound that pruning alone cannot hold is not a bound, and
|
|
103
|
+
* the bucket nobody has written to is the cheapest thing in the buffer to lose.
|
|
100
104
|
*/
|
|
101
105
|
maxTotalLogBufferSize?: number | null;
|
|
102
106
|
/**
|
|
@@ -59,14 +59,46 @@ export declare function nextMode(current: HeapWatchMode, usedRatio: number, esca
|
|
|
59
59
|
* Live calibration, both readings from the same hub minutes apart: 2491MB
|
|
60
60
|
* stranded before the reclaim, 438MB after. 438MB is this process's irreducible
|
|
61
61
|
* native floor (sqlite pages, libvips, sharp, thread stacks, node's own text) —
|
|
62
|
-
* collecting against it would buy nothing and cost a stall every minute.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* collecting against it would buy nothing and cost a stall every minute.
|
|
63
|
+
*
|
|
64
|
+
* ## Why this came down from 1536 (2026-08-17)
|
|
65
|
+
*
|
|
66
|
+
* The trigger was originally set to fire only on an unmistakable strand. It
|
|
67
|
+
* does, and the pass on the record works exactly as designed:
|
|
68
|
+
*
|
|
69
|
+
* [mem] reclaim hub-main stranded=2365MB rss=4016MB→2368MB freed=1648MB took=654ms
|
|
70
|
+
*
|
|
71
|
+
* But the number it was protecting against changed. The host now runs with a
|
|
72
|
+
* 24GiB cgroup ceiling and **no swap**, and is OOM-killing a neighbouring VM.
|
|
73
|
+
* RSS this process is holding and not using is no longer merely untidy — it is
|
|
74
|
+
* the headroom something else needed, and the OOM is triggered by the PEAK, not
|
|
75
|
+
* by the average. A trigger of 1536 means the peak is allowed to stand 1.5GiB
|
|
76
|
+
* above the floor before anything happens.
|
|
77
|
+
*
|
|
78
|
+
* 1024 is 2.3x the measured 438MB floor, so ordinary movement around the floor
|
|
79
|
+
* can never trip it, and every pass that does fire still returns roughly
|
|
80
|
+
* `1024 - 438` ≈ **580MB minimum** — a real return for a 250-650ms stall,
|
|
81
|
+
* not a stall spent collecting noise.
|
|
65
82
|
*/
|
|
66
|
-
export declare const HEAP_RECLAIM_TRIGGER_MB =
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
83
|
+
export declare const HEAP_RECLAIM_TRIGGER_MB = 1024;
|
|
84
|
+
/**
|
|
85
|
+
* Floor on how often the stop-the-world may run.
|
|
86
|
+
*
|
|
87
|
+
* Ten minutes was chosen to keep the worst case under 0.1% of the router's
|
|
88
|
+
* time. The worst case is not what governs the cost, though: the reclaim only
|
|
89
|
+
* runs when {@link shouldReclaim} says the process is genuinely stranded, and
|
|
90
|
+
* after a pass it is at the ~438MB floor. Live off-heap growth was measured at
|
|
91
|
+
* roughly 3GB/2h ≈ 25MB/min, so re-stranding the ~580MB needed to qualify again
|
|
92
|
+
* takes on the order of **20 minutes** — well outside any floor considered
|
|
93
|
+
* here. In steady state this constant therefore costs nothing at all.
|
|
94
|
+
*
|
|
95
|
+
* What it does govern is a BURST, which is the only case that matters on a host
|
|
96
|
+
* with no swap: the ten-minute floor let a peak that re-formed in seconds stand
|
|
97
|
+
* for ten minutes. Two minutes bounds that, and even the pathological case of a
|
|
98
|
+
* process that re-strands instantly costs 654ms per 120s — 0.55% of the
|
|
99
|
+
* router's time, against a ~1.5GiB peak it removes each pass.
|
|
100
|
+
*/
|
|
101
|
+
export declare const HEAP_RECLAIM_MIN_INTERVAL_MS = 120000;
|
|
70
102
|
export interface HeapSample {
|
|
71
103
|
readonly rssMb: number;
|
|
72
104
|
readonly heapUsedMb: number;
|
|
@@ -136,3 +168,70 @@ export interface HeapReclaimOptions {
|
|
|
136
168
|
* a diagnostic that can crash the process it watches is worse than none.
|
|
137
169
|
*/
|
|
138
170
|
export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions): () => void;
|
|
171
|
+
/**
|
|
172
|
+
* Steady heartbeat period for a HEAVY addon runner.
|
|
173
|
+
*
|
|
174
|
+
* hub-main logs once a minute because it is ONE process. The heavy runners are
|
|
175
|
+
* ~7 per node sharing the same hub log stream; at 60s each they would put 7
|
|
176
|
+
* routine lines a minute in it forever, which is how a log stops being read.
|
|
177
|
+
* Five minutes keeps the per-runner record (a leak is still a visible climb
|
|
178
|
+
* across samples) at ~1.4 lines/min across the class.
|
|
179
|
+
*
|
|
180
|
+
* This slows LOGGING only. The probe keeps running at
|
|
181
|
+
* {@link HEAP_WATCH_FAST_INTERVAL_MS} (probe and log cadences are separate —
|
|
182
|
+
* see the comment inside {@link startHeapWatch}), so the reclaim gate and the
|
|
183
|
+
* escalated burst-sampling react exactly as fast as hub-main's.
|
|
184
|
+
*/
|
|
185
|
+
export declare const RUNNER_HEAP_WATCH_INTERVAL_MS = 300000;
|
|
186
|
+
/** Wiring for {@link startRunnerHeapWatch}. */
|
|
187
|
+
export interface RunnerHeapWatchOptions {
|
|
188
|
+
/**
|
|
189
|
+
* `CAMSTACK_RUNNER_HEAP_PROFILE` as the parent passed it. Anything but
|
|
190
|
+
* `'heavy'` (including unset — the un-annotated default is light) starts
|
|
191
|
+
* nothing: light runners are small, already carry V8 heap flags, and a
|
|
192
|
+
* quiet runner must never pay a stop-the-world.
|
|
193
|
+
*/
|
|
194
|
+
readonly heapProfile: string | undefined;
|
|
195
|
+
/** Log label — the runner's nodeId, so `[mem]` lines group per runner. */
|
|
196
|
+
readonly label: string;
|
|
197
|
+
readonly sink: HeapWatchSink;
|
|
198
|
+
/**
|
|
199
|
+
* `CAMSTACK_RUNNER_HEAP_RECLAIM` as the environment carries it. `'off'`
|
|
200
|
+
* keeps the heartbeat and disables only the reclaim pass — the operator
|
|
201
|
+
* kill-switch for the media path, usable without a rebuild.
|
|
202
|
+
*/
|
|
203
|
+
readonly reclaimSwitch?: string;
|
|
204
|
+
/** Heartbeat period override. Defaults to {@link RUNNER_HEAP_WATCH_INTERVAL_MS}. */
|
|
205
|
+
readonly intervalMs?: number;
|
|
206
|
+
/** Test injection. Production omits it and gets {@link createV8Reclaimer}. */
|
|
207
|
+
readonly reclaimOptions?: HeapReclaimOptions;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Start the heap watch + stranded-page reclaimer for ONE addon runner, iff it
|
|
211
|
+
* is a heavy one. Returns the stop function, or undefined when nothing started.
|
|
212
|
+
*
|
|
213
|
+
* ## Why heavy runners get hub-main's mechanism
|
|
214
|
+
*
|
|
215
|
+
* The media-path runners deliberately run with NO old-space ceiling
|
|
216
|
+
* (`maxOldSpaceMb: 0` — a flat cap turned a disk-stall backlog into
|
|
217
|
+
* `Ineffective mark-compacts near heap limit` and killed recording, twice on
|
|
218
|
+
* 2026-08-17). Uncapped, V8 feels no pressure, so a one-off burst becomes the
|
|
219
|
+
* runner's permanent RSS: the recorder measured 2122MB holding a ~50MB index.
|
|
220
|
+
* hub-main had exactly this shape and its reclaimer is proven live — 11 passes
|
|
221
|
+
* in 4h, 375–1652MB returned per pass at 550–680ms each. Same mechanism, same
|
|
222
|
+
* `shouldReclaim` stranded-over-trigger gate; never a second implementation.
|
|
223
|
+
*
|
|
224
|
+
* ## The stall, priced for the runners that get it
|
|
225
|
+
*
|
|
226
|
+
* A pass is a sub-second stop-the-world at most once per
|
|
227
|
+
* {@link HEAP_RECLAIM_MIN_INTERVAL_MS}, and only while >1GiB is stranded. For
|
|
228
|
+
* the recorder — finalizing a segment every ~300ms across 34 profile-writers —
|
|
229
|
+
* the pause delays WATCHER work, not video: ffmpeg children keep writing to
|
|
230
|
+
* disk independently, the playlist tail is the durable queue, and the next
|
|
231
|
+
* tick drains what accrued. For stream-broker a pass can hitch live frames
|
|
232
|
+
* once per qualifying window; after a pass the runner sits at its floor and
|
|
233
|
+
* re-stranding the trigger takes tens of minutes, so in practice the class
|
|
234
|
+
* pays a few passes per hour against the multi-GiB peak that was OOM-killing
|
|
235
|
+
* a neighbouring VM on a host with no swap.
|
|
236
|
+
*/
|
|
237
|
+
export declare function startRunnerHeapWatch(options: RunnerHeapWatchOptions): (() => void) | undefined;
|
|
@@ -5,6 +5,40 @@ interface RunnerAddonSpec {
|
|
|
5
5
|
readonly addonId: string;
|
|
6
6
|
readonly addonDir: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Default old-space ceiling for a HEAVY runner, in MB.
|
|
10
|
+
*
|
|
11
|
+
* ## Why a heavy runner needs a ceiling at all
|
|
12
|
+
*
|
|
13
|
+
* `heapProfile: 'heavy'` used to mean "V8 defaults", i.e. no `--max-old-space-size` — which on this
|
|
14
|
+
* host is V8's own memory-derived default (multiple GB). Nothing then bounds a burst, and V8 does
|
|
15
|
+
* not hand committed pages back (the long argument for that is in `kernel/heap-watch.ts`), so a
|
|
16
|
+
* one-off burst becomes the runner's permanent RSS.
|
|
17
|
+
*
|
|
18
|
+
* Measured on the live hub, 2026-08-17: the `recorder` runner reaches **2122 MB RSS** and stays
|
|
19
|
+
* there after the deferred archive walk (99,809 segments in 84s), while the `RecordingIndex` it
|
|
20
|
+
* actually retains is arithmetically ~21 MB (a `SegmentRow` is 5 numbers + 3 strings ≈ 207 B). The
|
|
21
|
+
* same addon on two agent nodes carrying the same code and a smaller archive sits at **58 MB**.
|
|
22
|
+
* The ~2 GB is walk garbage held as high-water in a heap that had no reason to collect it.
|
|
23
|
+
*
|
|
24
|
+
* ## Why 1024
|
|
25
|
+
*
|
|
26
|
+
* From the same live topology snapshot, every heavy runner's FULL RSS — which is a strict
|
|
27
|
+
* over-estimate of its old space, since it also contains native allocations, `external` and
|
|
28
|
+
* `arrayBuffers`, none of which `--max-old-space-size` bounds:
|
|
29
|
+
*
|
|
30
|
+
* detection 471 MB · provider-reolink 507 MB · pipeline-orchestrator 238 MB · ml 218 MB ·
|
|
31
|
+
* snapshot 185 MB · decoder-nodeav 174 MB · audio-analyzer 170 MB
|
|
32
|
+
*
|
|
33
|
+
* 1024 MB clears the largest of those by ~2x while cutting the recorder's observed ratchet by
|
|
34
|
+
* more than half. It is a CEILING, not a target: a runner that fits today keeps behaving exactly
|
|
35
|
+
* as it does now, because V8 only collects harder as it approaches the limit.
|
|
36
|
+
*
|
|
37
|
+
* A runner whose working set genuinely does not fit declares its own number in its manifest
|
|
38
|
+
* (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
|
|
39
|
+
* `stream-broker` (1323 MB measured, frame buffers) is the one shipped addon that does.
|
|
40
|
+
*/
|
|
41
|
+
export declare const HEAVY_MAX_OLD_MB_DEFAULT = 1024;
|
|
8
42
|
interface SpawnedProcess {
|
|
9
43
|
readonly pid: number;
|
|
10
44
|
readonly nodeId: string;
|
|
@@ -54,11 +54,13 @@ export declare class LogManager {
|
|
|
54
54
|
* `perAddonCapacity` bounds EACH addon's bucket, not the total — a chatty addon
|
|
55
55
|
* evicts only its own lines, so quiet addons keep their sparse history.
|
|
56
56
|
*
|
|
57
|
-
* `options.maxTotalEntries`
|
|
58
|
-
* per-bucket bound scales with addon count
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
57
|
+
* `options.maxTotalEntries` ceilings the total across all buckets, because the
|
|
58
|
+
* per-bucket bound scales with addon count, and `options.maxBuckets` ceilings the
|
|
59
|
+
* bucket count itself. When the total is exceeded, entries at or below
|
|
60
|
+
* `options.pruneLevel` (default `debug`) are discarded first — a live addon's
|
|
61
|
+
* `warn`/`error` are never evicted to satisfy it — and only if that cannot reach
|
|
62
|
+
* the target are whole least-recently-written buckets dropped. Both bounds are ON
|
|
63
|
+
* by default; pass an explicit `null` to opt out. See {@link PartitionedLogBuffer}.
|
|
62
64
|
*/
|
|
63
65
|
constructor(perAddonCapacity?: number, options?: PartitionedLogBufferOptions);
|
|
64
66
|
/** Entries currently retained across every bucket. */
|
|
@@ -1,14 +1,52 @@
|
|
|
1
1
|
import { LogEntry, LogFilter, LogLevel } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* Default ceiling on retained entries across every bucket.
|
|
4
|
+
*
|
|
5
|
+
* This used to be `null` — no aggregate bound at all — and the only thing
|
|
6
|
+
* holding hub-main's log memory was `perAddonCapacity x buckets`, a product
|
|
7
|
+
* that grows with the roster. Measured on the live cluster: 37 forked runners
|
|
8
|
+
* on the hub plus its in-process builtins, and hub-main additionally ingests
|
|
9
|
+
* every line from both agents, so ~60 buckets is the ordinary case and
|
|
10
|
+
* 60 x 5000 = 300k retained entries.
|
|
11
|
+
*
|
|
12
|
+
* A realistic entry (timestamp, level, scope, ~70-char message, three tags, no
|
|
13
|
+
* `meta`) measures **~306 B retained** in V8. So the old unbounded case is
|
|
14
|
+
* ~92MB of a hub process that is being asked to fit inside a cgroup with a
|
|
15
|
+
* neighbour to protect, and 50_000 is **~15MB**.
|
|
16
|
+
*
|
|
17
|
+
* 50_000 across a ~60-bucket roster is ~830 entries per addon on average while
|
|
18
|
+
* still letting a single chatty addon hold its full 5000-entry ring — the
|
|
19
|
+
* partitioning is not weakened, only the total is held.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DEFAULT_MAX_TOTAL_LOG_ENTRIES = 50000;
|
|
22
|
+
/**
|
|
23
|
+
* Default ceiling on the NUMBER of buckets.
|
|
24
|
+
*
|
|
25
|
+
* Buckets were created on first write and never removed, so every addon id
|
|
26
|
+
* that ever logged — a removed addon, a renamed one, a one-off worker id —
|
|
27
|
+
* kept a ring alive for the life of the process. {@link DEFAULT_MAX_TOTAL_LOG_ENTRIES}
|
|
28
|
+
* alone does not fix that: the total sweep may only discard entries at or below
|
|
29
|
+
* `pruneLevel`, so a dead addon's 5000 `info` lines are untouchable by it.
|
|
30
|
+
*
|
|
31
|
+
* 128 is ~2x the ~60-bucket live roster, so no real deployment ever evicts on
|
|
32
|
+
* bucket count alone; it exists to make an unbounded id space impossible.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_MAX_LOG_BUCKETS = 128;
|
|
2
35
|
export interface PartitionedLogBufferOptions {
|
|
3
36
|
/** Hard ring size for EACH addon's bucket. */
|
|
4
37
|
readonly perAddonCapacity?: number;
|
|
5
38
|
/**
|
|
6
|
-
* Soft ceiling on entries across all buckets. `null`
|
|
7
|
-
*
|
|
39
|
+
* Soft ceiling on entries across all buckets. `null` means the only bound is
|
|
40
|
+
* the per-addon rings. Defaults to {@link DEFAULT_MAX_TOTAL_LOG_ENTRIES}.
|
|
8
41
|
*/
|
|
9
42
|
readonly maxTotalEntries?: number | null;
|
|
10
43
|
/** Highest severity the total-cap sweep may discard. Default `debug`. */
|
|
11
44
|
readonly pruneLevel?: LogLevel;
|
|
45
|
+
/**
|
|
46
|
+
* Hard ceiling on the number of buckets. `null` means unbounded. Defaults to
|
|
47
|
+
* {@link DEFAULT_MAX_LOG_BUCKETS}.
|
|
48
|
+
*/
|
|
49
|
+
readonly maxBuckets?: number | null;
|
|
12
50
|
}
|
|
13
51
|
/**
|
|
14
52
|
* A log buffer partitioned into one fixed-capacity {@link LogRingBuffer} per
|
|
@@ -19,32 +57,48 @@ export interface PartitionedLogBufferOptions {
|
|
|
19
57
|
* Drop-in for {@link LogRingBuffer} from {@link LogManager}'s perspective —
|
|
20
58
|
* exposes the same `push` / `getAll` / `query` / `clear` surface.
|
|
21
59
|
*
|
|
22
|
-
* ##
|
|
60
|
+
* ## Three bounds, doing different jobs
|
|
23
61
|
*
|
|
24
62
|
* **`perAddonCapacity`** is a HARD bound: each bucket is a fixed-size ring, so
|
|
25
63
|
* total memory can never exceed `perAddonCapacity × buckets` whatever else
|
|
26
64
|
* happens. This is the safety net.
|
|
27
65
|
*
|
|
28
|
-
* **`maxTotalEntries`**
|
|
29
|
-
* the hard bound scales with addon count —
|
|
30
|
-
* entries on
|
|
31
|
-
* entries at or below `pruneLevel` (default `debug`), oldest first, from the
|
|
66
|
+
* **`maxTotalEntries`** targets the total across all buckets, and it exists because
|
|
67
|
+
* the hard bound scales with addon count — 60 buckets × 5000 was 300k retained
|
|
68
|
+
* entries on the live hub. When the total is exceeded, the sweep first discards
|
|
69
|
+
* only entries at or below `pruneLevel` (default `debug`), oldest first, from the
|
|
32
70
|
* largest buckets first.
|
|
33
71
|
*
|
|
34
|
-
* It is deliberately soft. Meeting a total cap by discarding
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* that can evict an error is not a feature.
|
|
72
|
+
* It is deliberately soft ON A LIVE BUCKET. Meeting a total cap by discarding
|
|
73
|
+
* `warn` and `error` from an addon that is still logging would trade the entries
|
|
74
|
+
* that explain an incident for entries about frame timings. A cap that can evict
|
|
75
|
+
* a live addon's error is not a feature.
|
|
39
76
|
*
|
|
40
77
|
* Largest-bucket-first because the noisy addon is the one creating the pressure —
|
|
41
78
|
* the same principle that motivated partitioning in the first place.
|
|
79
|
+
*
|
|
80
|
+
* **`maxBuckets`, and bucket eviction, are the hard backstop.** Pruning alone
|
|
81
|
+
* cannot bound this structure: it may only discard at or below `pruneLevel`, so
|
|
82
|
+
* a bucket full of `info` from an addon that no longer exists was immortal, and
|
|
83
|
+
* the roster the product scales with is not fixed. When pruning cannot reach the
|
|
84
|
+
* target — or the bucket count exceeds `maxBuckets` — whole buckets are dropped
|
|
85
|
+
* **least-recently-WRITTEN first**, and never the bucket currently being written
|
|
86
|
+
* to. That is precisely the dead addon's bucket, and it is precisely not the
|
|
87
|
+
* bucket of the addon whose incident is in progress.
|
|
42
88
|
*/
|
|
43
89
|
export declare class PartitionedLogBuffer {
|
|
44
90
|
private readonly buffers;
|
|
91
|
+
/**
|
|
92
|
+
* Bucket key → the write ordinal of its last `push`. A monotonic counter, not
|
|
93
|
+
* a clock: eviction order must follow the order writes actually happened, and
|
|
94
|
+
* a wall clock that steps backwards would pick the wrong victim.
|
|
95
|
+
*/
|
|
96
|
+
private readonly lastWriteSeq;
|
|
97
|
+
private writeSeq;
|
|
45
98
|
private perAddonCapacity;
|
|
46
99
|
private maxTotalEntries;
|
|
47
100
|
private pruneLevel;
|
|
101
|
+
private maxBuckets;
|
|
48
102
|
/** Running total, so `push` does not walk every bucket to decide. */
|
|
49
103
|
private totalEntries;
|
|
50
104
|
constructor(perAddonCapacity?: number, options?: PartitionedLogBufferOptions);
|
|
@@ -59,18 +113,40 @@ export declare class PartitionedLogBuffer {
|
|
|
59
113
|
readonly perAddonCapacity?: number;
|
|
60
114
|
readonly maxTotalEntries?: number | null;
|
|
61
115
|
readonly pruneLevel?: LogLevel;
|
|
116
|
+
readonly maxBuckets?: number | null;
|
|
62
117
|
}): void;
|
|
63
118
|
private bufferFor;
|
|
64
119
|
push(entry: LogEntry): void;
|
|
65
120
|
/**
|
|
66
|
-
* Bring the total down to `PRUNE_TARGET_RATIO × cap
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
121
|
+
* Bring the total down to `PRUNE_TARGET_RATIO × cap`.
|
|
122
|
+
*
|
|
123
|
+
* Two passes, in this order and no other: first discard prunable entries,
|
|
124
|
+
* largest bucket first, which is free of information cost. Only if that
|
|
125
|
+
* cannot reach the target does the second pass drop WHOLE buckets,
|
|
126
|
+
* least-recently-written first — the bound has to hold even when every
|
|
127
|
+
* retained entry is above `pruneLevel`, and a bucket nobody has written to is
|
|
128
|
+
* the cheapest thing in the structure to lose.
|
|
70
129
|
*/
|
|
71
130
|
private enforceTotalCap;
|
|
131
|
+
/**
|
|
132
|
+
* Drop whole buckets, least-recently-WRITTEN first, until the total is at or
|
|
133
|
+
* below `target`. Never touches `protectedKey` (the bucket being written to
|
|
134
|
+
* right now) and always leaves at least one bucket standing.
|
|
135
|
+
*/
|
|
136
|
+
private evictBucketsUntil;
|
|
137
|
+
/** Drop exactly `count` buckets, least-recently-written first. */
|
|
138
|
+
private evictBuckets;
|
|
139
|
+
/** Bucket keys ordered least-recently-written first, excluding `protectedKey`.
|
|
140
|
+
* A bucket with no recorded write sorts first — it can only be a leftover. */
|
|
141
|
+
private evictionOrder;
|
|
142
|
+
/** Remove a bucket and everything that indexes it. An empty ring left behind
|
|
143
|
+
* is the leak this class shipped with — the map only ever grew. */
|
|
144
|
+
private dropBucket;
|
|
72
145
|
/** Entries retained across every bucket. */
|
|
73
146
|
size(): number;
|
|
147
|
+
/** Buckets currently retained. Observability for the aggregate bound — an
|
|
148
|
+
* addon roster that only ever grows is what this class had to stop doing. */
|
|
149
|
+
bucketCount(): number;
|
|
74
150
|
/** Every retained entry across all buckets, newest-first (mirrors
|
|
75
151
|
* {@link LogRingBuffer.getAll}). Used to replay history to a destination. */
|
|
76
152
|
getAll(): LogEntry[];
|
|
@@ -83,6 +159,9 @@ export declare class PartitionedLogBuffer {
|
|
|
83
159
|
*/
|
|
84
160
|
query(filter: LogFilter): LogEntry[];
|
|
85
161
|
clear(filter?: LogFilter): number;
|
|
162
|
+
/** An emptied bucket is removed rather than left as an empty ring: the map
|
|
163
|
+
* used to grow with every addon id that ever logged and never shrink. */
|
|
164
|
+
private dropIfEmpty;
|
|
86
165
|
/** The bucket key a filter pins to via `tags.addonId`, or null when the
|
|
87
166
|
* filter doesn't constrain the addon (→ scan all buckets). */
|
|
88
167
|
private pinnedAddonKey;
|