@camstack/addon-pipeline 1.2.29 → 1.2.31
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +448 -192
- package/dist/detection-pipeline/index.mjs +447 -191
- package/dist/{dist-DUHlZcWd.js → dist-BvFLZyhW.js} +105 -3
- package/dist/{dist-CsKaXmP8.mjs → dist-Du7_SNiR.mjs} +105 -3
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +127 -6
- package/dist/pipeline-runner/index.mjs +126 -6
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{step-definitions-1GIwU6R8.mjs → step-definitions-BkY9_IM6.mjs} +1 -1
- package/dist/{step-definitions-Drb9cWbB.js → step-definitions-gC-Ovglc.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-tHahN-lK.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BUXeX-vb.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ByHi7ZAe.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CscpvIRR.mjs +26 -0
- package/dist/stream-broker/{hostInit-CIwilJUZ.mjs → hostInit-DJ3h-b1X.mjs} +3 -3
- package/dist/stream-broker/index.js +3 -3
- package/dist/stream-broker/index.mjs +3 -3
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-B7VdNJlO.js → MaskShapeCanvas-DI4BY7W2-B0DyP46h.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-YyB4uLdH.js → MotionZonesSettings-NcxxQN8r-CzGSy_EO.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DfqixEad.js → PrivacyMaskSettings-APgPLF7p-CEgMcOOW.js} +1 -1
- package/embed-dist/assets/{index-zVLLVdIS.js → index-CHXOWKHF.js} +14 -14
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D1zJmtUe.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CSWUWZ8c.mjs +0 -26
|
@@ -3,10 +3,12 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_step_definitions = require("../step-definitions-
|
|
6
|
+
const require_dist = require("../dist-BvFLZyhW.js");
|
|
7
|
+
const require_step_definitions = require("../step-definitions-gC-Ovglc.js");
|
|
8
8
|
const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
|
|
9
9
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
|
|
10
|
+
let node_os = require("node:os");
|
|
11
|
+
node_os = require_chunk.__toESM(node_os);
|
|
10
12
|
let node_child_process = require("node:child_process");
|
|
11
13
|
let sharp = require("sharp");
|
|
12
14
|
sharp = require_chunk.__toESM(sharp);
|
|
@@ -14,8 +16,6 @@ let node_fs = require("node:fs");
|
|
|
14
16
|
node_fs = require_chunk.__toESM(node_fs);
|
|
15
17
|
let node_path = require("node:path");
|
|
16
18
|
node_path = require_chunk.__toESM(node_path);
|
|
17
|
-
let node_os = require("node:os");
|
|
18
|
-
node_os = require_chunk.__toESM(node_os);
|
|
19
19
|
//#region src/detection-pipeline/runtimes.ts
|
|
20
20
|
var KNOWN_PLATFORMS = [
|
|
21
21
|
"darwin",
|
|
@@ -81,14 +81,28 @@ function probedToHardwareInfo(hw) {
|
|
|
81
81
|
coral: coralInfoFrom(hw)
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* THE per-runtime pool provisioning registry — the single source for every
|
|
86
|
+
* pool's concurrency / worker count / batch tuning, keyed by the runtime THAT
|
|
87
|
+
* POOL runs. `engine-factory` used to carry a second, parallel copy of these
|
|
88
|
+
* defaults; it no longer does.
|
|
89
|
+
*
|
|
90
|
+
* `windowMs` / `maxBatchSize` are inert unless `batchMode !== 'none'`, and
|
|
91
|
+
* `numStreams` / `intraOpThreads` are inert at 0 (the runtime picks) — they are
|
|
92
|
+
* still spelled out per row so a reader never has to guess which default a
|
|
93
|
+
* missing key would have hit.
|
|
94
|
+
*/
|
|
84
95
|
var RUNTIME_DETAIL = {
|
|
85
96
|
onnx: {
|
|
86
97
|
label: "ONNX Runtime",
|
|
87
98
|
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
88
99
|
tuning: {
|
|
89
100
|
concurrency: 4,
|
|
101
|
+
numWorkers: 1,
|
|
90
102
|
batchMode: "list",
|
|
103
|
+
windowMs: 2,
|
|
91
104
|
maxBatchSize: 8,
|
|
105
|
+
numStreams: 0,
|
|
92
106
|
intraOpThreads: 0
|
|
93
107
|
}
|
|
94
108
|
},
|
|
@@ -97,8 +111,12 @@ var RUNTIME_DETAIL = {
|
|
|
97
111
|
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
98
112
|
tuning: {
|
|
99
113
|
concurrency: 1,
|
|
114
|
+
numWorkers: 1,
|
|
100
115
|
batchMode: "none",
|
|
101
|
-
|
|
116
|
+
windowMs: 2,
|
|
117
|
+
maxBatchSize: 8,
|
|
118
|
+
numStreams: 0,
|
|
119
|
+
intraOpThreads: 0
|
|
102
120
|
}
|
|
103
121
|
},
|
|
104
122
|
coreml: {
|
|
@@ -106,10 +124,12 @@ var RUNTIME_DETAIL = {
|
|
|
106
124
|
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
107
125
|
tuning: {
|
|
108
126
|
concurrency: 1,
|
|
127
|
+
numWorkers: 1,
|
|
109
128
|
batchMode: "none",
|
|
110
129
|
windowMs: 8,
|
|
111
130
|
maxBatchSize: 8,
|
|
112
|
-
|
|
131
|
+
numStreams: 0,
|
|
132
|
+
intraOpThreads: 0
|
|
113
133
|
}
|
|
114
134
|
},
|
|
115
135
|
edgetpu: {
|
|
@@ -117,8 +137,12 @@ var RUNTIME_DETAIL = {
|
|
|
117
137
|
pythonRequirements: ["requirements.txt", "requirements-edgetpu.txt"],
|
|
118
138
|
tuning: {
|
|
119
139
|
concurrency: 1,
|
|
140
|
+
numWorkers: 1,
|
|
120
141
|
batchMode: "none",
|
|
121
|
-
|
|
142
|
+
windowMs: 2,
|
|
143
|
+
maxBatchSize: 8,
|
|
144
|
+
numStreams: 0,
|
|
145
|
+
intraOpThreads: 0
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
};
|
|
@@ -161,6 +185,12 @@ function modelFormatFor(id) {
|
|
|
161
185
|
function pythonRequirementsFor(id) {
|
|
162
186
|
return RUNTIME_DETAIL[id].pythonRequirements;
|
|
163
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* The pool provisioning for ONE runtime. Callers must pass the runtime of the
|
|
190
|
+
* pool they are provisioning — never the node's default runtime. See
|
|
191
|
+
* `engine/pool-provisioning.ts` for the resolver that layers an operator
|
|
192
|
+
* override on top of this.
|
|
193
|
+
*/
|
|
164
194
|
function tuningFor(id) {
|
|
165
195
|
return RUNTIME_DETAIL[id].tuning;
|
|
166
196
|
}
|
|
@@ -379,6 +409,62 @@ var RAW_FMT_CODE = {
|
|
|
379
409
|
* large frame) never trips; override via env for constrained hardware.
|
|
380
410
|
*/
|
|
381
411
|
var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
|
|
412
|
+
/**
|
|
413
|
+
* Max inference requests outstanding to ONE worker before new ones are SHED.
|
|
414
|
+
*
|
|
415
|
+
* Without this the camera dies permanently the first time the worker gets
|
|
416
|
+
* slow, and it did: device 615 produced nothing for 52 minutes on 2026-07-31.
|
|
417
|
+
* The worker was neither dead nor deadlocked — it was replying at full rate,
|
|
418
|
+
* about a minute behind, and every reply landed after {@link
|
|
419
|
+
* POOL_INFER_TIMEOUT_MS} had already abandoned its request. The proof is in the
|
|
420
|
+
* logs: a steady drip of `Response for unknown request id`, request ids
|
|
421
|
+
* climbing ~13/s, for as long as the outage lasted.
|
|
422
|
+
*
|
|
423
|
+
* That state is self-sustaining and cannot drain, because frames keep arriving
|
|
424
|
+
* while every one of them is written straight to the pipe. `dispatch` had NO
|
|
425
|
+
* bound: a transient slowdown (inference went 40ms → 2500ms across several
|
|
426
|
+
* cameras at 16:19 that day) buys a backlog that is never paid off, and once
|
|
427
|
+
* the backlog exceeds the deadline the loss is total.
|
|
428
|
+
*
|
|
429
|
+
* The queue is therefore capped in TIME, not just in count: at the normal ~40ms
|
|
430
|
+
* per inference this is ~0.3s of work, and even at a 20× slowdown it stays well
|
|
431
|
+
* inside the 60s deadline — so a worker that falls behind sheds, catches up,
|
|
432
|
+
* and recovers on its own. Python already sheds under its own per-model bound
|
|
433
|
+
* and answers `{dropped: true}`; this is the same contract applied one layer
|
|
434
|
+
* earlier, where the unbounded queue actually is.
|
|
435
|
+
*/
|
|
436
|
+
var POOL_MAX_IN_FLIGHT = Math.max(1, Number(process.env["CAMSTACK_POOL_MAX_IN_FLIGHT"]) || 8);
|
|
437
|
+
/**
|
|
438
|
+
* Reply deadline for a LIVE inference request (ms), as opposed to the generous
|
|
439
|
+
* {@link POOL_INFER_TIMEOUT_MS} that model loads and commands keep.
|
|
440
|
+
*
|
|
441
|
+
* 60 seconds was never a deadline for live detection — it is three orders of
|
|
442
|
+
* magnitude past the point where the answer is worth having. Frames arrive at
|
|
443
|
+
* ~13/s; a detection that lands 60s late describes a scene that no longer
|
|
444
|
+
* exists, and the only thing the wait accomplished was holding a queue slot
|
|
445
|
+
* while the backlog grew. Operator directive 2026-07-31: "a few seconds are
|
|
446
|
+
* already too many for this scenario".
|
|
447
|
+
*
|
|
448
|
+
* Three seconds is deliberately still loose — normal inference on this fleet is
|
|
449
|
+
* 35-47ms, so this trips only at a ~70× slowdown — but it bounds the damage at
|
|
450
|
+
* seconds instead of a minute, and combined with {@link POOL_MAX_IN_FLIGHT} it
|
|
451
|
+
* makes the backlog mathematically unable to outrun the deadline: the queue can
|
|
452
|
+
* never hold more than `cap × deadline` of work.
|
|
453
|
+
*
|
|
454
|
+
* Commands and model loads keep the long timeout on purpose: a cold model load
|
|
455
|
+
* legitimately takes tens of seconds, and shedding or timing one out
|
|
456
|
+
* desynchronizes the worker's model slots.
|
|
457
|
+
*/
|
|
458
|
+
var POOL_LIVE_INFER_TIMEOUT_MS = Math.max(250, Number(process.env["CAMSTACK_POOL_LIVE_INFER_TIMEOUT_MS"]) || 3e3);
|
|
459
|
+
/** Opcodes the in-flight cap may shed. Commands and frame-cache writes are
|
|
460
|
+
* NEVER shed: a dropped command silently desynchronizes model state, and a
|
|
461
|
+
* dropped `cacheFrame` strands the `inferCached` that follows it. */
|
|
462
|
+
var SHEDDABLE_MSG_TYPES = new Set([
|
|
463
|
+
MSG_INFER_JPEG,
|
|
464
|
+
MSG_INFER_RAW,
|
|
465
|
+
MSG_INFER_BATCH,
|
|
466
|
+
MSG_INFER_CACHED
|
|
467
|
+
]);
|
|
382
468
|
var PoolHandle = class {
|
|
383
469
|
pool;
|
|
384
470
|
modelIndex;
|
|
@@ -458,6 +544,13 @@ var PoolWorker = class {
|
|
|
458
544
|
process = null;
|
|
459
545
|
receiveBuffer = Buffer.alloc(0);
|
|
460
546
|
pending = /* @__PURE__ */ new Map();
|
|
547
|
+
/** True while the in-flight cap is shedding — so the warning fires on the
|
|
548
|
+
* EDGE rather than once per dropped frame. */
|
|
549
|
+
shedding = false;
|
|
550
|
+
/** Inference requests shed by the in-flight cap, for this worker's lifetime.
|
|
551
|
+
* Surfaced so "the camera is quiet" and "the camera is saturated" can be
|
|
552
|
+
* told apart without reading the logs. */
|
|
553
|
+
shedCount = 0;
|
|
461
554
|
nextRequestId = 1;
|
|
462
555
|
ready = false;
|
|
463
556
|
log;
|
|
@@ -469,6 +562,15 @@ var PoolWorker = class {
|
|
|
469
562
|
getPid() {
|
|
470
563
|
return this.process?.pid ?? null;
|
|
471
564
|
}
|
|
565
|
+
/** Requests written and not yet answered. This is the BACKLOG — the number
|
|
566
|
+
* nothing measured while a camera sat at zero frames for 52 minutes. */
|
|
567
|
+
getInFlight() {
|
|
568
|
+
return this.pending.size;
|
|
569
|
+
}
|
|
570
|
+
/** Inference shed by the in-flight cap over this worker's lifetime. */
|
|
571
|
+
getShedCount() {
|
|
572
|
+
return this.shedCount;
|
|
573
|
+
}
|
|
472
574
|
isReady() {
|
|
473
575
|
return this.ready;
|
|
474
576
|
}
|
|
@@ -617,10 +719,51 @@ var PoolWorker = class {
|
|
|
617
719
|
this.ready = false;
|
|
618
720
|
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
619
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Shed an inference request when the worker already has {@link
|
|
724
|
+
* POOL_MAX_IN_FLIGHT} outstanding — the backlog is what turns a slow worker
|
|
725
|
+
* into a dead camera. Returns the shed response, or `null` to proceed.
|
|
726
|
+
*
|
|
727
|
+
* A shed is LOUD the first time and then rate-limited: at frame rate this
|
|
728
|
+
* fires ~13×/s, so logging every one would bury the signal it is meant to
|
|
729
|
+
* raise — but logging none is how a camera went dark for 52 minutes looking
|
|
730
|
+
* like a quiet afternoon.
|
|
731
|
+
*/
|
|
732
|
+
shedIfSaturated(msgType) {
|
|
733
|
+
if (!SHEDDABLE_MSG_TYPES.has(msgType)) return null;
|
|
734
|
+
if (this.pending.size < POOL_MAX_IN_FLIGHT) {
|
|
735
|
+
this.shedding = false;
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
if (!this.shedding) {
|
|
739
|
+
this.shedding = true;
|
|
740
|
+
this.log.warn("worker saturated — shedding inference until it catches up", { meta: {
|
|
741
|
+
worker: this.opts.workerLabel,
|
|
742
|
+
pid: this.getPid(),
|
|
743
|
+
runtime: this.opts.poolRuntime,
|
|
744
|
+
device: this.opts.device ?? "default",
|
|
745
|
+
inFlight: this.pending.size,
|
|
746
|
+
cap: POOL_MAX_IN_FLIGHT,
|
|
747
|
+
deadlineMs: POOL_LIVE_INFER_TIMEOUT_MS
|
|
748
|
+
} });
|
|
749
|
+
}
|
|
750
|
+
this.shedCount++;
|
|
751
|
+
return {
|
|
752
|
+
dropped: true,
|
|
753
|
+
shedReason: "worker-in-flight-cap"
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/** Live inference gets seconds; commands and model loads keep the long
|
|
757
|
+
* timeout — see {@link POOL_LIVE_INFER_TIMEOUT_MS}. */
|
|
758
|
+
deadlineFor(msgType) {
|
|
759
|
+
return SHEDDABLE_MSG_TYPES.has(msgType) ? POOL_LIVE_INFER_TIMEOUT_MS : POOL_INFER_TIMEOUT_MS;
|
|
760
|
+
}
|
|
620
761
|
dispatch(msgType, payload) {
|
|
762
|
+
const shed = this.shedIfSaturated(msgType);
|
|
763
|
+
if (shed) return Promise.resolve(shed);
|
|
621
764
|
const reqId = this.allocRequestId();
|
|
622
765
|
return new Promise((resolve, reject) => {
|
|
623
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
766
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
624
767
|
this.pending.set(reqId, {
|
|
625
768
|
resolve,
|
|
626
769
|
reject,
|
|
@@ -640,24 +783,30 @@ var PoolWorker = class {
|
|
|
640
783
|
* `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
|
|
641
784
|
* The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
|
|
642
785
|
*/
|
|
643
|
-
armRequestTimeout(reqId, reject) {
|
|
786
|
+
armRequestTimeout(reqId, reject, timeoutMs) {
|
|
644
787
|
const timer = setTimeout(() => {
|
|
645
788
|
if (this.pending.delete(reqId)) {
|
|
646
789
|
this.log.error("inference request timed out", { meta: {
|
|
647
790
|
worker: this.opts.workerLabel,
|
|
791
|
+
pid: this.getPid(),
|
|
792
|
+
runtime: this.opts.poolRuntime,
|
|
793
|
+
device: this.opts.device ?? "default",
|
|
794
|
+
inFlight: this.pending.size,
|
|
648
795
|
reqId,
|
|
649
796
|
timeoutMs: POOL_INFER_TIMEOUT_MS
|
|
650
797
|
} });
|
|
651
|
-
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${
|
|
798
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${timeoutMs}ms (worker alive, no reply)`));
|
|
652
799
|
}
|
|
653
|
-
},
|
|
800
|
+
}, timeoutMs);
|
|
654
801
|
timer.unref?.();
|
|
655
802
|
return timer;
|
|
656
803
|
}
|
|
657
804
|
dispatchMulti(msgType, payloadLen, parts) {
|
|
805
|
+
const shed = this.shedIfSaturated(msgType);
|
|
806
|
+
if (shed) return Promise.resolve(shed);
|
|
658
807
|
const reqId = this.allocRequestId();
|
|
659
808
|
return new Promise((resolve, reject) => {
|
|
660
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
809
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
661
810
|
this.pending.set(reqId, {
|
|
662
811
|
resolve,
|
|
663
812
|
reject,
|
|
@@ -763,6 +912,22 @@ var SharedInferencePool = class {
|
|
|
763
912
|
}
|
|
764
913
|
device;
|
|
765
914
|
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
915
|
+
/** Summed backlog and shed count across the pool's workers. A rising
|
|
916
|
+
* `inFlight` with a rising `shed` is a worker falling behind; a rising
|
|
917
|
+
* `inFlight` with zero `shed` and no results is the state that used to be
|
|
918
|
+
* invisible. */
|
|
919
|
+
getBacklog() {
|
|
920
|
+
let inFlight = 0;
|
|
921
|
+
let shed = 0;
|
|
922
|
+
for (const w of this.workers) {
|
|
923
|
+
inFlight += w.getInFlight();
|
|
924
|
+
shed += w.getShedCount();
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
inFlight,
|
|
928
|
+
shed
|
|
929
|
+
};
|
|
930
|
+
}
|
|
766
931
|
getPid() {
|
|
767
932
|
return this.workers[0]?.getPid() ?? null;
|
|
768
933
|
}
|
|
@@ -796,6 +961,7 @@ var SharedInferencePool = class {
|
|
|
796
961
|
loadedCount,
|
|
797
962
|
startupMs,
|
|
798
963
|
runtime: this.poolRuntime,
|
|
964
|
+
device: this.device ?? "default",
|
|
799
965
|
workers: this.numWorkers,
|
|
800
966
|
pythonWorkers: innerWorkers,
|
|
801
967
|
pids: this.getPids()
|
|
@@ -887,7 +1053,10 @@ var SharedInferencePool = class {
|
|
|
887
1053
|
async dispose() {
|
|
888
1054
|
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
889
1055
|
this.workers.length = 0;
|
|
890
|
-
this.log.info("Pool process(es) terminated"
|
|
1056
|
+
this.log.info("Pool process(es) terminated", { meta: {
|
|
1057
|
+
runtime: this.poolRuntime,
|
|
1058
|
+
device: this.device ?? "default"
|
|
1059
|
+
} });
|
|
891
1060
|
}
|
|
892
1061
|
/**
|
|
893
1062
|
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
@@ -900,7 +1069,9 @@ var SharedInferencePool = class {
|
|
|
900
1069
|
this.droppedResponseCount++;
|
|
901
1070
|
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
902
1071
|
modelIndex,
|
|
903
|
-
droppedTotal: this.droppedResponseCount
|
|
1072
|
+
droppedTotal: this.droppedResponseCount,
|
|
1073
|
+
runtime: this.poolRuntime,
|
|
1074
|
+
device: this.device ?? "default"
|
|
904
1075
|
} });
|
|
905
1076
|
}
|
|
906
1077
|
return result;
|
|
@@ -1236,6 +1407,126 @@ var PipelineModelManager = class {
|
|
|
1236
1407
|
}
|
|
1237
1408
|
};
|
|
1238
1409
|
//#endregion
|
|
1410
|
+
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1411
|
+
/**
|
|
1412
|
+
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1413
|
+
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1414
|
+
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1415
|
+
* than silently building a wrong pool. Format comes from the shared
|
|
1416
|
+
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1417
|
+
*/
|
|
1418
|
+
function resolveDeviceEngine(deviceKey) {
|
|
1419
|
+
const colon = deviceKey.indexOf(":");
|
|
1420
|
+
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1421
|
+
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1422
|
+
const format = require_dist.DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1423
|
+
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1424
|
+
return {
|
|
1425
|
+
runtime: "python",
|
|
1426
|
+
backend,
|
|
1427
|
+
format,
|
|
1428
|
+
device
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* The inverse of {@link resolveDeviceEngine}: the `<backend>:<device>` key that
|
|
1433
|
+
* names the pool an engine belongs to. Every pool-level log line carries this —
|
|
1434
|
+
* without it a default pool logs anonymously and six hours of logs cannot tell
|
|
1435
|
+
* "the NPU is unused" from "the NPU IS the anonymous default pool".
|
|
1436
|
+
* Falls back to the bare backend when the engine carries no device.
|
|
1437
|
+
*/
|
|
1438
|
+
function deviceKeyOf(engine) {
|
|
1439
|
+
return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/detection-pipeline/engine/pool-provisioning.ts
|
|
1443
|
+
/** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
|
|
1444
|
+
var OVERRIDABLE_KEYS = [
|
|
1445
|
+
"concurrency",
|
|
1446
|
+
"numWorkers",
|
|
1447
|
+
"batchMode",
|
|
1448
|
+
"windowMs",
|
|
1449
|
+
"maxBatchSize",
|
|
1450
|
+
"numStreams",
|
|
1451
|
+
"intraOpThreads"
|
|
1452
|
+
];
|
|
1453
|
+
/** Backends that are not a runtime of their own and run on onnxruntime. */
|
|
1454
|
+
var BACKEND_TO_RUNTIME = {
|
|
1455
|
+
onnx: "onnx",
|
|
1456
|
+
cpu: "onnx",
|
|
1457
|
+
cuda: "onnx",
|
|
1458
|
+
openvino: "openvino",
|
|
1459
|
+
coreml: "coreml",
|
|
1460
|
+
edgetpu: "edgetpu"
|
|
1461
|
+
};
|
|
1462
|
+
/**
|
|
1463
|
+
* The registry runtime a device-pool backend provisions from. Unknown backends
|
|
1464
|
+
* floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
|
|
1465
|
+
* format, so a pool never comes up unprovisioned.
|
|
1466
|
+
*/
|
|
1467
|
+
function runtimeIdForBackend(backend) {
|
|
1468
|
+
return BACKEND_TO_RUNTIME[backend] ?? "onnx";
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve the provisioning for a pool running `engine`, applying the precedence
|
|
1472
|
+
* documented at the top of this module. `engine.device` is not consulted today
|
|
1473
|
+
* (see the module doc) but is part of the signature so a bench-backed
|
|
1474
|
+
* per-device row lands here and nowhere else.
|
|
1475
|
+
*/
|
|
1476
|
+
function resolvePoolProvisioning(engine, override = {}) {
|
|
1477
|
+
const runtime = runtimeIdForBackend(engine.backend);
|
|
1478
|
+
const profile = tuningFor(runtime);
|
|
1479
|
+
return {
|
|
1480
|
+
runtime,
|
|
1481
|
+
overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
|
|
1482
|
+
concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
|
|
1483
|
+
numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
|
|
1484
|
+
batchMode: override.batchMode ?? profile.batchMode,
|
|
1485
|
+
windowMs: override.windowMs ?? profile.windowMs,
|
|
1486
|
+
maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
|
|
1487
|
+
numStreams: override.numStreams ?? profile.numStreams,
|
|
1488
|
+
intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function positiveNumber(value) {
|
|
1492
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1493
|
+
}
|
|
1494
|
+
function batchMode(value) {
|
|
1495
|
+
return value === "none" || value === "list" || value === "window" ? value : void 0;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Read the operator override out of the detection addon's settings. Only a
|
|
1499
|
+
* NON-AUTO value counts: the sliders persist `0` and the select persists `''`
|
|
1500
|
+
* for "Auto (backend default)", and those must resolve to the runtime's
|
|
1501
|
+
* registry value, not to a literal 0 worker threads.
|
|
1502
|
+
*
|
|
1503
|
+
* The result is deliberately sparse — an empty object means "nothing
|
|
1504
|
+
* overridden", which is what lets a per-device pool provision from its own
|
|
1505
|
+
* runtime instead of inheriting the node's.
|
|
1506
|
+
*/
|
|
1507
|
+
function readOperatorOverride(settings) {
|
|
1508
|
+
const override = {};
|
|
1509
|
+
const concurrency = positiveNumber(settings.concurrency);
|
|
1510
|
+
if (concurrency !== void 0) override.concurrency = concurrency;
|
|
1511
|
+
const numWorkers = positiveNumber(settings.numWorkers);
|
|
1512
|
+
if (numWorkers !== void 0) override.numWorkers = numWorkers;
|
|
1513
|
+
const mode = batchMode(settings.batchMode);
|
|
1514
|
+
if (mode !== void 0) override.batchMode = mode;
|
|
1515
|
+
const windowMs = positiveNumber(settings.windowMs);
|
|
1516
|
+
if (windowMs !== void 0) override.windowMs = windowMs;
|
|
1517
|
+
const maxBatchSize = positiveNumber(settings.maxBatchSize);
|
|
1518
|
+
if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
|
|
1519
|
+
const numStreams = positiveNumber(settings.numStreams);
|
|
1520
|
+
if (numStreams !== void 0) override.numStreams = numStreams;
|
|
1521
|
+
const intraOpThreads = positiveNumber(settings.intraOpThreads);
|
|
1522
|
+
if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
|
|
1523
|
+
return override;
|
|
1524
|
+
}
|
|
1525
|
+
/** Two overrides carry the same fields with the same values. */
|
|
1526
|
+
function overridesEqual(a, b) {
|
|
1527
|
+
return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
|
|
1528
|
+
}
|
|
1529
|
+
//#endregion
|
|
1239
1530
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
1240
1531
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
1241
1532
|
coreml: "coreml",
|
|
@@ -1305,9 +1596,31 @@ var EngineFactory = class {
|
|
|
1305
1596
|
poolManager = null;
|
|
1306
1597
|
log;
|
|
1307
1598
|
opts;
|
|
1599
|
+
/**
|
|
1600
|
+
* `<backend>:<device>` — on EVERY log line this factory writes. A pool that
|
|
1601
|
+
* logs anonymously cannot be told apart from another pool of the same runtime
|
|
1602
|
+
* on a different accelerator.
|
|
1603
|
+
*/
|
|
1604
|
+
deviceKey;
|
|
1605
|
+
/**
|
|
1606
|
+
* Resolved at CONSTRUCTION, not at pool spawn, so it is inspectable without
|
|
1607
|
+
* starting Python and so a pool that fails to spawn still logs what it was
|
|
1608
|
+
* trying to be.
|
|
1609
|
+
*/
|
|
1610
|
+
resolvedProvisioning;
|
|
1308
1611
|
constructor(opts) {
|
|
1309
1612
|
this.opts = opts;
|
|
1310
1613
|
this.log = opts.logger;
|
|
1614
|
+
this.deviceKey = deviceKeyOf(opts.engine);
|
|
1615
|
+
this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* The provisioning this factory's pool runs with — resolved from the pool's
|
|
1619
|
+
* OWN runtime plus any operator override. Read by the effective-tuning
|
|
1620
|
+
* readout and by the tests that pin the per-runtime behaviour.
|
|
1621
|
+
*/
|
|
1622
|
+
get provisioning() {
|
|
1623
|
+
return this.resolvedProvisioning;
|
|
1311
1624
|
}
|
|
1312
1625
|
/** Detection always uses the Python pool. */
|
|
1313
1626
|
get usesPythonPool() {
|
|
@@ -1409,40 +1722,51 @@ var EngineFactory = class {
|
|
|
1409
1722
|
this.poolManager = null;
|
|
1410
1723
|
}
|
|
1411
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* The one pool-init log line. `deviceKey` / `device` are the reason it
|
|
1727
|
+
* exists: the default pool used to log neither, so an operator reading a
|
|
1728
|
+
* day of logs could not tell whether the NPU was idle or was simply the
|
|
1729
|
+
* anonymous default pool.
|
|
1730
|
+
*
|
|
1731
|
+
* `overriddenFields` replaces the old `source` field. `source` claimed
|
|
1732
|
+
* `'auto-default' | 'user-override'` about the CONCURRENCY TUNING but read as
|
|
1733
|
+
* the provenance of the device/engine choice, and was `'user-override'` on
|
|
1734
|
+
* every pool because the addon always passed explicit tuning. This lists the
|
|
1735
|
+
* fields the operator actually overrode — empty means pure registry.
|
|
1736
|
+
*/
|
|
1737
|
+
logProvisioning() {
|
|
1738
|
+
const p = this.resolvedProvisioning;
|
|
1739
|
+
this.log.info("Python inference pool", { meta: {
|
|
1740
|
+
deviceKey: this.deviceKey,
|
|
1741
|
+
backend: this.opts.engine.backend,
|
|
1742
|
+
device: this.opts.engine.device ?? null,
|
|
1743
|
+
runtime: p.runtime,
|
|
1744
|
+
concurrency: p.concurrency,
|
|
1745
|
+
numWorkers: p.numWorkers,
|
|
1746
|
+
overriddenFields: p.overriddenFields,
|
|
1747
|
+
tuning: {
|
|
1748
|
+
batchMode: p.batchMode,
|
|
1749
|
+
windowMs: p.windowMs,
|
|
1750
|
+
maxBatchSize: p.maxBatchSize,
|
|
1751
|
+
numStreams: p.numStreams,
|
|
1752
|
+
intraOpThreads: p.intraOpThreads
|
|
1753
|
+
}
|
|
1754
|
+
} });
|
|
1755
|
+
}
|
|
1412
1756
|
async initPythonPool(steps) {
|
|
1757
|
+
this.logProvisioning();
|
|
1413
1758
|
const pythonPath = this.opts.pythonPath;
|
|
1414
1759
|
if (!pythonPath) throw new Error("EngineFactory: pythonPath is required — the addon must call ctx.deps.ensurePython() and pass the result. The embedded portable Python download likely failed; check the addon logs for the download error.");
|
|
1415
1760
|
const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
|
|
1416
1761
|
if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
|
|
1417
|
-
const concurrency = this.
|
|
1418
|
-
coreml: 1,
|
|
1419
|
-
openvino: 1,
|
|
1420
|
-
onnxruntime: 4,
|
|
1421
|
-
edgetpu: 1
|
|
1422
|
-
}[poolRuntime];
|
|
1423
|
-
const isAutoDefault = this.opts.concurrency === void 0;
|
|
1424
|
-
const defaultBatchMode = {
|
|
1425
|
-
coreml: "window",
|
|
1426
|
-
openvino: "none",
|
|
1427
|
-
onnxruntime: "list",
|
|
1428
|
-
edgetpu: "none"
|
|
1429
|
-
};
|
|
1430
|
-
const tuning = this.opts.tuning ?? {};
|
|
1762
|
+
const { concurrency, numWorkers } = this.resolvedProvisioning;
|
|
1431
1763
|
const resolvedTuning = {
|
|
1432
|
-
batchMode:
|
|
1433
|
-
windowMs:
|
|
1434
|
-
maxBatchSize:
|
|
1435
|
-
numStreams:
|
|
1436
|
-
intraOpThreads:
|
|
1764
|
+
batchMode: this.resolvedProvisioning.batchMode,
|
|
1765
|
+
windowMs: this.resolvedProvisioning.windowMs,
|
|
1766
|
+
maxBatchSize: this.resolvedProvisioning.maxBatchSize,
|
|
1767
|
+
numStreams: this.resolvedProvisioning.numStreams,
|
|
1768
|
+
intraOpThreads: this.resolvedProvisioning.intraOpThreads
|
|
1437
1769
|
};
|
|
1438
|
-
const numWorkers = Math.max(1, this.opts.numWorkers ?? 1);
|
|
1439
|
-
this.log.info("Python inference pool", { meta: {
|
|
1440
|
-
runtime: poolRuntime,
|
|
1441
|
-
concurrency,
|
|
1442
|
-
numWorkers,
|
|
1443
|
-
source: isAutoDefault ? "auto-default" : "user-override",
|
|
1444
|
-
tuning: resolvedTuning
|
|
1445
|
-
} });
|
|
1446
1770
|
this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
|
|
1447
1771
|
concurrency,
|
|
1448
1772
|
tuning: resolvedTuning,
|
|
@@ -1498,28 +1822,6 @@ var EngineFactory = class {
|
|
|
1498
1822
|
}
|
|
1499
1823
|
};
|
|
1500
1824
|
//#endregion
|
|
1501
|
-
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1502
|
-
/**
|
|
1503
|
-
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1504
|
-
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1505
|
-
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1506
|
-
* than silently building a wrong pool. Format comes from the shared
|
|
1507
|
-
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1508
|
-
*/
|
|
1509
|
-
function resolveDeviceEngine(deviceKey) {
|
|
1510
|
-
const colon = deviceKey.indexOf(":");
|
|
1511
|
-
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1512
|
-
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1513
|
-
const format = require_dist.DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1514
|
-
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1515
|
-
return {
|
|
1516
|
-
runtime: "python",
|
|
1517
|
-
backend,
|
|
1518
|
-
format,
|
|
1519
|
-
device
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
//#endregion
|
|
1523
1825
|
//#region src/detection-pipeline/engine/idle-pool-reaper.ts
|
|
1524
1826
|
var IdlePoolReaper = class {
|
|
1525
1827
|
lastUsed = /* @__PURE__ */ new Map();
|
|
@@ -4961,8 +5263,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4961
5263
|
modelsDir: this.modelsDir,
|
|
4962
5264
|
logger: this.log.child("engine-verify"),
|
|
4963
5265
|
pythonPath: this.executorOptions.pythonPath,
|
|
4964
|
-
|
|
4965
|
-
|
|
5266
|
+
provisioning: {
|
|
5267
|
+
concurrency: 1,
|
|
5268
|
+
numWorkers: 1
|
|
5269
|
+
}
|
|
4966
5270
|
});
|
|
4967
5271
|
try {
|
|
4968
5272
|
await probe.initialize([]);
|
|
@@ -5718,9 +6022,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5718
6022
|
modelsDir: this.modelsDir,
|
|
5719
6023
|
logger: this.log.child("engine-override"),
|
|
5720
6024
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5721
|
-
|
|
5722
|
-
tuning: this.executorOptions.tuning,
|
|
5723
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6025
|
+
provisioning: this.executorOptions.provisioning
|
|
5724
6026
|
});
|
|
5725
6027
|
const initPromise = newFactory.initialize([]);
|
|
5726
6028
|
this.overrideCache = {
|
|
@@ -5900,9 +6202,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5900
6202
|
modelsDir: this.modelsDir,
|
|
5901
6203
|
logger: this.log.child("engine-override"),
|
|
5902
6204
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5903
|
-
|
|
5904
|
-
tuning: this.executorOptions.tuning,
|
|
5905
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6205
|
+
provisioning: this.executorOptions.provisioning
|
|
5906
6206
|
});
|
|
5907
6207
|
const initPromise = newFactory.initialize([]);
|
|
5908
6208
|
this.overrideCache = {
|
|
@@ -6434,13 +6734,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6434
6734
|
if (!this.engineFactory) return;
|
|
6435
6735
|
await this.engineFactory.uncacheFrame(input.frameId);
|
|
6436
6736
|
}
|
|
6737
|
+
/**
|
|
6738
|
+
* The tuning of the node's DEFAULT pool. Per-device pools each resolve their
|
|
6739
|
+
* own runtime's row, so this is not "the node's tuning" — the per-pool truth
|
|
6740
|
+
* is on each pool's `Python inference pool` log line and in `listLoadedEngines`.
|
|
6741
|
+
*/
|
|
6437
6742
|
async getEffectiveTuning() {
|
|
6438
|
-
const
|
|
6743
|
+
const p = resolvePoolProvisioning(this.currentEngine, this.executorOptions.provisioning);
|
|
6439
6744
|
return {
|
|
6440
|
-
batchMode:
|
|
6441
|
-
windowMs:
|
|
6442
|
-
maxBatchSize:
|
|
6443
|
-
concurrency:
|
|
6745
|
+
batchMode: p.batchMode,
|
|
6746
|
+
windowMs: p.windowMs,
|
|
6747
|
+
maxBatchSize: p.maxBatchSize,
|
|
6748
|
+
concurrency: p.concurrency
|
|
6444
6749
|
};
|
|
6445
6750
|
}
|
|
6446
6751
|
/**
|
|
@@ -6458,9 +6763,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6458
6763
|
modelsDir: this.modelsDir,
|
|
6459
6764
|
logger: this.log.child("engine"),
|
|
6460
6765
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6461
|
-
|
|
6462
|
-
tuning: this.executorOptions.tuning,
|
|
6463
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6766
|
+
provisioning: this.executorOptions.provisioning
|
|
6464
6767
|
});
|
|
6465
6768
|
await this.engineFactory.initialize([]);
|
|
6466
6769
|
}
|
|
@@ -6499,13 +6802,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6499
6802
|
modelsDir: this.modelsDir,
|
|
6500
6803
|
logger: this.log.child(`engine:${deviceKey}`),
|
|
6501
6804
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6502
|
-
|
|
6503
|
-
tuning: this.executorOptions.tuning,
|
|
6504
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6805
|
+
provisioning: this.executorOptions.provisioning
|
|
6505
6806
|
});
|
|
6506
6807
|
try {
|
|
6507
6808
|
await factory.initialize([]);
|
|
6508
6809
|
} catch (err) {
|
|
6810
|
+
this.log.error("Device pool failed to initialize", { meta: {
|
|
6811
|
+
deviceKey,
|
|
6812
|
+
backend: engine.backend,
|
|
6813
|
+
device: engine.device ?? null,
|
|
6814
|
+
provisioning: factory.provisioning,
|
|
6815
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6816
|
+
} });
|
|
6509
6817
|
await factory.dispose().catch(() => void 0);
|
|
6510
6818
|
throw err;
|
|
6511
6819
|
}
|
|
@@ -7116,21 +7424,22 @@ function toNodePlatform(value) {
|
|
|
7116
7424
|
return value === "darwin" || value === "linux" || value === "win32" ? value : void 0;
|
|
7117
7425
|
}
|
|
7118
7426
|
/**
|
|
7119
|
-
*
|
|
7120
|
-
*
|
|
7121
|
-
*
|
|
7122
|
-
* these and rebuilds the provider when they flip.
|
|
7427
|
+
* Slider keys on the "Inference executor" section that map 1:1 onto a
|
|
7428
|
+
* {@link RuntimePoolProfile} field. Used to inject the node-default runtime's
|
|
7429
|
+
* registry value as the field's suggested default in `getGlobalSettings`.
|
|
7123
7430
|
*/
|
|
7124
|
-
var
|
|
7431
|
+
var PROFILE_SLIDER_KEYS = [
|
|
7125
7432
|
"concurrency",
|
|
7126
|
-
"
|
|
7433
|
+
"numWorkers",
|
|
7127
7434
|
"windowMs",
|
|
7128
7435
|
"maxBatchSize",
|
|
7129
7436
|
"numStreams",
|
|
7130
|
-
"intraOpThreads"
|
|
7131
|
-
"numWorkers"
|
|
7437
|
+
"intraOpThreads"
|
|
7132
7438
|
];
|
|
7133
|
-
|
|
7439
|
+
function isProfileSliderKey(key) {
|
|
7440
|
+
return PROFILE_SLIDER_KEYS.some((k) => k === key);
|
|
7441
|
+
}
|
|
7442
|
+
var DetectionPipelineAddon = class extends require_dist.BaseAddon {
|
|
7134
7443
|
provider = null;
|
|
7135
7444
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
7136
7445
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
@@ -7145,14 +7454,17 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7145
7454
|
* ENGINE_METRICS_HEARTBEAT_MS for liveness. */
|
|
7146
7455
|
lastEmittedEngineSnapshot = null;
|
|
7147
7456
|
/**
|
|
7148
|
-
* Last-applied
|
|
7149
|
-
*
|
|
7150
|
-
*
|
|
7151
|
-
*
|
|
7152
|
-
*
|
|
7153
|
-
*
|
|
7457
|
+
* Last-applied OPERATOR OVERRIDE, taken at the end of `onInitialize` and
|
|
7458
|
+
* refreshed at the end of `onConfigChanged`. Lets the change handler diff the
|
|
7459
|
+
* current config against the snapshot and decide whether a pool respawn is
|
|
7460
|
+
* necessary — most config edits (engine cascade, audio settings) don't touch
|
|
7461
|
+
* the tuning fields and don't deserve a multi-second model reload.
|
|
7462
|
+
*
|
|
7463
|
+
* The per-runtime registry values are compile-time constants resolved inside
|
|
7464
|
+
* each pool, so they can never be the reason for a respawn; only an operator
|
|
7465
|
+
* edit can.
|
|
7154
7466
|
*/
|
|
7155
|
-
|
|
7467
|
+
lastAppliedOverride = {};
|
|
7156
7468
|
/**
|
|
7157
7469
|
* Embedded Python path resolved once at boot via
|
|
7158
7470
|
* `ctx.deps.ensurePython()`. Empty string means the download failed
|
|
@@ -7320,17 +7632,16 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7320
7632
|
...section,
|
|
7321
7633
|
fields: section.fields.map((field) => {
|
|
7322
7634
|
if (field.type === "select") {
|
|
7323
|
-
if (field.key === "batchMode"
|
|
7635
|
+
if (field.key === "batchMode") return {
|
|
7324
7636
|
...field,
|
|
7325
|
-
default: tuning
|
|
7637
|
+
default: tuning.batchMode
|
|
7326
7638
|
};
|
|
7327
7639
|
}
|
|
7328
7640
|
if (field.type === "slider" && "key" in field) {
|
|
7329
|
-
const tuned = tuning[field.key];
|
|
7330
7641
|
const sliderField = field;
|
|
7331
|
-
let patchedField =
|
|
7642
|
+
let patchedField = isProfileSliderKey(sliderField.key) ? {
|
|
7332
7643
|
...sliderField,
|
|
7333
|
-
default:
|
|
7644
|
+
default: tuning[sliderField.key]
|
|
7334
7645
|
} : sliderField;
|
|
7335
7646
|
if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
|
|
7336
7647
|
...patchedField,
|
|
@@ -7439,60 +7750,39 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7439
7750
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7440
7751
|
}
|
|
7441
7752
|
/**
|
|
7442
|
-
*
|
|
7753
|
+
* The operator's explicit deviations from the per-runtime registry — and
|
|
7754
|
+
* NOTHING else.
|
|
7443
7755
|
*
|
|
7444
|
-
*
|
|
7445
|
-
*
|
|
7446
|
-
*
|
|
7447
|
-
*
|
|
7448
|
-
*
|
|
7756
|
+
* This used to be `resolveBackendTuning()`: it read `tuningFor(<the node's
|
|
7757
|
+
* default backend>)` and handed the whole resolved row to the provider, which
|
|
7758
|
+
* passed it verbatim to every pool it built. That is how an `openvino:npu`
|
|
7759
|
+
* pool on an onnx-default node came up with onnx's `concurrency: 4`. The
|
|
7760
|
+
* per-runtime values now resolve inside each pool
|
|
7761
|
+
* (`engine/pool-provisioning.ts`), keyed by that pool's own runtime, so
|
|
7762
|
+
* nothing backend-shaped may be computed here.
|
|
7449
7763
|
*
|
|
7450
|
-
*
|
|
7451
|
-
*
|
|
7452
|
-
*
|
|
7764
|
+
* Precedence, in full: an operator override below wins over the pool's
|
|
7765
|
+
* runtime registry row; an "Auto" field (slider `0`, select `''`) is not an
|
|
7766
|
+
* override. An override is node-wide by design — the operator set it against
|
|
7767
|
+
* a node, not against a runtime.
|
|
7453
7768
|
*/
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
|
|
7457
|
-
const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
|
|
7458
|
-
return {
|
|
7459
|
-
concurrency: num(t["concurrency"], 1),
|
|
7460
|
-
batchMode: batch(t["batchMode"], "window"),
|
|
7461
|
-
windowMs: num(t["windowMs"], 2),
|
|
7462
|
-
maxBatchSize: num(t["maxBatchSize"], 8),
|
|
7463
|
-
numStreams: num(t["numStreams"], 0),
|
|
7464
|
-
intraOpThreads: num(t["intraOpThreads"], 0),
|
|
7465
|
-
numWorkers: num(t["numWorkers"], 1)
|
|
7466
|
-
};
|
|
7769
|
+
resolveOperatorOverride() {
|
|
7770
|
+
return readOperatorOverride(this.config);
|
|
7467
7771
|
}
|
|
7468
7772
|
/**
|
|
7469
|
-
* Construct a fresh provider
|
|
7470
|
-
* truth for the provider wiring — shared by `onInitialize
|
|
7471
|
-
*
|
|
7773
|
+
* Construct a fresh provider carrying the operator override. Single source of
|
|
7774
|
+
* truth for the provider wiring — shared by `onInitialize` and the
|
|
7775
|
+
* `onConfigChanged` pool respawn. NOTHING backend-derived is passed: each
|
|
7776
|
+
* pool resolves its own runtime's registry row.
|
|
7472
7777
|
*/
|
|
7473
|
-
buildProvider(modelsDir,
|
|
7778
|
+
buildProvider(modelsDir, override) {
|
|
7474
7779
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7475
7780
|
return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|
|
7476
|
-
|
|
7477
|
-
tuning: {
|
|
7478
|
-
batchMode: tuning.batchMode,
|
|
7479
|
-
windowMs: tuning.windowMs,
|
|
7480
|
-
maxBatchSize: tuning.maxBatchSize,
|
|
7481
|
-
numStreams: tuning.numStreams,
|
|
7482
|
-
intraOpThreads: tuning.intraOpThreads
|
|
7483
|
-
},
|
|
7484
|
-
numWorkers: tuning.numWorkers,
|
|
7781
|
+
provisioning: override,
|
|
7485
7782
|
pythonPath: this.pythonPath,
|
|
7486
7783
|
pythonAddonDir: this.pythonAddonDir
|
|
7487
7784
|
});
|
|
7488
7785
|
}
|
|
7489
|
-
/**
|
|
7490
|
-
* Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
|
|
7491
|
-
* post-resolve / config-change respawn is actually needed).
|
|
7492
|
-
*/
|
|
7493
|
-
static poolTuningEqual(a, b) {
|
|
7494
|
-
return a.concurrency === b.concurrency && a.batchMode === b.batchMode && a.windowMs === b.windowMs && a.maxBatchSize === b.maxBatchSize && a.numStreams === b.numStreams && a.intraOpThreads === b.intraOpThreads && a.numWorkers === b.numWorkers;
|
|
7495
|
-
}
|
|
7496
7786
|
async onInitialize() {
|
|
7497
7787
|
const modelsDir = await this.resolveModelsDir();
|
|
7498
7788
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
@@ -7501,23 +7791,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7501
7791
|
if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
|
|
7502
7792
|
else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
|
|
7503
7793
|
await this.proactivelyInstallOpenvino();
|
|
7504
|
-
|
|
7505
|
-
this.provider = this.buildProvider(modelsDir, initialTuning);
|
|
7794
|
+
this.provider = this.buildProvider(modelsDir, this.resolveOperatorOverride());
|
|
7506
7795
|
await this.provider.init();
|
|
7507
7796
|
await this.provider.setApi(this.ctx);
|
|
7508
|
-
const resolvedTuning = this.resolveBackendTuning();
|
|
7509
|
-
if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
|
|
7510
|
-
this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
|
|
7511
|
-
from: initialTuning,
|
|
7512
|
-
to: resolvedTuning
|
|
7513
|
-
} });
|
|
7514
|
-
this.provider = this.buildProvider(modelsDir, resolvedTuning);
|
|
7515
|
-
await this.provider.init();
|
|
7516
|
-
await this.provider.setApi(this.ctx);
|
|
7517
|
-
}
|
|
7518
7797
|
await this.provider.warmPool();
|
|
7519
7798
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
7520
|
-
this.
|
|
7799
|
+
this.lastAppliedOverride = this.resolveOperatorOverride();
|
|
7521
7800
|
return [{
|
|
7522
7801
|
capability: require_dist.pipelineExecutorCapability,
|
|
7523
7802
|
provider: this.provider
|
|
@@ -7603,36 +7882,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7603
7882
|
}
|
|
7604
7883
|
}
|
|
7605
7884
|
/**
|
|
7606
|
-
* Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
|
|
7607
|
-
* `resolveBackendTuning`). Stored config values for these fields are
|
|
7608
|
-
* ignored, so this snapshot only changes when `engineBackend` flips
|
|
7609
|
-
* onto a different `tuningFor` row.
|
|
7610
|
-
*/
|
|
7611
|
-
snapshotPoolConfig() {
|
|
7612
|
-
const t = this.resolveBackendTuning();
|
|
7613
|
-
return {
|
|
7614
|
-
concurrency: t.concurrency ?? 0,
|
|
7615
|
-
batchMode: t.batchMode,
|
|
7616
|
-
windowMs: t.windowMs,
|
|
7617
|
-
maxBatchSize: t.maxBatchSize,
|
|
7618
|
-
numStreams: t.numStreams,
|
|
7619
|
-
intraOpThreads: t.intraOpThreads,
|
|
7620
|
-
numWorkers: t.numWorkers
|
|
7621
|
-
};
|
|
7622
|
-
}
|
|
7623
|
-
poolConfigChanged() {
|
|
7624
|
-
const next = this.snapshotPoolConfig();
|
|
7625
|
-
const prev = this.lastAppliedPoolConfig;
|
|
7626
|
-
for (const k of POOL_BOUND_KEYS) if (next[k] !== prev[k]) return true;
|
|
7627
|
-
return false;
|
|
7628
|
-
}
|
|
7629
|
-
/**
|
|
7630
7885
|
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7631
7886
|
* SOLE apply path for tuning changes — none of those fields declare
|
|
7632
7887
|
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7633
|
-
* every model on every set, which is exactly what we want to avoid):
|
|
7634
|
-
*
|
|
7635
|
-
*
|
|
7888
|
+
* every model on every set, which is exactly what we want to avoid): a changed
|
|
7889
|
+
* OPERATOR OVERRIDE drives an in-place pool respawn. The per-runtime registry
|
|
7890
|
+
* values cannot change at runtime, so they can never trigger one. The engine
|
|
7636
7891
|
* SELECTION is no longer operator-facing — the node-default backend is derived
|
|
7637
7892
|
* once at boot (`setApi` → `resolveAutoEngine`) from static hardware, so there
|
|
7638
7893
|
* is no per-write engine re-select. The respawn applies optimistically — the
|
|
@@ -7640,14 +7895,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7640
7895
|
* crashed), no addon bounce.
|
|
7641
7896
|
*/
|
|
7642
7897
|
async onConfigChanged() {
|
|
7643
|
-
|
|
7898
|
+
const next = this.resolveOperatorOverride();
|
|
7899
|
+
if (overridesEqual(next, this.lastAppliedOverride)) return;
|
|
7644
7900
|
if (!this.provider) {
|
|
7645
|
-
this.
|
|
7901
|
+
this.lastAppliedOverride = next;
|
|
7646
7902
|
return;
|
|
7647
7903
|
}
|
|
7648
|
-
this.ctx.logger.info("detection-pipeline tuning changed — respawning pool", { meta: {
|
|
7649
|
-
prev: this.
|
|
7650
|
-
next
|
|
7904
|
+
this.ctx.logger.info("detection-pipeline tuning override changed — respawning pool", { meta: {
|
|
7905
|
+
prev: this.lastAppliedOverride,
|
|
7906
|
+
next
|
|
7651
7907
|
} });
|
|
7652
7908
|
try {
|
|
7653
7909
|
await this.provider.shutdown();
|
|
@@ -7655,11 +7911,11 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends require_dist.B
|
|
|
7655
7911
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7656
7912
|
}
|
|
7657
7913
|
const modelsDir = await this.resolveModelsDir();
|
|
7658
|
-
this.provider = this.buildProvider(modelsDir,
|
|
7914
|
+
this.provider = this.buildProvider(modelsDir, next);
|
|
7659
7915
|
await this.provider.init();
|
|
7660
7916
|
await this.provider.setApi(this.ctx);
|
|
7661
7917
|
await this.provider.warmPool();
|
|
7662
|
-
this.
|
|
7918
|
+
this.lastAppliedOverride = next;
|
|
7663
7919
|
this.ctx.logger.info("detection-pipeline pool respawn complete");
|
|
7664
7920
|
}
|
|
7665
7921
|
};
|