@camstack/addon-post-analysis 1.2.91 → 1.2.93
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/{dist-CquggoRC.mjs → dist-3QRyOemL.mjs} +353 -20
- package/dist/{dist-CTIUuGRB.js → dist-DYoWeRRt.js} +370 -19
- package/dist/embedding-encoder/index.js +171 -4
- package/dist/embedding-encoder/index.mjs +171 -4
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Q3EjHtct.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4-ljPpt.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-D2EZ3nOf.mjs → hostInit-BoUejl7E.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +389 -160
- package/dist/pipeline-analytics/index.mjs +352 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +3 -2
|
@@ -29,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
enumerable: true
|
|
30
30
|
}) : target, mod));
|
|
31
31
|
//#endregion
|
|
32
|
-
//#region ../types/dist/event-category-
|
|
32
|
+
//#region ../types/dist/event-category-XfKNtfCc.mjs
|
|
33
33
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
34
34
|
EventCategory["SystemBoot"] = "system.boot";
|
|
35
35
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -68,6 +68,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
68
68
|
EventCategory["AddonInstalled"] = "addon.installed";
|
|
69
69
|
EventCategory["AddonUninstalled"] = "addon.uninstalled";
|
|
70
70
|
EventCategory["AddonCrashed"] = "addon.crashed";
|
|
71
|
+
/**
|
|
72
|
+
* A RUNNER the D6 crash circuit-breaker gave up on — terminal.
|
|
73
|
+
*
|
|
74
|
+
* `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
|
|
75
|
+
* respawned"); this is the one that never resolves itself. Emitted exactly
|
|
76
|
+
* once per trip, by `process-service.ts`, carrying the node, the runner, the
|
|
77
|
+
* addons it hosted and the crash count that tripped the breaker.
|
|
78
|
+
*
|
|
79
|
+
* It exists because a runner marked terminally `failed` used to be SILENT:
|
|
80
|
+
* on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
|
|
81
|
+
* errors, the breaker stopped respawning it (correctly), `/health` kept
|
|
82
|
+
* returning 200, and fleet-wide recording was gone for 3h15 before the
|
|
83
|
+
* operator's phone told him. The Notification Center's system-event intake
|
|
84
|
+
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
85
|
+
*/
|
|
86
|
+
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
71
87
|
EventCategory["AddonError"] = "addon.error";
|
|
72
88
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
73
89
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -14845,6 +14861,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14845
14861
|
"node-offline",
|
|
14846
14862
|
"node-inference-unavailable",
|
|
14847
14863
|
"detection-blind",
|
|
14864
|
+
"addon-crash-loop",
|
|
14848
14865
|
"addon-update-available",
|
|
14849
14866
|
"server-update-available",
|
|
14850
14867
|
"alarm-triggered",
|
|
@@ -15970,6 +15987,10 @@ var NC_CONDITION_CATALOG = [
|
|
|
15970
15987
|
value: "detection-blind",
|
|
15971
15988
|
label: "Camera detecting nothing"
|
|
15972
15989
|
},
|
|
15990
|
+
{
|
|
15991
|
+
value: "addon-crash-loop",
|
|
15992
|
+
label: "Addon stopped after repeated crashes"
|
|
15993
|
+
},
|
|
15973
15994
|
{
|
|
15974
15995
|
value: "addon-update-available",
|
|
15975
15996
|
label: "Addon update available"
|
|
@@ -21487,20 +21508,6 @@ var VectorStatsResultSchema = object({
|
|
|
21487
21508
|
exact: boolean()
|
|
21488
21509
|
});
|
|
21489
21510
|
method(VectorDeclareIndexInputSchema, _void(), { kind: "mutation" }), method(VectorUpsertInputSchema, VectorUpsertResultSchema, { kind: "mutation" }), method(VectorQueryInputSchema, VectorQueryResultSchema), method(VectorGetInputSchema, VectorGetResultSchema), method(VectorDeleteInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorDeleteByFilterInputSchema, VectorDeleteResultSchema, { kind: "mutation" }), method(VectorStatsInputSchema, VectorStatsResultSchema);
|
|
21490
|
-
/**
|
|
21491
|
-
* `videoclips` — the unified, navigable-clip surface for a camera.
|
|
21492
|
-
*
|
|
21493
|
-
* A device-scoped WRAPPER cap (like `pipeline-analytics`): exactly one active
|
|
21494
|
-
* provider per device, substitutable. The DEFAULT provider (registered by
|
|
21495
|
-
* `addon-post-analysis`, `defaultActive: true`) composes the analytics event
|
|
21496
|
-
* log (markers + thumbnails) with the recorder's `getPlaybackManifest` — a clip
|
|
21497
|
-
* is a time-WINDOW over existing footage, never a separate file. A camera that
|
|
21498
|
-
* exposes NATIVE onboard clips (Reolink/Hikvision NVR) can later substitute the
|
|
21499
|
-
* wrapper provider for its device and serve its own clip catalog + URLs.
|
|
21500
|
-
*
|
|
21501
|
-
* A `Clip` is purely time-based (subtree-blind): playback resolves segments by
|
|
21502
|
-
* temporal overlap, so the API never decides `continuous` vs `events`.
|
|
21503
|
-
*/
|
|
21504
21511
|
var ClipSchema = object({
|
|
21505
21512
|
/** Opaque, provider-namespaced id. The default provider encodes the time
|
|
21506
21513
|
* window so `getClipPlayback` is self-contained (no event re-query). */
|
|
@@ -21526,12 +21533,41 @@ var ClipSchema = object({
|
|
|
21526
21533
|
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
21527
21534
|
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
21528
21535
|
* mint their own stills.
|
|
21536
|
+
*
|
|
21537
|
+
* **VOUCHED, never fabricated.** A provider emits this only for an event it
|
|
21538
|
+
* has CONFIRMED owns at least one media row (its own, or its owning track's).
|
|
21539
|
+
* Absent is meaningful — "this visit has no event still" — never "we did not
|
|
21540
|
+
* look". Stamping it from a URL template made 35% of one camera's clips point
|
|
21541
|
+
* at a 404 (device 3836, 2026-08-17: 64 of 179 clips dead, 63 of them motion).
|
|
21542
|
+
* A read that FAILS drops the claim; it never invents it.
|
|
21529
21543
|
*/
|
|
21530
21544
|
thumbnail: string().optional(),
|
|
21531
|
-
/**
|
|
21532
|
-
*
|
|
21533
|
-
*
|
|
21545
|
+
/**
|
|
21546
|
+
* An instant INSIDE this visit's footage, hole-safe, where a recorded still
|
|
21547
|
+
* can be decoded. Present whenever the visit came from recorded availability;
|
|
21548
|
+
* absent on a per-event padded window (there is no footage to promise).
|
|
21549
|
+
*
|
|
21550
|
+
* This is not a thumbnail and not a second byte path: it is the argument to
|
|
21551
|
+
* the recorder's existing still route. The surface — never the provider —
|
|
21552
|
+
* decides whether to use it. It is the midpoint of the visit's LONGEST
|
|
21553
|
+
* contiguous range, not of the visit: a visit spans its holes by
|
|
21554
|
+
* construction, so a naive midpoint lands in dead air.
|
|
21555
|
+
*/
|
|
21556
|
+
stillAtMs: number().optional(),
|
|
21557
|
+
/**
|
|
21558
|
+
* Analytics event ids that overlap this visit — a BOUNDED sample, newest
|
|
21559
|
+
* first within kind (object → motion → audio), capped at
|
|
21560
|
+
* {@link MAX_CLIP_EVENT_IDS}. Empty on footage-only clips.
|
|
21561
|
+
*
|
|
21562
|
+
* Bounded because it is not a payload the surface pages through: one visit on
|
|
21563
|
+
* device 615 carried 2 345 ids, and `eventIds` was 99% of a 153 KB
|
|
21564
|
+
* camera-day. Read {@link eventCount} for the true total.
|
|
21565
|
+
*/
|
|
21534
21566
|
eventIds: array(string()).optional(),
|
|
21567
|
+
/** How many analytics events actually overlap this visit. Differs from
|
|
21568
|
+
* `eventIds.length` exactly when the sample was capped — so a truncated
|
|
21569
|
+
* list is never mistaken for a quiet visit. */
|
|
21570
|
+
eventCount: number().int().nonnegative().optional(),
|
|
21535
21571
|
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
21536
21572
|
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
21537
21573
|
* bar keeps showing them via `recording.getAvailability`. */
|
|
@@ -38553,7 +38589,7 @@ function systemEventFilterApplies(kind, filter) {
|
|
|
38553
38589
|
switch (filter) {
|
|
38554
38590
|
case "deviceIds": return kind.startsWith("device-") || kind.startsWith("stream-") || kind === "detection-blind" || kind === "alarm-triggered" || kind === "export-completed";
|
|
38555
38591
|
case "deviceTypes": return kind.startsWith("device-") || kind === "detection-blind";
|
|
38556
|
-
case "nodeIds": return kind.startsWith("node-") || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
|
|
38592
|
+
case "nodeIds": return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
|
|
38557
38593
|
case "packageNames": return kind.endsWith("update-available") || kind === "addon-updated" || kind === "server-updated";
|
|
38558
38594
|
}
|
|
38559
38595
|
}
|
|
@@ -39010,6 +39046,303 @@ function cosineSimilarity(a, b) {
|
|
|
39010
39046
|
function hfModelUrl(repo, path) {
|
|
39011
39047
|
return `https://huggingface.co/${repo}/resolve/main/${path}`;
|
|
39012
39048
|
}
|
|
39049
|
+
var MB = 1024 * 1024;
|
|
39050
|
+
var DEFAULT_POOL_MEMORY_POLICY = {
|
|
39051
|
+
sampleIntervalMs: 6e4,
|
|
39052
|
+
baselineSettleSamples: 5,
|
|
39053
|
+
baselineSampleCount: 3,
|
|
39054
|
+
restartMultiple: 4,
|
|
39055
|
+
floorBytes: 1024 * MB,
|
|
39056
|
+
ceilingBytes: 3072 * MB,
|
|
39057
|
+
cooldownMs: 30 * 6e4,
|
|
39058
|
+
maxRestartsPerWindow: 6,
|
|
39059
|
+
restartWindowMs: 1440 * 6e4
|
|
39060
|
+
};
|
|
39061
|
+
/**
|
|
39062
|
+
* Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
|
|
39063
|
+
* absent values keep the default — an operator typo must never disable the
|
|
39064
|
+
* bound or set it to zero.
|
|
39065
|
+
*/
|
|
39066
|
+
function resolvePoolMemoryPolicy(env) {
|
|
39067
|
+
const num = (key, fallback, min) => {
|
|
39068
|
+
const raw = env[key];
|
|
39069
|
+
if (raw === void 0) return fallback;
|
|
39070
|
+
const parsed = Number(raw);
|
|
39071
|
+
return Number.isFinite(parsed) && parsed >= min ? parsed : fallback;
|
|
39072
|
+
};
|
|
39073
|
+
const d = DEFAULT_POOL_MEMORY_POLICY;
|
|
39074
|
+
return {
|
|
39075
|
+
sampleIntervalMs: num("CAMSTACK_POOL_MEM_INTERVAL_MS", d.sampleIntervalMs, 5e3),
|
|
39076
|
+
baselineSettleSamples: d.baselineSettleSamples,
|
|
39077
|
+
baselineSampleCount: d.baselineSampleCount,
|
|
39078
|
+
restartMultiple: num("CAMSTACK_POOL_MEM_MULTIPLE", d.restartMultiple, 1.5),
|
|
39079
|
+
floorBytes: num("CAMSTACK_POOL_MEM_FLOOR_MB", d.floorBytes / MB, 128) * MB,
|
|
39080
|
+
ceilingBytes: num("CAMSTACK_POOL_MEM_CEILING_MB", d.ceilingBytes / MB, 256) * MB,
|
|
39081
|
+
cooldownMs: num("CAMSTACK_POOL_MEM_COOLDOWN_MS", d.cooldownMs, 6e4),
|
|
39082
|
+
maxRestartsPerWindow: num("CAMSTACK_POOL_MEM_MAX_RESTARTS", d.maxRestartsPerWindow, 1),
|
|
39083
|
+
restartWindowMs: num("CAMSTACK_POOL_MEM_RESTART_WINDOW_MS", d.restartWindowMs, 6e4)
|
|
39084
|
+
};
|
|
39085
|
+
}
|
|
39086
|
+
/** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
|
|
39087
|
+
* Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
|
|
39088
|
+
function parseProcStatus(text) {
|
|
39089
|
+
const kb = (label) => {
|
|
39090
|
+
const match = text.match(new RegExp(`^${label}:\\s+(\\d+)\\s*kB`, "m"));
|
|
39091
|
+
return match ? Number(match[1]) * 1024 : null;
|
|
39092
|
+
};
|
|
39093
|
+
const rssBytes = kb("VmRSS");
|
|
39094
|
+
if (rssBytes === null) return null;
|
|
39095
|
+
const threadsMatch = text.match(/^Threads:\s+(\d+)/m);
|
|
39096
|
+
return {
|
|
39097
|
+
rssBytes,
|
|
39098
|
+
vmBytes: kb("VmSize") ?? 0,
|
|
39099
|
+
hwmBytes: kb("VmHWM") ?? 0,
|
|
39100
|
+
swapBytes: kb("VmSwap") ?? 0,
|
|
39101
|
+
threads: threadsMatch ? Number(threadsMatch[1]) : 0
|
|
39102
|
+
};
|
|
39103
|
+
}
|
|
39104
|
+
function initialPoolMemoryState() {
|
|
39105
|
+
return {
|
|
39106
|
+
settleSeen: 0,
|
|
39107
|
+
baselineWindow: [],
|
|
39108
|
+
baselineBytes: null,
|
|
39109
|
+
restartsAt: [],
|
|
39110
|
+
lastRestartAt: null
|
|
39111
|
+
};
|
|
39112
|
+
}
|
|
39113
|
+
function median(values) {
|
|
39114
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
39115
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
39116
|
+
}
|
|
39117
|
+
function advanceBaseline(state, rssBytes, policy) {
|
|
39118
|
+
if (state.baselineBytes !== null) return state;
|
|
39119
|
+
if (state.settleSeen < policy.baselineSettleSamples) return {
|
|
39120
|
+
...state,
|
|
39121
|
+
settleSeen: state.settleSeen + 1
|
|
39122
|
+
};
|
|
39123
|
+
const window = [...state.baselineWindow, rssBytes];
|
|
39124
|
+
if (window.length < policy.baselineSampleCount) return {
|
|
39125
|
+
...state,
|
|
39126
|
+
baselineWindow: window
|
|
39127
|
+
};
|
|
39128
|
+
return {
|
|
39129
|
+
...state,
|
|
39130
|
+
baselineWindow: window,
|
|
39131
|
+
baselineBytes: median(window)
|
|
39132
|
+
};
|
|
39133
|
+
}
|
|
39134
|
+
/** The one place a pool's restart threshold is computed. */
|
|
39135
|
+
function poolMemoryThreshold(baselineBytes, policy) {
|
|
39136
|
+
if (baselineBytes === null) return policy.ceilingBytes;
|
|
39137
|
+
return Math.min(policy.ceilingBytes, Math.max(policy.floorBytes, policy.restartMultiple * baselineBytes));
|
|
39138
|
+
}
|
|
39139
|
+
function pruneRestarts(restartsAt, nowMs, policy) {
|
|
39140
|
+
return restartsAt.filter((t) => nowMs - t < policy.restartWindowMs);
|
|
39141
|
+
}
|
|
39142
|
+
/**
|
|
39143
|
+
* Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
|
|
39144
|
+
* The caller performs (and logs) the restart, then commits it via
|
|
39145
|
+
* {@link commitWatchdogRestart}.
|
|
39146
|
+
*/
|
|
39147
|
+
function evaluatePoolMemory(state, rssBytes, nowMs, policy) {
|
|
39148
|
+
const next = advanceBaseline(state, rssBytes, policy);
|
|
39149
|
+
const thresholdBytes = poolMemoryThreshold(next.baselineBytes, policy);
|
|
39150
|
+
if (rssBytes <= thresholdBytes) return {
|
|
39151
|
+
state: next,
|
|
39152
|
+
action: next.baselineBytes === null ? "baseline-pending" : "ok",
|
|
39153
|
+
baselineBytes: next.baselineBytes,
|
|
39154
|
+
thresholdBytes
|
|
39155
|
+
};
|
|
39156
|
+
const inWindow = pruneRestarts(next.restartsAt, nowMs, policy);
|
|
39157
|
+
const pruned = {
|
|
39158
|
+
...next,
|
|
39159
|
+
restartsAt: inWindow
|
|
39160
|
+
};
|
|
39161
|
+
if (inWindow.length >= policy.maxRestartsPerWindow) return {
|
|
39162
|
+
state: pruned,
|
|
39163
|
+
action: "exhausted",
|
|
39164
|
+
baselineBytes: next.baselineBytes,
|
|
39165
|
+
thresholdBytes
|
|
39166
|
+
};
|
|
39167
|
+
if (pruned.lastRestartAt !== null && nowMs - pruned.lastRestartAt < policy.cooldownMs) return {
|
|
39168
|
+
state: pruned,
|
|
39169
|
+
action: "cooldown",
|
|
39170
|
+
baselineBytes: next.baselineBytes,
|
|
39171
|
+
thresholdBytes
|
|
39172
|
+
};
|
|
39173
|
+
return {
|
|
39174
|
+
state: pruned,
|
|
39175
|
+
action: "restart",
|
|
39176
|
+
baselineBytes: next.baselineBytes,
|
|
39177
|
+
thresholdBytes
|
|
39178
|
+
};
|
|
39179
|
+
}
|
|
39180
|
+
/** Record a performed watchdog restart: budget consumed, baseline reset —
|
|
39181
|
+
* the recreated pool is a NEW process and gets a fresh settle window. */
|
|
39182
|
+
function commitWatchdogRestart(state, nowMs) {
|
|
39183
|
+
return {
|
|
39184
|
+
...initialPoolMemoryState(),
|
|
39185
|
+
restartsAt: [...state.restartsAt, nowMs],
|
|
39186
|
+
lastRestartAt: nowMs
|
|
39187
|
+
};
|
|
39188
|
+
}
|
|
39189
|
+
/** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
|
|
39190
|
+
* kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
|
|
39191
|
+
* restart budget is kept: it bounds the WATCHDOG, not the pool. */
|
|
39192
|
+
function resetPoolBaseline(state) {
|
|
39193
|
+
return {
|
|
39194
|
+
...initialPoolMemoryState(),
|
|
39195
|
+
restartsAt: state.restartsAt,
|
|
39196
|
+
lastRestartAt: state.lastRestartAt
|
|
39197
|
+
};
|
|
39198
|
+
}
|
|
39199
|
+
/**
|
|
39200
|
+
* Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
|
|
39201
|
+
* several pools at once (e.g. after a model rollout grew every baseline)
|
|
39202
|
+
* would take detection down on every camera simultaneously; one per sweep
|
|
39203
|
+
* means the survivors keep serving while the worst offender reloads, and the
|
|
39204
|
+
* next sweep (one interval later) takes the next one.
|
|
39205
|
+
*/
|
|
39206
|
+
function pickRestartCandidate(candidates) {
|
|
39207
|
+
if (candidates.length === 0) return null;
|
|
39208
|
+
let worst = candidates[0];
|
|
39209
|
+
for (const c of candidates) if (c.rssBytes / c.thresholdBytes > worst.rssBytes / worst.thresholdBytes) worst = c;
|
|
39210
|
+
return worst.key;
|
|
39211
|
+
}
|
|
39212
|
+
var PoolMemoryWatchdog = class {
|
|
39213
|
+
pools = /* @__PURE__ */ new Map();
|
|
39214
|
+
timer = null;
|
|
39215
|
+
sweeping = false;
|
|
39216
|
+
stopped = false;
|
|
39217
|
+
opts;
|
|
39218
|
+
now;
|
|
39219
|
+
setTimer;
|
|
39220
|
+
clearTimer;
|
|
39221
|
+
constructor(opts) {
|
|
39222
|
+
this.opts = opts;
|
|
39223
|
+
this.now = opts.now ?? (() => Date.now());
|
|
39224
|
+
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
39225
|
+
this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
|
|
39226
|
+
}
|
|
39227
|
+
start() {
|
|
39228
|
+
if (this.timer || this.stopped) return;
|
|
39229
|
+
this.arm();
|
|
39230
|
+
}
|
|
39231
|
+
stop() {
|
|
39232
|
+
this.stopped = true;
|
|
39233
|
+
if (this.timer) {
|
|
39234
|
+
this.clearTimer(this.timer);
|
|
39235
|
+
this.timer = null;
|
|
39236
|
+
}
|
|
39237
|
+
}
|
|
39238
|
+
arm() {
|
|
39239
|
+
const timer = this.setTimer(() => {
|
|
39240
|
+
this.sweep().finally(() => {
|
|
39241
|
+
if (!this.stopped) this.arm();
|
|
39242
|
+
});
|
|
39243
|
+
}, this.opts.policy.sampleIntervalMs);
|
|
39244
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
39245
|
+
this.timer = timer;
|
|
39246
|
+
}
|
|
39247
|
+
/** One sweep: sample → log every pool → restart at most one. Public so the
|
|
39248
|
+
* loop is testable without fake global timers. */
|
|
39249
|
+
async sweep() {
|
|
39250
|
+
if (this.sweeping) return;
|
|
39251
|
+
this.sweeping = true;
|
|
39252
|
+
try {
|
|
39253
|
+
await this.doSweep();
|
|
39254
|
+
} catch (err) {
|
|
39255
|
+
this.opts.log.warn("pool memory sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
39256
|
+
} finally {
|
|
39257
|
+
this.sweeping = false;
|
|
39258
|
+
}
|
|
39259
|
+
}
|
|
39260
|
+
async doSweep() {
|
|
39261
|
+
const nowMs = this.now();
|
|
39262
|
+
const samples = await this.opts.sample();
|
|
39263
|
+
const candidates = [];
|
|
39264
|
+
const verdictByKey = /* @__PURE__ */ new Map();
|
|
39265
|
+
for (const t of samples) {
|
|
39266
|
+
const tracked = this.pools.get(t.key);
|
|
39267
|
+
let state = tracked?.state ?? initialPoolMemoryState();
|
|
39268
|
+
if (tracked && tracked.pids.length > 0 && !samePids(tracked.pids, t.pids)) {
|
|
39269
|
+
this.opts.log.warn("pool process changed outside the watchdog — re-baselining", { meta: {
|
|
39270
|
+
poolKey: t.key,
|
|
39271
|
+
previousPids: tracked.pids,
|
|
39272
|
+
pids: t.pids
|
|
39273
|
+
} });
|
|
39274
|
+
state = resetPoolBaseline(state);
|
|
39275
|
+
}
|
|
39276
|
+
const verdict = evaluatePoolMemory(state, t.rssBytes, nowMs, this.opts.policy);
|
|
39277
|
+
this.pools.set(t.key, {
|
|
39278
|
+
state: verdict.state,
|
|
39279
|
+
pids: t.pids
|
|
39280
|
+
});
|
|
39281
|
+
verdictByKey.set(t.key, verdict);
|
|
39282
|
+
this.opts.log.info("pool memory", { meta: {
|
|
39283
|
+
poolKey: t.key,
|
|
39284
|
+
pids: t.pids,
|
|
39285
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39286
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
39287
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39288
|
+
action: verdict.action,
|
|
39289
|
+
...t.meta
|
|
39290
|
+
} });
|
|
39291
|
+
if (verdict.action === "restart") candidates.push({
|
|
39292
|
+
key: t.key,
|
|
39293
|
+
rssBytes: t.rssBytes,
|
|
39294
|
+
thresholdBytes: verdict.thresholdBytes
|
|
39295
|
+
});
|
|
39296
|
+
else if (verdict.action === "cooldown") this.opts.log.warn("pool over memory threshold but inside restart cooldown", { meta: {
|
|
39297
|
+
poolKey: t.key,
|
|
39298
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39299
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39300
|
+
cooldownMs: this.opts.policy.cooldownMs
|
|
39301
|
+
} });
|
|
39302
|
+
else if (verdict.action === "exhausted") this.opts.log.error("pool memory watchdog gave up — restart budget exhausted, pool leaks faster than restarts can pay for; needs a heap profile (py-spy / tracemalloc)", { meta: {
|
|
39303
|
+
poolKey: t.key,
|
|
39304
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39305
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39306
|
+
maxRestartsPerWindow: this.opts.policy.maxRestartsPerWindow,
|
|
39307
|
+
restartWindowMs: this.opts.policy.restartWindowMs
|
|
39308
|
+
} });
|
|
39309
|
+
}
|
|
39310
|
+
for (const key of [...this.pools.keys()]) if (!samples.some((t) => t.key === key)) this.pools.delete(key);
|
|
39311
|
+
const pickedKey = pickRestartCandidate(candidates);
|
|
39312
|
+
if (pickedKey === null) return;
|
|
39313
|
+
const picked = candidates.find((c) => c.key === pickedKey);
|
|
39314
|
+
const verdict = verdictByKey.get(pickedKey);
|
|
39315
|
+
const tracked = this.pools.get(pickedKey);
|
|
39316
|
+
const restartsInWindow = tracked.state.restartsAt.length + 1;
|
|
39317
|
+
this.opts.log.warn("pool memory watchdog restarting pool — RSS over threshold", { meta: {
|
|
39318
|
+
poolKey: pickedKey,
|
|
39319
|
+
pids: tracked.pids,
|
|
39320
|
+
rssMb: Math.round(picked.rssBytes / MB),
|
|
39321
|
+
thresholdMb: Math.round(picked.thresholdBytes / MB),
|
|
39322
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
39323
|
+
restartsInWindow,
|
|
39324
|
+
deferredCandidates: candidates.filter((c) => c.key !== pickedKey).map((c) => c.key)
|
|
39325
|
+
} });
|
|
39326
|
+
try {
|
|
39327
|
+
await this.opts.restart(pickedKey);
|
|
39328
|
+
this.pools.set(pickedKey, {
|
|
39329
|
+
state: commitWatchdogRestart(tracked.state, this.now()),
|
|
39330
|
+
pids: []
|
|
39331
|
+
});
|
|
39332
|
+
} catch (err) {
|
|
39333
|
+
this.opts.log.error("pool memory watchdog restart failed", { meta: {
|
|
39334
|
+
poolKey: pickedKey,
|
|
39335
|
+
error: err instanceof Error ? err.message : String(err)
|
|
39336
|
+
} });
|
|
39337
|
+
}
|
|
39338
|
+
}
|
|
39339
|
+
};
|
|
39340
|
+
function samePids(a, b) {
|
|
39341
|
+
if (a.length !== b.length) return false;
|
|
39342
|
+
const sortedA = [...a].sort((x, y) => x - y);
|
|
39343
|
+
const sortedB = [...b].sort((x, y) => x - y);
|
|
39344
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
39345
|
+
}
|
|
39013
39346
|
/**
|
|
39014
39347
|
* Vector wire codec — base64 of little-endian Float32.
|
|
39015
39348
|
*
|
|
@@ -39219,6 +39552,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
|
|
|
39219
39552
|
return OpsLogEntrySchema;
|
|
39220
39553
|
}
|
|
39221
39554
|
});
|
|
39555
|
+
Object.defineProperty(exports, "PoolMemoryWatchdog", {
|
|
39556
|
+
enumerable: true,
|
|
39557
|
+
get: function() {
|
|
39558
|
+
return PoolMemoryWatchdog;
|
|
39559
|
+
}
|
|
39560
|
+
});
|
|
39222
39561
|
Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
|
|
39223
39562
|
enumerable: true,
|
|
39224
39563
|
get: function() {
|
|
@@ -39489,6 +39828,12 @@ Object.defineProperty(exports, "object", {
|
|
|
39489
39828
|
return object;
|
|
39490
39829
|
}
|
|
39491
39830
|
});
|
|
39831
|
+
Object.defineProperty(exports, "parseProcStatus", {
|
|
39832
|
+
enumerable: true,
|
|
39833
|
+
get: function() {
|
|
39834
|
+
return parseProcStatus;
|
|
39835
|
+
}
|
|
39836
|
+
});
|
|
39492
39837
|
Object.defineProperty(exports, "partialRecord", {
|
|
39493
39838
|
enumerable: true,
|
|
39494
39839
|
get: function() {
|
|
@@ -39525,6 +39870,12 @@ Object.defineProperty(exports, "record", {
|
|
|
39525
39870
|
return record;
|
|
39526
39871
|
}
|
|
39527
39872
|
});
|
|
39873
|
+
Object.defineProperty(exports, "resolvePoolMemoryPolicy", {
|
|
39874
|
+
enumerable: true,
|
|
39875
|
+
get: function() {
|
|
39876
|
+
return resolvePoolMemoryPolicy;
|
|
39877
|
+
}
|
|
39878
|
+
});
|
|
39528
39879
|
Object.defineProperty(exports, "sceneMonitorCapability", {
|
|
39529
39880
|
enumerable: true,
|
|
39530
39881
|
get: function() {
|