@camstack/addon-pipeline 1.2.94 → 1.2.95
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-utils-DnUCCZVx.js → addon-utils-UMWiMAxR.js} +1 -1
- package/dist/audio-analyzer/index.js +58 -4
- package/dist/audio-analyzer/index.mjs +56 -2
- package/dist/detection-pipeline/index.js +194 -6
- package/dist/detection-pipeline/index.mjs +191 -3
- package/dist/{dist-XbrYiMV3.mjs → dist-C49o1k7u.mjs} +316 -2
- package/dist/{dist-BbRv3bM2.js → dist-hyryByiw.js} +333 -1
- package/dist/{event-loop-stall-monitor-CFWrOZ6G.mjs → event-loop-stall-monitor-CfxZBgNi.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-D9hqbc68.js → event-loop-stall-monitor-DPNTzGOT.js} +1 -1
- package/dist/{lazy-sharp-U0EtN7_C.js → lazy-sharp-Dxg6UeW-.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/process-memory-B9-UD7Rp.js +78 -0
- package/dist/process-memory-DXOB6eHX.mjs +66 -0
- package/dist/recorder/index.js +423 -47
- package/dist/recorder/index.mjs +422 -46
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BNOPhQ-y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CF-c0cDZ.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BjvmJK_d.mjs +26 -0
- package/dist/stream-broker/{hostInit-DCV2-mSi.mjs → hostInit-Cpcf2hDB.mjs} +3 -3
- package/dist/stream-broker/index.js +25 -7
- package/dist/stream-broker/index.mjs +25 -7
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CNC-ZcU3.js → worker-protocol-DR_a77D5.js} +1 -1
- package/dist/{worker-protocol-D0ewr67U.mjs → worker-protocol-DqQ2GI4M.mjs} +1 -1
- package/package.json +12 -7
- package/python/inference_pool.py +178 -3
- package/python/test_inference_pool_backpressure.py +7 -1
- package/python/test_inference_pool_memstats.py +146 -0
- package/dist/node-topology-platform-BkR_k6WT.mjs +0 -15
- package/dist/node-topology-platform-CFZ7F4xW.js +0 -20
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DMyOrQHq.mjs +0 -26
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-
|
|
2
|
+
import { Ct as nodePin, Dt as sleep, Ft as object, H as evaluateZoneRules, Lt as string, N as defaultDeviceFor$1, R as detectionPipelineCapability, Rt as union, T as YAMNET_TO_MACRO, V as enumerateInferenceDevices, X as pipelineExecutorCapability, ct as errMsg, d as DEVICE_BACKEND_TO_FORMAT, kt as array, mt as BaseAddon, ot as supportedRuntimes$1, rt as runtimeDevices$1, t as APPLE_SA_TO_MACRO, tt as resolvePoolMemoryPolicy, vt as createEvent, wt as parseJsonUnknown, y as PoolMemoryWatchdog, yt as hydrateSchema, zt as EventCategory } from "../dist-C49o1k7u.mjs";
|
|
3
|
+
import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-CfxZBgNi.mjs";
|
|
4
4
|
import { t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { n as pickNodePlatformArch, t as readProcessMemory } from "../process-memory-DXOB6eHX.mjs";
|
|
6
6
|
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-A2S9D7pu.mjs";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
@@ -701,6 +701,14 @@ var PoolWorker = class {
|
|
|
701
701
|
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
702
702
|
return await this.dispatch(MSG_COMMAND, payload);
|
|
703
703
|
}
|
|
704
|
+
/** Command whose reply shape is NOT the load/unload/replace/status envelope
|
|
705
|
+
* (e.g. `mem_stats`) — returns the raw JSON record, so no cast is needed
|
|
706
|
+
* at the call site. */
|
|
707
|
+
async sendRawCommand(cmd) {
|
|
708
|
+
this.ensureReady();
|
|
709
|
+
const payload = Buffer.from(JSON.stringify(cmd), "utf8");
|
|
710
|
+
return this.dispatch(MSG_COMMAND, payload);
|
|
711
|
+
}
|
|
704
712
|
async dispose() {
|
|
705
713
|
const proc = this.process;
|
|
706
714
|
if (!proc) return;
|
|
@@ -1065,6 +1073,46 @@ var SharedInferencePool = class {
|
|
|
1065
1073
|
for (const resp of responses) if (resp.status !== "ok") throw new Error(`Failed to replace model at index ${index}: ${resp.error ?? "unknown"}`);
|
|
1066
1074
|
return { loadMs: Math.max(...responses.map((r) => r.loadMs ?? 0)) };
|
|
1067
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Per-worker Python-side memory diagnostics (`mem_stats` command):
|
|
1078
|
+
* RSS/VmSize/peak/threads from /proc/self, getrusage maxrss, glibc malloc
|
|
1079
|
+
* arena count, gc counts, served-request counters, per-model queue depth,
|
|
1080
|
+
* cache entry counts. Queried by the pool memory watchdog once per sweep —
|
|
1081
|
+
* the internals that distinguish "leak" from "working set" and that
|
|
1082
|
+
* /proc/<pid> alone cannot see. A worker that is not ready (or whose
|
|
1083
|
+
* command fails) contributes an `error` record instead of vanishing.
|
|
1084
|
+
*/
|
|
1085
|
+
async getMemStats() {
|
|
1086
|
+
const out = [];
|
|
1087
|
+
for (const [i, w] of this.workers.entries()) {
|
|
1088
|
+
if (!w.isReady()) {
|
|
1089
|
+
out.push({
|
|
1090
|
+
worker: `w${i}`,
|
|
1091
|
+
pid: w.getPid(),
|
|
1092
|
+
stats: null,
|
|
1093
|
+
error: "worker not ready"
|
|
1094
|
+
});
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const stats = await w.sendRawCommand({ cmd: "mem_stats" });
|
|
1099
|
+
out.push({
|
|
1100
|
+
worker: `w${i}`,
|
|
1101
|
+
pid: w.getPid(),
|
|
1102
|
+
stats,
|
|
1103
|
+
error: null
|
|
1104
|
+
});
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
out.push({
|
|
1107
|
+
worker: `w${i}`,
|
|
1108
|
+
pid: w.getPid(),
|
|
1109
|
+
stats: null,
|
|
1110
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1068
1116
|
async getStatus() {
|
|
1069
1117
|
if (this.workers.length === 0) return [];
|
|
1070
1118
|
return (await this.workers[0].sendCommand({ cmd: "status" })).models ?? [];
|
|
@@ -1783,6 +1831,29 @@ var EngineFactory = class {
|
|
|
1783
1831
|
getPoolPid() {
|
|
1784
1832
|
return this.pool?.getPid() ?? null;
|
|
1785
1833
|
}
|
|
1834
|
+
/** All live worker pids of the underlying Python pool (memory watchdog). */
|
|
1835
|
+
getPoolPids() {
|
|
1836
|
+
return (this.pool?.getPids() ?? []).filter((pid) => pid !== null);
|
|
1837
|
+
}
|
|
1838
|
+
/** Summed backlog / shed / dropped across the pool's workers — the cheap
|
|
1839
|
+
* internals the memory watchdog attaches to its periodic `pool memory`
|
|
1840
|
+
* line so a leak (RSS up, work flat) is separable from load. */
|
|
1841
|
+
getPoolBacklog() {
|
|
1842
|
+
if (!this.pool) return {
|
|
1843
|
+
inFlight: 0,
|
|
1844
|
+
shed: 0,
|
|
1845
|
+
dropped: 0
|
|
1846
|
+
};
|
|
1847
|
+
return {
|
|
1848
|
+
...this.pool.getBacklog(),
|
|
1849
|
+
dropped: this.pool.getDroppedResponseCount()
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
/** Python-side per-worker memory diagnostics (see SharedInferencePool). */
|
|
1853
|
+
async getPoolMemStats() {
|
|
1854
|
+
if (!this.pool) return [];
|
|
1855
|
+
return this.pool.getMemStats();
|
|
1856
|
+
}
|
|
1786
1857
|
/**
|
|
1787
1858
|
* Whether this factory exposes the batched fast path (`batchInferRaw`).
|
|
1788
1859
|
*/
|
|
@@ -1992,6 +2063,65 @@ var IdlePoolReaper = class {
|
|
|
1992
2063
|
}
|
|
1993
2064
|
};
|
|
1994
2065
|
//#endregion
|
|
2066
|
+
//#region src/detection-pipeline/engine/pool-memory-guard.ts
|
|
2067
|
+
function createDetectionPoolMemoryGuard(source, logger) {
|
|
2068
|
+
const log = logger.child("pool-memory");
|
|
2069
|
+
return new PoolMemoryWatchdog({
|
|
2070
|
+
policy: resolvePoolMemoryPolicy(process.env),
|
|
2071
|
+
log,
|
|
2072
|
+
restart: (key) => source.restartPool(key),
|
|
2073
|
+
sample: async () => {
|
|
2074
|
+
const out = [];
|
|
2075
|
+
for (const pool of source.listPools()) {
|
|
2076
|
+
const telemetry = await samplePool(pool, log);
|
|
2077
|
+
if (telemetry) out.push(telemetry);
|
|
2078
|
+
}
|
|
2079
|
+
return out;
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
async function samplePool(pool, log) {
|
|
2084
|
+
const pids = pool.factory.getPoolPids();
|
|
2085
|
+
if (pids.length === 0) return null;
|
|
2086
|
+
let maxRssBytes = 0;
|
|
2087
|
+
const workers = [];
|
|
2088
|
+
for (const pid of pids) {
|
|
2089
|
+
const mem = await readProcessMemory(pid);
|
|
2090
|
+
if (!mem) {
|
|
2091
|
+
log.warn("pool memory sample dropped for worker pid", { meta: {
|
|
2092
|
+
poolKey: pool.key,
|
|
2093
|
+
pid
|
|
2094
|
+
} });
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
if (mem.rssBytes > maxRssBytes) maxRssBytes = mem.rssBytes;
|
|
2098
|
+
workers.push({
|
|
2099
|
+
pid,
|
|
2100
|
+
rssMb: Math.round(mem.rssBytes / (1024 * 1024)),
|
|
2101
|
+
vmMb: Math.round(mem.vmBytes / (1024 * 1024)),
|
|
2102
|
+
peakRssMb: Math.round(mem.hwmBytes / (1024 * 1024)),
|
|
2103
|
+
swapMb: Math.round(mem.swapBytes / (1024 * 1024)),
|
|
2104
|
+
threads: mem.threads
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
if (workers.length === 0) return null;
|
|
2108
|
+
const backlog = pool.factory.getPoolBacklog();
|
|
2109
|
+
const memStats = await pool.factory.getPoolMemStats().catch((err) => err instanceof Error ? err.message : String(err));
|
|
2110
|
+
return {
|
|
2111
|
+
key: pool.key,
|
|
2112
|
+
pids,
|
|
2113
|
+
rssBytes: maxRssBytes,
|
|
2114
|
+
meta: {
|
|
2115
|
+
workers,
|
|
2116
|
+
inFlight: backlog.inFlight,
|
|
2117
|
+
shed: backlog.shed,
|
|
2118
|
+
dropped: backlog.dropped,
|
|
2119
|
+
modelsLoaded: pool.factory.listLoaded().length,
|
|
2120
|
+
memStats
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
//#endregion
|
|
1995
2125
|
//#region src/detection-pipeline/engine-provisioner.ts
|
|
1996
2126
|
/** Incremental backoff growing to a ~5 min cap; retries indefinitely at cap. */
|
|
1997
2127
|
var BACKOFF_SCHEDULE_MS = [
|
|
@@ -4783,6 +4913,55 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4783
4913
|
} })
|
|
4784
4914
|
});
|
|
4785
4915
|
static OVERRIDE_CACHE_TTL_MS = 6e4;
|
|
4916
|
+
/**
|
|
4917
|
+
* RSS bound + periodic memory telemetry for the Python inference pools
|
|
4918
|
+
* (2026-08-18 OOM audit: the pools, not the recorder, were the host's OOM
|
|
4919
|
+
* driver — grow for hours, kernel-killed at 3.8/8.8 GB, respawn, repeat).
|
|
4920
|
+
* Policy, restart budget and the `pool memory` Loki line live in
|
|
4921
|
+
* `@camstack/types` (`PoolMemoryWatchdog`); this provider only enumerates
|
|
4922
|
+
* its pools and exposes its EXISTING dispose-and-recreate machinery as the
|
|
4923
|
+
* restart action. Started in `init()`, stopped in `shutdown()`.
|
|
4924
|
+
*/
|
|
4925
|
+
poolMemoryGuard = null;
|
|
4926
|
+
/** Watchdog view of the live pools: the node-default pool plus every
|
|
4927
|
+
* per-device pool. Keys are stable per pool identity so the watchdog's
|
|
4928
|
+
* baseline tracks one pool across sweeps. */
|
|
4929
|
+
listGuardedPools() {
|
|
4930
|
+
const pools = [];
|
|
4931
|
+
if (this.engineFactory) pools.push({
|
|
4932
|
+
key: `default:${deviceKeyOf(this.currentEngine)}`,
|
|
4933
|
+
factory: this.engineFactory
|
|
4934
|
+
});
|
|
4935
|
+
for (const [deviceKey, factory] of this.factoriesByDevice) pools.push({
|
|
4936
|
+
key: deviceKey,
|
|
4937
|
+
factory
|
|
4938
|
+
});
|
|
4939
|
+
return pools;
|
|
4940
|
+
}
|
|
4941
|
+
/**
|
|
4942
|
+
* Watchdog restart action — the same dispose-then-lazy-recreate the idle
|
|
4943
|
+
* reaper and the tuning respawn already use, never a second lifecycle. The
|
|
4944
|
+
* map/field is cleared FIRST so a concurrent dispatch rebuilds fresh via
|
|
4945
|
+
* the single-flight guards instead of grabbing the disposing instance;
|
|
4946
|
+
* models reload on the next dispatch via `ensureModelsForSteps`. Frames
|
|
4947
|
+
* arriving during the seconds-long re-spin are dropped (and counted by the
|
|
4948
|
+
* existing shed/timeout paths), not crashed — a bounded gap, unlike the
|
|
4949
|
+
* kernel's alternative.
|
|
4950
|
+
*/
|
|
4951
|
+
async restartGuardedPool(key) {
|
|
4952
|
+
if (key.startsWith("default:")) {
|
|
4953
|
+
const factory = this.engineFactory;
|
|
4954
|
+
if (!factory) return;
|
|
4955
|
+
this.engineFactory = null;
|
|
4956
|
+
await factory.dispose();
|
|
4957
|
+
return;
|
|
4958
|
+
}
|
|
4959
|
+
const factory = this.factoriesByDevice.get(key);
|
|
4960
|
+
if (!factory) return;
|
|
4961
|
+
this.factoriesByDevice.delete(key);
|
|
4962
|
+
this.deviceReaper.cancel(key);
|
|
4963
|
+
await factory.dispose();
|
|
4964
|
+
}
|
|
4786
4965
|
/** Read the addon settings store (all keys). */
|
|
4787
4966
|
readStore;
|
|
4788
4967
|
/** Write a patch to the addon settings store. */
|
|
@@ -4818,6 +4997,13 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4818
4997
|
async init() {
|
|
4819
4998
|
this.needsAutoPick = true;
|
|
4820
4999
|
this.log.info("Node-default engine derive deferred to setApi()");
|
|
5000
|
+
if (!this.poolMemoryGuard) {
|
|
5001
|
+
this.poolMemoryGuard = createDetectionPoolMemoryGuard({
|
|
5002
|
+
listPools: () => this.listGuardedPools(),
|
|
5003
|
+
restartPool: (key) => this.restartGuardedPool(key)
|
|
5004
|
+
}, this.log);
|
|
5005
|
+
this.poolMemoryGuard.start();
|
|
5006
|
+
}
|
|
4821
5007
|
}
|
|
4822
5008
|
/**
|
|
4823
5009
|
* Lazy-install the pip requirements file matching `engine.backend` into
|
|
@@ -6648,6 +6834,8 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6648
6834
|
};
|
|
6649
6835
|
}
|
|
6650
6836
|
async shutdown() {
|
|
6837
|
+
this.poolMemoryGuard?.stop();
|
|
6838
|
+
this.poolMemoryGuard = null;
|
|
6651
6839
|
await this.evictOverrideCache("shutdown");
|
|
6652
6840
|
if (this.engineFactory) {
|
|
6653
6841
|
await this.engineFactory.dispose();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-XfKNtfCc.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -38,6 +38,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
38
38
|
EventCategory["AddonInstalled"] = "addon.installed";
|
|
39
39
|
EventCategory["AddonUninstalled"] = "addon.uninstalled";
|
|
40
40
|
EventCategory["AddonCrashed"] = "addon.crashed";
|
|
41
|
+
/**
|
|
42
|
+
* A RUNNER the D6 crash circuit-breaker gave up on — terminal.
|
|
43
|
+
*
|
|
44
|
+
* `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
|
|
45
|
+
* respawned"); this is the one that never resolves itself. Emitted exactly
|
|
46
|
+
* once per trip, by `process-service.ts`, carrying the node, the runner, the
|
|
47
|
+
* addons it hosted and the crash count that tripped the breaker.
|
|
48
|
+
*
|
|
49
|
+
* It exists because a runner marked terminally `failed` used to be SILENT:
|
|
50
|
+
* on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
|
|
51
|
+
* errors, the breaker stopped respawning it (correctly), `/health` kept
|
|
52
|
+
* returning 200, and fleet-wide recording was gone for 3h15 before the
|
|
53
|
+
* operator's phone told him. The Notification Center's system-event intake
|
|
54
|
+
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
55
|
+
*/
|
|
56
|
+
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
41
57
|
EventCategory["AddonError"] = "addon.error";
|
|
42
58
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
43
59
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -16071,6 +16087,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
16071
16087
|
"node-offline",
|
|
16072
16088
|
"node-inference-unavailable",
|
|
16073
16089
|
"detection-blind",
|
|
16090
|
+
"addon-crash-loop",
|
|
16074
16091
|
"addon-update-available",
|
|
16075
16092
|
"server-update-available",
|
|
16076
16093
|
"alarm-triggered",
|
|
@@ -36425,6 +36442,303 @@ function maskUrlCredentials(rawUrl) {
|
|
|
36425
36442
|
return rawUrl;
|
|
36426
36443
|
}
|
|
36427
36444
|
}
|
|
36445
|
+
var MB = 1024 * 1024;
|
|
36446
|
+
var DEFAULT_POOL_MEMORY_POLICY = {
|
|
36447
|
+
sampleIntervalMs: 6e4,
|
|
36448
|
+
baselineSettleSamples: 5,
|
|
36449
|
+
baselineSampleCount: 3,
|
|
36450
|
+
restartMultiple: 4,
|
|
36451
|
+
floorBytes: 1024 * MB,
|
|
36452
|
+
ceilingBytes: 3072 * MB,
|
|
36453
|
+
cooldownMs: 30 * 6e4,
|
|
36454
|
+
maxRestartsPerWindow: 6,
|
|
36455
|
+
restartWindowMs: 1440 * 6e4
|
|
36456
|
+
};
|
|
36457
|
+
/**
|
|
36458
|
+
* Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
|
|
36459
|
+
* absent values keep the default — an operator typo must never disable the
|
|
36460
|
+
* bound or set it to zero.
|
|
36461
|
+
*/
|
|
36462
|
+
function resolvePoolMemoryPolicy(env) {
|
|
36463
|
+
const num = (key, fallback, min) => {
|
|
36464
|
+
const raw = env[key];
|
|
36465
|
+
if (raw === void 0) return fallback;
|
|
36466
|
+
const parsed = Number(raw);
|
|
36467
|
+
return Number.isFinite(parsed) && parsed >= min ? parsed : fallback;
|
|
36468
|
+
};
|
|
36469
|
+
const d = DEFAULT_POOL_MEMORY_POLICY;
|
|
36470
|
+
return {
|
|
36471
|
+
sampleIntervalMs: num("CAMSTACK_POOL_MEM_INTERVAL_MS", d.sampleIntervalMs, 5e3),
|
|
36472
|
+
baselineSettleSamples: d.baselineSettleSamples,
|
|
36473
|
+
baselineSampleCount: d.baselineSampleCount,
|
|
36474
|
+
restartMultiple: num("CAMSTACK_POOL_MEM_MULTIPLE", d.restartMultiple, 1.5),
|
|
36475
|
+
floorBytes: num("CAMSTACK_POOL_MEM_FLOOR_MB", d.floorBytes / MB, 128) * MB,
|
|
36476
|
+
ceilingBytes: num("CAMSTACK_POOL_MEM_CEILING_MB", d.ceilingBytes / MB, 256) * MB,
|
|
36477
|
+
cooldownMs: num("CAMSTACK_POOL_MEM_COOLDOWN_MS", d.cooldownMs, 6e4),
|
|
36478
|
+
maxRestartsPerWindow: num("CAMSTACK_POOL_MEM_MAX_RESTARTS", d.maxRestartsPerWindow, 1),
|
|
36479
|
+
restartWindowMs: num("CAMSTACK_POOL_MEM_RESTART_WINDOW_MS", d.restartWindowMs, 6e4)
|
|
36480
|
+
};
|
|
36481
|
+
}
|
|
36482
|
+
/** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
|
|
36483
|
+
* Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
|
|
36484
|
+
function parseProcStatus(text) {
|
|
36485
|
+
const kb = (label) => {
|
|
36486
|
+
const match = text.match(new RegExp(`^${label}:\\s+(\\d+)\\s*kB`, "m"));
|
|
36487
|
+
return match ? Number(match[1]) * 1024 : null;
|
|
36488
|
+
};
|
|
36489
|
+
const rssBytes = kb("VmRSS");
|
|
36490
|
+
if (rssBytes === null) return null;
|
|
36491
|
+
const threadsMatch = text.match(/^Threads:\s+(\d+)/m);
|
|
36492
|
+
return {
|
|
36493
|
+
rssBytes,
|
|
36494
|
+
vmBytes: kb("VmSize") ?? 0,
|
|
36495
|
+
hwmBytes: kb("VmHWM") ?? 0,
|
|
36496
|
+
swapBytes: kb("VmSwap") ?? 0,
|
|
36497
|
+
threads: threadsMatch ? Number(threadsMatch[1]) : 0
|
|
36498
|
+
};
|
|
36499
|
+
}
|
|
36500
|
+
function initialPoolMemoryState() {
|
|
36501
|
+
return {
|
|
36502
|
+
settleSeen: 0,
|
|
36503
|
+
baselineWindow: [],
|
|
36504
|
+
baselineBytes: null,
|
|
36505
|
+
restartsAt: [],
|
|
36506
|
+
lastRestartAt: null
|
|
36507
|
+
};
|
|
36508
|
+
}
|
|
36509
|
+
function median(values) {
|
|
36510
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
36511
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
36512
|
+
}
|
|
36513
|
+
function advanceBaseline(state, rssBytes, policy) {
|
|
36514
|
+
if (state.baselineBytes !== null) return state;
|
|
36515
|
+
if (state.settleSeen < policy.baselineSettleSamples) return {
|
|
36516
|
+
...state,
|
|
36517
|
+
settleSeen: state.settleSeen + 1
|
|
36518
|
+
};
|
|
36519
|
+
const window = [...state.baselineWindow, rssBytes];
|
|
36520
|
+
if (window.length < policy.baselineSampleCount) return {
|
|
36521
|
+
...state,
|
|
36522
|
+
baselineWindow: window
|
|
36523
|
+
};
|
|
36524
|
+
return {
|
|
36525
|
+
...state,
|
|
36526
|
+
baselineWindow: window,
|
|
36527
|
+
baselineBytes: median(window)
|
|
36528
|
+
};
|
|
36529
|
+
}
|
|
36530
|
+
/** The one place a pool's restart threshold is computed. */
|
|
36531
|
+
function poolMemoryThreshold(baselineBytes, policy) {
|
|
36532
|
+
if (baselineBytes === null) return policy.ceilingBytes;
|
|
36533
|
+
return Math.min(policy.ceilingBytes, Math.max(policy.floorBytes, policy.restartMultiple * baselineBytes));
|
|
36534
|
+
}
|
|
36535
|
+
function pruneRestarts(restartsAt, nowMs, policy) {
|
|
36536
|
+
return restartsAt.filter((t) => nowMs - t < policy.restartWindowMs);
|
|
36537
|
+
}
|
|
36538
|
+
/**
|
|
36539
|
+
* Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
|
|
36540
|
+
* The caller performs (and logs) the restart, then commits it via
|
|
36541
|
+
* {@link commitWatchdogRestart}.
|
|
36542
|
+
*/
|
|
36543
|
+
function evaluatePoolMemory(state, rssBytes, nowMs, policy) {
|
|
36544
|
+
const next = advanceBaseline(state, rssBytes, policy);
|
|
36545
|
+
const thresholdBytes = poolMemoryThreshold(next.baselineBytes, policy);
|
|
36546
|
+
if (rssBytes <= thresholdBytes) return {
|
|
36547
|
+
state: next,
|
|
36548
|
+
action: next.baselineBytes === null ? "baseline-pending" : "ok",
|
|
36549
|
+
baselineBytes: next.baselineBytes,
|
|
36550
|
+
thresholdBytes
|
|
36551
|
+
};
|
|
36552
|
+
const inWindow = pruneRestarts(next.restartsAt, nowMs, policy);
|
|
36553
|
+
const pruned = {
|
|
36554
|
+
...next,
|
|
36555
|
+
restartsAt: inWindow
|
|
36556
|
+
};
|
|
36557
|
+
if (inWindow.length >= policy.maxRestartsPerWindow) return {
|
|
36558
|
+
state: pruned,
|
|
36559
|
+
action: "exhausted",
|
|
36560
|
+
baselineBytes: next.baselineBytes,
|
|
36561
|
+
thresholdBytes
|
|
36562
|
+
};
|
|
36563
|
+
if (pruned.lastRestartAt !== null && nowMs - pruned.lastRestartAt < policy.cooldownMs) return {
|
|
36564
|
+
state: pruned,
|
|
36565
|
+
action: "cooldown",
|
|
36566
|
+
baselineBytes: next.baselineBytes,
|
|
36567
|
+
thresholdBytes
|
|
36568
|
+
};
|
|
36569
|
+
return {
|
|
36570
|
+
state: pruned,
|
|
36571
|
+
action: "restart",
|
|
36572
|
+
baselineBytes: next.baselineBytes,
|
|
36573
|
+
thresholdBytes
|
|
36574
|
+
};
|
|
36575
|
+
}
|
|
36576
|
+
/** Record a performed watchdog restart: budget consumed, baseline reset —
|
|
36577
|
+
* the recreated pool is a NEW process and gets a fresh settle window. */
|
|
36578
|
+
function commitWatchdogRestart(state, nowMs) {
|
|
36579
|
+
return {
|
|
36580
|
+
...initialPoolMemoryState(),
|
|
36581
|
+
restartsAt: [...state.restartsAt, nowMs],
|
|
36582
|
+
lastRestartAt: nowMs
|
|
36583
|
+
};
|
|
36584
|
+
}
|
|
36585
|
+
/** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
|
|
36586
|
+
* kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
|
|
36587
|
+
* restart budget is kept: it bounds the WATCHDOG, not the pool. */
|
|
36588
|
+
function resetPoolBaseline(state) {
|
|
36589
|
+
return {
|
|
36590
|
+
...initialPoolMemoryState(),
|
|
36591
|
+
restartsAt: state.restartsAt,
|
|
36592
|
+
lastRestartAt: state.lastRestartAt
|
|
36593
|
+
};
|
|
36594
|
+
}
|
|
36595
|
+
/**
|
|
36596
|
+
* Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
|
|
36597
|
+
* several pools at once (e.g. after a model rollout grew every baseline)
|
|
36598
|
+
* would take detection down on every camera simultaneously; one per sweep
|
|
36599
|
+
* means the survivors keep serving while the worst offender reloads, and the
|
|
36600
|
+
* next sweep (one interval later) takes the next one.
|
|
36601
|
+
*/
|
|
36602
|
+
function pickRestartCandidate(candidates) {
|
|
36603
|
+
if (candidates.length === 0) return null;
|
|
36604
|
+
let worst = candidates[0];
|
|
36605
|
+
for (const c of candidates) if (c.rssBytes / c.thresholdBytes > worst.rssBytes / worst.thresholdBytes) worst = c;
|
|
36606
|
+
return worst.key;
|
|
36607
|
+
}
|
|
36608
|
+
var PoolMemoryWatchdog = class {
|
|
36609
|
+
pools = /* @__PURE__ */ new Map();
|
|
36610
|
+
timer = null;
|
|
36611
|
+
sweeping = false;
|
|
36612
|
+
stopped = false;
|
|
36613
|
+
opts;
|
|
36614
|
+
now;
|
|
36615
|
+
setTimer;
|
|
36616
|
+
clearTimer;
|
|
36617
|
+
constructor(opts) {
|
|
36618
|
+
this.opts = opts;
|
|
36619
|
+
this.now = opts.now ?? (() => Date.now());
|
|
36620
|
+
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
36621
|
+
this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
|
|
36622
|
+
}
|
|
36623
|
+
start() {
|
|
36624
|
+
if (this.timer || this.stopped) return;
|
|
36625
|
+
this.arm();
|
|
36626
|
+
}
|
|
36627
|
+
stop() {
|
|
36628
|
+
this.stopped = true;
|
|
36629
|
+
if (this.timer) {
|
|
36630
|
+
this.clearTimer(this.timer);
|
|
36631
|
+
this.timer = null;
|
|
36632
|
+
}
|
|
36633
|
+
}
|
|
36634
|
+
arm() {
|
|
36635
|
+
const timer = this.setTimer(() => {
|
|
36636
|
+
this.sweep().finally(() => {
|
|
36637
|
+
if (!this.stopped) this.arm();
|
|
36638
|
+
});
|
|
36639
|
+
}, this.opts.policy.sampleIntervalMs);
|
|
36640
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
36641
|
+
this.timer = timer;
|
|
36642
|
+
}
|
|
36643
|
+
/** One sweep: sample → log every pool → restart at most one. Public so the
|
|
36644
|
+
* loop is testable without fake global timers. */
|
|
36645
|
+
async sweep() {
|
|
36646
|
+
if (this.sweeping) return;
|
|
36647
|
+
this.sweeping = true;
|
|
36648
|
+
try {
|
|
36649
|
+
await this.doSweep();
|
|
36650
|
+
} catch (err) {
|
|
36651
|
+
this.opts.log.warn("pool memory sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
36652
|
+
} finally {
|
|
36653
|
+
this.sweeping = false;
|
|
36654
|
+
}
|
|
36655
|
+
}
|
|
36656
|
+
async doSweep() {
|
|
36657
|
+
const nowMs = this.now();
|
|
36658
|
+
const samples = await this.opts.sample();
|
|
36659
|
+
const candidates = [];
|
|
36660
|
+
const verdictByKey = /* @__PURE__ */ new Map();
|
|
36661
|
+
for (const t of samples) {
|
|
36662
|
+
const tracked = this.pools.get(t.key);
|
|
36663
|
+
let state = tracked?.state ?? initialPoolMemoryState();
|
|
36664
|
+
if (tracked && tracked.pids.length > 0 && !samePids(tracked.pids, t.pids)) {
|
|
36665
|
+
this.opts.log.warn("pool process changed outside the watchdog — re-baselining", { meta: {
|
|
36666
|
+
poolKey: t.key,
|
|
36667
|
+
previousPids: tracked.pids,
|
|
36668
|
+
pids: t.pids
|
|
36669
|
+
} });
|
|
36670
|
+
state = resetPoolBaseline(state);
|
|
36671
|
+
}
|
|
36672
|
+
const verdict = evaluatePoolMemory(state, t.rssBytes, nowMs, this.opts.policy);
|
|
36673
|
+
this.pools.set(t.key, {
|
|
36674
|
+
state: verdict.state,
|
|
36675
|
+
pids: t.pids
|
|
36676
|
+
});
|
|
36677
|
+
verdictByKey.set(t.key, verdict);
|
|
36678
|
+
this.opts.log.info("pool memory", { meta: {
|
|
36679
|
+
poolKey: t.key,
|
|
36680
|
+
pids: t.pids,
|
|
36681
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
36682
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
36683
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
36684
|
+
action: verdict.action,
|
|
36685
|
+
...t.meta
|
|
36686
|
+
} });
|
|
36687
|
+
if (verdict.action === "restart") candidates.push({
|
|
36688
|
+
key: t.key,
|
|
36689
|
+
rssBytes: t.rssBytes,
|
|
36690
|
+
thresholdBytes: verdict.thresholdBytes
|
|
36691
|
+
});
|
|
36692
|
+
else if (verdict.action === "cooldown") this.opts.log.warn("pool over memory threshold but inside restart cooldown", { meta: {
|
|
36693
|
+
poolKey: t.key,
|
|
36694
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
36695
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
36696
|
+
cooldownMs: this.opts.policy.cooldownMs
|
|
36697
|
+
} });
|
|
36698
|
+
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: {
|
|
36699
|
+
poolKey: t.key,
|
|
36700
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
36701
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
36702
|
+
maxRestartsPerWindow: this.opts.policy.maxRestartsPerWindow,
|
|
36703
|
+
restartWindowMs: this.opts.policy.restartWindowMs
|
|
36704
|
+
} });
|
|
36705
|
+
}
|
|
36706
|
+
for (const key of [...this.pools.keys()]) if (!samples.some((t) => t.key === key)) this.pools.delete(key);
|
|
36707
|
+
const pickedKey = pickRestartCandidate(candidates);
|
|
36708
|
+
if (pickedKey === null) return;
|
|
36709
|
+
const picked = candidates.find((c) => c.key === pickedKey);
|
|
36710
|
+
const verdict = verdictByKey.get(pickedKey);
|
|
36711
|
+
const tracked = this.pools.get(pickedKey);
|
|
36712
|
+
const restartsInWindow = tracked.state.restartsAt.length + 1;
|
|
36713
|
+
this.opts.log.warn("pool memory watchdog restarting pool — RSS over threshold", { meta: {
|
|
36714
|
+
poolKey: pickedKey,
|
|
36715
|
+
pids: tracked.pids,
|
|
36716
|
+
rssMb: Math.round(picked.rssBytes / MB),
|
|
36717
|
+
thresholdMb: Math.round(picked.thresholdBytes / MB),
|
|
36718
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
36719
|
+
restartsInWindow,
|
|
36720
|
+
deferredCandidates: candidates.filter((c) => c.key !== pickedKey).map((c) => c.key)
|
|
36721
|
+
} });
|
|
36722
|
+
try {
|
|
36723
|
+
await this.opts.restart(pickedKey);
|
|
36724
|
+
this.pools.set(pickedKey, {
|
|
36725
|
+
state: commitWatchdogRestart(tracked.state, this.now()),
|
|
36726
|
+
pids: []
|
|
36727
|
+
});
|
|
36728
|
+
} catch (err) {
|
|
36729
|
+
this.opts.log.error("pool memory watchdog restart failed", { meta: {
|
|
36730
|
+
poolKey: pickedKey,
|
|
36731
|
+
error: err instanceof Error ? err.message : String(err)
|
|
36732
|
+
} });
|
|
36733
|
+
}
|
|
36734
|
+
}
|
|
36735
|
+
};
|
|
36736
|
+
function samePids(a, b) {
|
|
36737
|
+
if (a.length !== b.length) return false;
|
|
36738
|
+
const sortedA = [...a].sort((x, y) => x - y);
|
|
36739
|
+
const sortedB = [...b].sort((x, y) => x - y);
|
|
36740
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
36741
|
+
}
|
|
36428
36742
|
/**
|
|
36429
36743
|
* Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
|
|
36430
36744
|
* drain() returns up to maxCount items in FIFO order and removes them.
|
|
@@ -36762,4 +37076,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
36762
37076
|
return out;
|
|
36763
37077
|
}
|
|
36764
37078
|
//#endregion
|
|
36765
|
-
export {
|
|
37079
|
+
export { recordingExportCapability as $, cameraStreamsCapability as A, boolean as At, egressTransportFromRequest as B, RecordingConfigSchema as C, nodePin as Ct, audioAnalysisCapability as D, sleep as Dt, addonWidgetsSourceCapability as E, selectAssignedProfileSlots as Et, deriveBatteryPresence as F, object as Ft, maskUrlCredentials as G, evaluateZoneRules as H, deriveDetailCropRect as I, record as It, pickDetailCropConvention as J, motionDetectionCapability as K, deriveRecordingMode as L, string as Lt, customAction as M, lazy as Mt, defaultDeviceFor as N, literal as Nt, audioAnalyzerCapability as O, _enum as Ot, defineCustomActions as P, number as Pt, recordingCapability as Q, detectionPipelineCapability as R, union as Rt, RECORDING_EXPORT_MAX_READ_BYTES as S, makeSourceBrokerId as St, YAMNET_TO_MACRO as T, parseProfileBrokerId as Tt, hfModelUrl as U, enumerateInferenceDevices as V, mapAudioLabelToMacro as W, pipelineExecutorCapability as X, pickNativeLeaseOverride as Y, pipelineRunnerCapability as Z, NativeLeaseSettingsSchema as _, DeviceType as _t, COCO_80_LABELS as a, streamBrokerCapability as at, RATE_CONTROL_RELAXED as b, isEvent as bt, DEFAULT_DETAIL_CROP_CONVENTION as c, errMsg as ct, DEVICE_BACKEND_TO_FORMAT as d, buildFfmpegArgs as dt, resolveEgressDecodeHwAccel as et, EVENT_PAD_MS as f, invocationFromEncodeProfile as ft, NativeLeaseAdmissionSchema as g, DeviceFeature as gt, HF_BASE_URL as h, CAM_PROFILE_ORDER as ht, BatteryStatusSchema as i, storageEvictableCapability as it, createHwAccelCache as j, discriminatedUnion as jt, batteryCapability as k, array as kt, DEFAULT_EVENTS_BAND_BUFFER_SEC as l, AUDIO_PRESETS as lt, ExportRecordSchema as m, BaseAddon as mt, AUDIO_BACKEND_CHOICES as n, resolveRecordingProfiles as nt, COCO_TO_MACRO as o, supportedRuntimes as ot, EncodeProfileSchema as p, isSoftwareDecode as pt, parseProcStatus as q, AUDIO_MACRO_LABELS as r, runtimeDevices as rt, DEFAULT_AUDIO_ANALYZER_CONFIG as s, webrtcSessionCapability as st, APPLE_SA_TO_MACRO as t, resolvePoolMemoryPolicy as tt, DEFAULT_NATIVE_LEASE_SETTINGS as u, Fmp4BoxSplitter as ut, OpsLogEntrySchema as v, createEvent as vt, RingBuffer as w, parseJsonUnknown as wt, RATE_CONTROL_TIGHT as x, makeProfileBrokerId as xt, PoolMemoryWatchdog as y, hydrateSchema as yt, egressTranscodeSharingKey as z, EventCategory as zt };
|