@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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { n as __require } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-
|
|
3
|
-
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-
|
|
2
|
+
import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-Du7_SNiR.mjs";
|
|
3
|
+
import { a as getStepDefinition, i as getStep, n as ALL_STEPS, o as resolveModelForFormat, r as getDefaultModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-BkY9_IM6.mjs";
|
|
4
4
|
import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
|
|
5
5
|
import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
|
|
6
|
+
import * as os from "node:os";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
8
|
import sharp from "sharp";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
10
|
import * as path$1 from "node:path";
|
|
10
|
-
import * as os from "node:os";
|
|
11
11
|
//#region src/detection-pipeline/runtimes.ts
|
|
12
12
|
var KNOWN_PLATFORMS = [
|
|
13
13
|
"darwin",
|
|
@@ -73,14 +73,28 @@ function probedToHardwareInfo(hw) {
|
|
|
73
73
|
coral: coralInfoFrom(hw)
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* THE per-runtime pool provisioning registry — the single source for every
|
|
78
|
+
* pool's concurrency / worker count / batch tuning, keyed by the runtime THAT
|
|
79
|
+
* POOL runs. `engine-factory` used to carry a second, parallel copy of these
|
|
80
|
+
* defaults; it no longer does.
|
|
81
|
+
*
|
|
82
|
+
* `windowMs` / `maxBatchSize` are inert unless `batchMode !== 'none'`, and
|
|
83
|
+
* `numStreams` / `intraOpThreads` are inert at 0 (the runtime picks) — they are
|
|
84
|
+
* still spelled out per row so a reader never has to guess which default a
|
|
85
|
+
* missing key would have hit.
|
|
86
|
+
*/
|
|
76
87
|
var RUNTIME_DETAIL = {
|
|
77
88
|
onnx: {
|
|
78
89
|
label: "ONNX Runtime",
|
|
79
90
|
pythonRequirements: ["requirements.txt", "requirements-onnxruntime.txt"],
|
|
80
91
|
tuning: {
|
|
81
92
|
concurrency: 4,
|
|
93
|
+
numWorkers: 1,
|
|
82
94
|
batchMode: "list",
|
|
95
|
+
windowMs: 2,
|
|
83
96
|
maxBatchSize: 8,
|
|
97
|
+
numStreams: 0,
|
|
84
98
|
intraOpThreads: 0
|
|
85
99
|
}
|
|
86
100
|
},
|
|
@@ -89,8 +103,12 @@ var RUNTIME_DETAIL = {
|
|
|
89
103
|
pythonRequirements: ["requirements.txt", "requirements-openvino.txt"],
|
|
90
104
|
tuning: {
|
|
91
105
|
concurrency: 1,
|
|
106
|
+
numWorkers: 1,
|
|
92
107
|
batchMode: "none",
|
|
93
|
-
|
|
108
|
+
windowMs: 2,
|
|
109
|
+
maxBatchSize: 8,
|
|
110
|
+
numStreams: 0,
|
|
111
|
+
intraOpThreads: 0
|
|
94
112
|
}
|
|
95
113
|
},
|
|
96
114
|
coreml: {
|
|
@@ -98,10 +116,12 @@ var RUNTIME_DETAIL = {
|
|
|
98
116
|
pythonRequirements: ["requirements.txt", "requirements-coreml.txt"],
|
|
99
117
|
tuning: {
|
|
100
118
|
concurrency: 1,
|
|
119
|
+
numWorkers: 1,
|
|
101
120
|
batchMode: "none",
|
|
102
121
|
windowMs: 8,
|
|
103
122
|
maxBatchSize: 8,
|
|
104
|
-
|
|
123
|
+
numStreams: 0,
|
|
124
|
+
intraOpThreads: 0
|
|
105
125
|
}
|
|
106
126
|
},
|
|
107
127
|
edgetpu: {
|
|
@@ -109,8 +129,12 @@ var RUNTIME_DETAIL = {
|
|
|
109
129
|
pythonRequirements: ["requirements.txt", "requirements-edgetpu.txt"],
|
|
110
130
|
tuning: {
|
|
111
131
|
concurrency: 1,
|
|
132
|
+
numWorkers: 1,
|
|
112
133
|
batchMode: "none",
|
|
113
|
-
|
|
134
|
+
windowMs: 2,
|
|
135
|
+
maxBatchSize: 8,
|
|
136
|
+
numStreams: 0,
|
|
137
|
+
intraOpThreads: 0
|
|
114
138
|
}
|
|
115
139
|
}
|
|
116
140
|
};
|
|
@@ -153,6 +177,12 @@ function modelFormatFor(id) {
|
|
|
153
177
|
function pythonRequirementsFor(id) {
|
|
154
178
|
return RUNTIME_DETAIL[id].pythonRequirements;
|
|
155
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* The pool provisioning for ONE runtime. Callers must pass the runtime of the
|
|
182
|
+
* pool they are provisioning — never the node's default runtime. See
|
|
183
|
+
* `engine/pool-provisioning.ts` for the resolver that layers an operator
|
|
184
|
+
* override on top of this.
|
|
185
|
+
*/
|
|
156
186
|
function tuningFor(id) {
|
|
157
187
|
return RUNTIME_DETAIL[id].tuning;
|
|
158
188
|
}
|
|
@@ -371,6 +401,62 @@ var RAW_FMT_CODE = {
|
|
|
371
401
|
* large frame) never trips; override via env for constrained hardware.
|
|
372
402
|
*/
|
|
373
403
|
var POOL_INFER_TIMEOUT_MS = Math.max(1e3, Number(process.env["CAMSTACK_POOL_INFER_TIMEOUT_MS"]) || 6e4);
|
|
404
|
+
/**
|
|
405
|
+
* Max inference requests outstanding to ONE worker before new ones are SHED.
|
|
406
|
+
*
|
|
407
|
+
* Without this the camera dies permanently the first time the worker gets
|
|
408
|
+
* slow, and it did: device 615 produced nothing for 52 minutes on 2026-07-31.
|
|
409
|
+
* The worker was neither dead nor deadlocked — it was replying at full rate,
|
|
410
|
+
* about a minute behind, and every reply landed after {@link
|
|
411
|
+
* POOL_INFER_TIMEOUT_MS} had already abandoned its request. The proof is in the
|
|
412
|
+
* logs: a steady drip of `Response for unknown request id`, request ids
|
|
413
|
+
* climbing ~13/s, for as long as the outage lasted.
|
|
414
|
+
*
|
|
415
|
+
* That state is self-sustaining and cannot drain, because frames keep arriving
|
|
416
|
+
* while every one of them is written straight to the pipe. `dispatch` had NO
|
|
417
|
+
* bound: a transient slowdown (inference went 40ms → 2500ms across several
|
|
418
|
+
* cameras at 16:19 that day) buys a backlog that is never paid off, and once
|
|
419
|
+
* the backlog exceeds the deadline the loss is total.
|
|
420
|
+
*
|
|
421
|
+
* The queue is therefore capped in TIME, not just in count: at the normal ~40ms
|
|
422
|
+
* per inference this is ~0.3s of work, and even at a 20× slowdown it stays well
|
|
423
|
+
* inside the 60s deadline — so a worker that falls behind sheds, catches up,
|
|
424
|
+
* and recovers on its own. Python already sheds under its own per-model bound
|
|
425
|
+
* and answers `{dropped: true}`; this is the same contract applied one layer
|
|
426
|
+
* earlier, where the unbounded queue actually is.
|
|
427
|
+
*/
|
|
428
|
+
var POOL_MAX_IN_FLIGHT = Math.max(1, Number(process.env["CAMSTACK_POOL_MAX_IN_FLIGHT"]) || 8);
|
|
429
|
+
/**
|
|
430
|
+
* Reply deadline for a LIVE inference request (ms), as opposed to the generous
|
|
431
|
+
* {@link POOL_INFER_TIMEOUT_MS} that model loads and commands keep.
|
|
432
|
+
*
|
|
433
|
+
* 60 seconds was never a deadline for live detection — it is three orders of
|
|
434
|
+
* magnitude past the point where the answer is worth having. Frames arrive at
|
|
435
|
+
* ~13/s; a detection that lands 60s late describes a scene that no longer
|
|
436
|
+
* exists, and the only thing the wait accomplished was holding a queue slot
|
|
437
|
+
* while the backlog grew. Operator directive 2026-07-31: "a few seconds are
|
|
438
|
+
* already too many for this scenario".
|
|
439
|
+
*
|
|
440
|
+
* Three seconds is deliberately still loose — normal inference on this fleet is
|
|
441
|
+
* 35-47ms, so this trips only at a ~70× slowdown — but it bounds the damage at
|
|
442
|
+
* seconds instead of a minute, and combined with {@link POOL_MAX_IN_FLIGHT} it
|
|
443
|
+
* makes the backlog mathematically unable to outrun the deadline: the queue can
|
|
444
|
+
* never hold more than `cap × deadline` of work.
|
|
445
|
+
*
|
|
446
|
+
* Commands and model loads keep the long timeout on purpose: a cold model load
|
|
447
|
+
* legitimately takes tens of seconds, and shedding or timing one out
|
|
448
|
+
* desynchronizes the worker's model slots.
|
|
449
|
+
*/
|
|
450
|
+
var POOL_LIVE_INFER_TIMEOUT_MS = Math.max(250, Number(process.env["CAMSTACK_POOL_LIVE_INFER_TIMEOUT_MS"]) || 3e3);
|
|
451
|
+
/** Opcodes the in-flight cap may shed. Commands and frame-cache writes are
|
|
452
|
+
* NEVER shed: a dropped command silently desynchronizes model state, and a
|
|
453
|
+
* dropped `cacheFrame` strands the `inferCached` that follows it. */
|
|
454
|
+
var SHEDDABLE_MSG_TYPES = new Set([
|
|
455
|
+
MSG_INFER_JPEG,
|
|
456
|
+
MSG_INFER_RAW,
|
|
457
|
+
MSG_INFER_BATCH,
|
|
458
|
+
MSG_INFER_CACHED
|
|
459
|
+
]);
|
|
374
460
|
var PoolHandle = class {
|
|
375
461
|
pool;
|
|
376
462
|
modelIndex;
|
|
@@ -450,6 +536,13 @@ var PoolWorker = class {
|
|
|
450
536
|
process = null;
|
|
451
537
|
receiveBuffer = Buffer.alloc(0);
|
|
452
538
|
pending = /* @__PURE__ */ new Map();
|
|
539
|
+
/** True while the in-flight cap is shedding — so the warning fires on the
|
|
540
|
+
* EDGE rather than once per dropped frame. */
|
|
541
|
+
shedding = false;
|
|
542
|
+
/** Inference requests shed by the in-flight cap, for this worker's lifetime.
|
|
543
|
+
* Surfaced so "the camera is quiet" and "the camera is saturated" can be
|
|
544
|
+
* told apart without reading the logs. */
|
|
545
|
+
shedCount = 0;
|
|
453
546
|
nextRequestId = 1;
|
|
454
547
|
ready = false;
|
|
455
548
|
log;
|
|
@@ -461,6 +554,15 @@ var PoolWorker = class {
|
|
|
461
554
|
getPid() {
|
|
462
555
|
return this.process?.pid ?? null;
|
|
463
556
|
}
|
|
557
|
+
/** Requests written and not yet answered. This is the BACKLOG — the number
|
|
558
|
+
* nothing measured while a camera sat at zero frames for 52 minutes. */
|
|
559
|
+
getInFlight() {
|
|
560
|
+
return this.pending.size;
|
|
561
|
+
}
|
|
562
|
+
/** Inference shed by the in-flight cap over this worker's lifetime. */
|
|
563
|
+
getShedCount() {
|
|
564
|
+
return this.shedCount;
|
|
565
|
+
}
|
|
464
566
|
isReady() {
|
|
465
567
|
return this.ready;
|
|
466
568
|
}
|
|
@@ -609,10 +711,51 @@ var PoolWorker = class {
|
|
|
609
711
|
this.ready = false;
|
|
610
712
|
await terminateChild(proc, POOL_WORKER_TERM_GRACE_MS);
|
|
611
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* Shed an inference request when the worker already has {@link
|
|
716
|
+
* POOL_MAX_IN_FLIGHT} outstanding — the backlog is what turns a slow worker
|
|
717
|
+
* into a dead camera. Returns the shed response, or `null` to proceed.
|
|
718
|
+
*
|
|
719
|
+
* A shed is LOUD the first time and then rate-limited: at frame rate this
|
|
720
|
+
* fires ~13×/s, so logging every one would bury the signal it is meant to
|
|
721
|
+
* raise — but logging none is how a camera went dark for 52 minutes looking
|
|
722
|
+
* like a quiet afternoon.
|
|
723
|
+
*/
|
|
724
|
+
shedIfSaturated(msgType) {
|
|
725
|
+
if (!SHEDDABLE_MSG_TYPES.has(msgType)) return null;
|
|
726
|
+
if (this.pending.size < POOL_MAX_IN_FLIGHT) {
|
|
727
|
+
this.shedding = false;
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
if (!this.shedding) {
|
|
731
|
+
this.shedding = true;
|
|
732
|
+
this.log.warn("worker saturated — shedding inference until it catches up", { meta: {
|
|
733
|
+
worker: this.opts.workerLabel,
|
|
734
|
+
pid: this.getPid(),
|
|
735
|
+
runtime: this.opts.poolRuntime,
|
|
736
|
+
device: this.opts.device ?? "default",
|
|
737
|
+
inFlight: this.pending.size,
|
|
738
|
+
cap: POOL_MAX_IN_FLIGHT,
|
|
739
|
+
deadlineMs: POOL_LIVE_INFER_TIMEOUT_MS
|
|
740
|
+
} });
|
|
741
|
+
}
|
|
742
|
+
this.shedCount++;
|
|
743
|
+
return {
|
|
744
|
+
dropped: true,
|
|
745
|
+
shedReason: "worker-in-flight-cap"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/** Live inference gets seconds; commands and model loads keep the long
|
|
749
|
+
* timeout — see {@link POOL_LIVE_INFER_TIMEOUT_MS}. */
|
|
750
|
+
deadlineFor(msgType) {
|
|
751
|
+
return SHEDDABLE_MSG_TYPES.has(msgType) ? POOL_LIVE_INFER_TIMEOUT_MS : POOL_INFER_TIMEOUT_MS;
|
|
752
|
+
}
|
|
612
753
|
dispatch(msgType, payload) {
|
|
754
|
+
const shed = this.shedIfSaturated(msgType);
|
|
755
|
+
if (shed) return Promise.resolve(shed);
|
|
613
756
|
const reqId = this.allocRequestId();
|
|
614
757
|
return new Promise((resolve, reject) => {
|
|
615
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
758
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
616
759
|
this.pending.set(reqId, {
|
|
617
760
|
resolve,
|
|
618
761
|
reject,
|
|
@@ -632,24 +775,30 @@ var PoolWorker = class {
|
|
|
632
775
|
* `POOL_INFER_TIMEOUT_MS`. `unref` so it never keeps the event loop alive.
|
|
633
776
|
* The normal response + `rejectAll` paths clear it via `PendingRequest.timer`.
|
|
634
777
|
*/
|
|
635
|
-
armRequestTimeout(reqId, reject) {
|
|
778
|
+
armRequestTimeout(reqId, reject, timeoutMs) {
|
|
636
779
|
const timer = setTimeout(() => {
|
|
637
780
|
if (this.pending.delete(reqId)) {
|
|
638
781
|
this.log.error("inference request timed out", { meta: {
|
|
639
782
|
worker: this.opts.workerLabel,
|
|
783
|
+
pid: this.getPid(),
|
|
784
|
+
runtime: this.opts.poolRuntime,
|
|
785
|
+
device: this.opts.device ?? "default",
|
|
786
|
+
inFlight: this.pending.size,
|
|
640
787
|
reqId,
|
|
641
788
|
timeoutMs: POOL_INFER_TIMEOUT_MS
|
|
642
789
|
} });
|
|
643
|
-
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${
|
|
790
|
+
reject(/* @__PURE__ */ new Error(`PoolWorker[${this.opts.workerLabel}]: inference request ${reqId} timed out after ${timeoutMs}ms (worker alive, no reply)`));
|
|
644
791
|
}
|
|
645
|
-
},
|
|
792
|
+
}, timeoutMs);
|
|
646
793
|
timer.unref?.();
|
|
647
794
|
return timer;
|
|
648
795
|
}
|
|
649
796
|
dispatchMulti(msgType, payloadLen, parts) {
|
|
797
|
+
const shed = this.shedIfSaturated(msgType);
|
|
798
|
+
if (shed) return Promise.resolve(shed);
|
|
650
799
|
const reqId = this.allocRequestId();
|
|
651
800
|
return new Promise((resolve, reject) => {
|
|
652
|
-
const timer = this.armRequestTimeout(reqId, reject);
|
|
801
|
+
const timer = this.armRequestTimeout(reqId, reject, this.deadlineFor(msgType));
|
|
653
802
|
this.pending.set(reqId, {
|
|
654
803
|
resolve,
|
|
655
804
|
reject,
|
|
@@ -755,6 +904,22 @@ var SharedInferencePool = class {
|
|
|
755
904
|
}
|
|
756
905
|
device;
|
|
757
906
|
/** Pid of the first worker (for legacy callers). Use `getPids()` for all. */
|
|
907
|
+
/** Summed backlog and shed count across the pool's workers. A rising
|
|
908
|
+
* `inFlight` with a rising `shed` is a worker falling behind; a rising
|
|
909
|
+
* `inFlight` with zero `shed` and no results is the state that used to be
|
|
910
|
+
* invisible. */
|
|
911
|
+
getBacklog() {
|
|
912
|
+
let inFlight = 0;
|
|
913
|
+
let shed = 0;
|
|
914
|
+
for (const w of this.workers) {
|
|
915
|
+
inFlight += w.getInFlight();
|
|
916
|
+
shed += w.getShedCount();
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
inFlight,
|
|
920
|
+
shed
|
|
921
|
+
};
|
|
922
|
+
}
|
|
758
923
|
getPid() {
|
|
759
924
|
return this.workers[0]?.getPid() ?? null;
|
|
760
925
|
}
|
|
@@ -788,6 +953,7 @@ var SharedInferencePool = class {
|
|
|
788
953
|
loadedCount,
|
|
789
954
|
startupMs,
|
|
790
955
|
runtime: this.poolRuntime,
|
|
956
|
+
device: this.device ?? "default",
|
|
791
957
|
workers: this.numWorkers,
|
|
792
958
|
pythonWorkers: innerWorkers,
|
|
793
959
|
pids: this.getPids()
|
|
@@ -879,7 +1045,10 @@ var SharedInferencePool = class {
|
|
|
879
1045
|
async dispose() {
|
|
880
1046
|
await Promise.all(this.workers.map((w) => w.dispose()));
|
|
881
1047
|
this.workers.length = 0;
|
|
882
|
-
this.log.info("Pool process(es) terminated"
|
|
1048
|
+
this.log.info("Pool process(es) terminated", { meta: {
|
|
1049
|
+
runtime: this.poolRuntime,
|
|
1050
|
+
device: this.device ?? "default"
|
|
1051
|
+
} });
|
|
883
1052
|
}
|
|
884
1053
|
/**
|
|
885
1054
|
* Recognise a Python overload-shed response (`"dropped": true`) on the
|
|
@@ -892,7 +1061,9 @@ var SharedInferencePool = class {
|
|
|
892
1061
|
this.droppedResponseCount++;
|
|
893
1062
|
this.log.debug("Python pool shed frame under overload", { meta: {
|
|
894
1063
|
modelIndex,
|
|
895
|
-
droppedTotal: this.droppedResponseCount
|
|
1064
|
+
droppedTotal: this.droppedResponseCount,
|
|
1065
|
+
runtime: this.poolRuntime,
|
|
1066
|
+
device: this.device ?? "default"
|
|
896
1067
|
} });
|
|
897
1068
|
}
|
|
898
1069
|
return result;
|
|
@@ -1228,6 +1399,126 @@ var PipelineModelManager = class {
|
|
|
1228
1399
|
}
|
|
1229
1400
|
};
|
|
1230
1401
|
//#endregion
|
|
1402
|
+
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1403
|
+
/**
|
|
1404
|
+
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1405
|
+
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1406
|
+
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1407
|
+
* than silently building a wrong pool. Format comes from the shared
|
|
1408
|
+
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1409
|
+
*/
|
|
1410
|
+
function resolveDeviceEngine(deviceKey) {
|
|
1411
|
+
const colon = deviceKey.indexOf(":");
|
|
1412
|
+
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1413
|
+
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1414
|
+
const format = DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1415
|
+
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1416
|
+
return {
|
|
1417
|
+
runtime: "python",
|
|
1418
|
+
backend,
|
|
1419
|
+
format,
|
|
1420
|
+
device
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* The inverse of {@link resolveDeviceEngine}: the `<backend>:<device>` key that
|
|
1425
|
+
* names the pool an engine belongs to. Every pool-level log line carries this —
|
|
1426
|
+
* without it a default pool logs anonymously and six hours of logs cannot tell
|
|
1427
|
+
* "the NPU is unused" from "the NPU IS the anonymous default pool".
|
|
1428
|
+
* Falls back to the bare backend when the engine carries no device.
|
|
1429
|
+
*/
|
|
1430
|
+
function deviceKeyOf(engine) {
|
|
1431
|
+
return engine.device ? `${engine.backend}:${engine.device}` : engine.backend;
|
|
1432
|
+
}
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region src/detection-pipeline/engine/pool-provisioning.ts
|
|
1435
|
+
/** The fields a {@link PoolProvisioningOverride} may carry, in table order. */
|
|
1436
|
+
var OVERRIDABLE_KEYS = [
|
|
1437
|
+
"concurrency",
|
|
1438
|
+
"numWorkers",
|
|
1439
|
+
"batchMode",
|
|
1440
|
+
"windowMs",
|
|
1441
|
+
"maxBatchSize",
|
|
1442
|
+
"numStreams",
|
|
1443
|
+
"intraOpThreads"
|
|
1444
|
+
];
|
|
1445
|
+
/** Backends that are not a runtime of their own and run on onnxruntime. */
|
|
1446
|
+
var BACKEND_TO_RUNTIME = {
|
|
1447
|
+
onnx: "onnx",
|
|
1448
|
+
cpu: "onnx",
|
|
1449
|
+
cuda: "onnx",
|
|
1450
|
+
openvino: "openvino",
|
|
1451
|
+
coreml: "coreml",
|
|
1452
|
+
edgetpu: "edgetpu"
|
|
1453
|
+
};
|
|
1454
|
+
/**
|
|
1455
|
+
* The registry runtime a device-pool backend provisions from. Unknown backends
|
|
1456
|
+
* floor to `onnx` — the same floor `resolveDeviceEngine` applies to the model
|
|
1457
|
+
* format, so a pool never comes up unprovisioned.
|
|
1458
|
+
*/
|
|
1459
|
+
function runtimeIdForBackend(backend) {
|
|
1460
|
+
return BACKEND_TO_RUNTIME[backend] ?? "onnx";
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Resolve the provisioning for a pool running `engine`, applying the precedence
|
|
1464
|
+
* documented at the top of this module. `engine.device` is not consulted today
|
|
1465
|
+
* (see the module doc) but is part of the signature so a bench-backed
|
|
1466
|
+
* per-device row lands here and nowhere else.
|
|
1467
|
+
*/
|
|
1468
|
+
function resolvePoolProvisioning(engine, override = {}) {
|
|
1469
|
+
const runtime = runtimeIdForBackend(engine.backend);
|
|
1470
|
+
const profile = tuningFor(runtime);
|
|
1471
|
+
return {
|
|
1472
|
+
runtime,
|
|
1473
|
+
overriddenFields: OVERRIDABLE_KEYS.filter((k) => override[k] !== void 0),
|
|
1474
|
+
concurrency: Math.max(1, override.concurrency ?? profile.concurrency),
|
|
1475
|
+
numWorkers: Math.max(1, override.numWorkers ?? profile.numWorkers),
|
|
1476
|
+
batchMode: override.batchMode ?? profile.batchMode,
|
|
1477
|
+
windowMs: override.windowMs ?? profile.windowMs,
|
|
1478
|
+
maxBatchSize: override.maxBatchSize ?? profile.maxBatchSize,
|
|
1479
|
+
numStreams: override.numStreams ?? profile.numStreams,
|
|
1480
|
+
intraOpThreads: override.intraOpThreads ?? profile.intraOpThreads
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function positiveNumber(value) {
|
|
1484
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
1485
|
+
}
|
|
1486
|
+
function batchMode(value) {
|
|
1487
|
+
return value === "none" || value === "list" || value === "window" ? value : void 0;
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Read the operator override out of the detection addon's settings. Only a
|
|
1491
|
+
* NON-AUTO value counts: the sliders persist `0` and the select persists `''`
|
|
1492
|
+
* for "Auto (backend default)", and those must resolve to the runtime's
|
|
1493
|
+
* registry value, not to a literal 0 worker threads.
|
|
1494
|
+
*
|
|
1495
|
+
* The result is deliberately sparse — an empty object means "nothing
|
|
1496
|
+
* overridden", which is what lets a per-device pool provision from its own
|
|
1497
|
+
* runtime instead of inheriting the node's.
|
|
1498
|
+
*/
|
|
1499
|
+
function readOperatorOverride(settings) {
|
|
1500
|
+
const override = {};
|
|
1501
|
+
const concurrency = positiveNumber(settings.concurrency);
|
|
1502
|
+
if (concurrency !== void 0) override.concurrency = concurrency;
|
|
1503
|
+
const numWorkers = positiveNumber(settings.numWorkers);
|
|
1504
|
+
if (numWorkers !== void 0) override.numWorkers = numWorkers;
|
|
1505
|
+
const mode = batchMode(settings.batchMode);
|
|
1506
|
+
if (mode !== void 0) override.batchMode = mode;
|
|
1507
|
+
const windowMs = positiveNumber(settings.windowMs);
|
|
1508
|
+
if (windowMs !== void 0) override.windowMs = windowMs;
|
|
1509
|
+
const maxBatchSize = positiveNumber(settings.maxBatchSize);
|
|
1510
|
+
if (maxBatchSize !== void 0) override.maxBatchSize = maxBatchSize;
|
|
1511
|
+
const numStreams = positiveNumber(settings.numStreams);
|
|
1512
|
+
if (numStreams !== void 0) override.numStreams = numStreams;
|
|
1513
|
+
const intraOpThreads = positiveNumber(settings.intraOpThreads);
|
|
1514
|
+
if (intraOpThreads !== void 0) override.intraOpThreads = intraOpThreads;
|
|
1515
|
+
return override;
|
|
1516
|
+
}
|
|
1517
|
+
/** Two overrides carry the same fields with the same values. */
|
|
1518
|
+
function overridesEqual(a, b) {
|
|
1519
|
+
return OVERRIDABLE_KEYS.every((k) => a[k] === b[k]);
|
|
1520
|
+
}
|
|
1521
|
+
//#endregion
|
|
1231
1522
|
//#region src/detection-pipeline/engine/engine-factory.ts
|
|
1232
1523
|
var BACKEND_TO_POOL_RUNTIME = {
|
|
1233
1524
|
coreml: "coreml",
|
|
@@ -1297,9 +1588,31 @@ var EngineFactory = class {
|
|
|
1297
1588
|
poolManager = null;
|
|
1298
1589
|
log;
|
|
1299
1590
|
opts;
|
|
1591
|
+
/**
|
|
1592
|
+
* `<backend>:<device>` — on EVERY log line this factory writes. A pool that
|
|
1593
|
+
* logs anonymously cannot be told apart from another pool of the same runtime
|
|
1594
|
+
* on a different accelerator.
|
|
1595
|
+
*/
|
|
1596
|
+
deviceKey;
|
|
1597
|
+
/**
|
|
1598
|
+
* Resolved at CONSTRUCTION, not at pool spawn, so it is inspectable without
|
|
1599
|
+
* starting Python and so a pool that fails to spawn still logs what it was
|
|
1600
|
+
* trying to be.
|
|
1601
|
+
*/
|
|
1602
|
+
resolvedProvisioning;
|
|
1300
1603
|
constructor(opts) {
|
|
1301
1604
|
this.opts = opts;
|
|
1302
1605
|
this.log = opts.logger;
|
|
1606
|
+
this.deviceKey = deviceKeyOf(opts.engine);
|
|
1607
|
+
this.resolvedProvisioning = resolvePoolProvisioning(opts.engine, opts.provisioning);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* The provisioning this factory's pool runs with — resolved from the pool's
|
|
1611
|
+
* OWN runtime plus any operator override. Read by the effective-tuning
|
|
1612
|
+
* readout and by the tests that pin the per-runtime behaviour.
|
|
1613
|
+
*/
|
|
1614
|
+
get provisioning() {
|
|
1615
|
+
return this.resolvedProvisioning;
|
|
1303
1616
|
}
|
|
1304
1617
|
/** Detection always uses the Python pool. */
|
|
1305
1618
|
get usesPythonPool() {
|
|
@@ -1401,40 +1714,51 @@ var EngineFactory = class {
|
|
|
1401
1714
|
this.poolManager = null;
|
|
1402
1715
|
}
|
|
1403
1716
|
}
|
|
1717
|
+
/**
|
|
1718
|
+
* The one pool-init log line. `deviceKey` / `device` are the reason it
|
|
1719
|
+
* exists: the default pool used to log neither, so an operator reading a
|
|
1720
|
+
* day of logs could not tell whether the NPU was idle or was simply the
|
|
1721
|
+
* anonymous default pool.
|
|
1722
|
+
*
|
|
1723
|
+
* `overriddenFields` replaces the old `source` field. `source` claimed
|
|
1724
|
+
* `'auto-default' | 'user-override'` about the CONCURRENCY TUNING but read as
|
|
1725
|
+
* the provenance of the device/engine choice, and was `'user-override'` on
|
|
1726
|
+
* every pool because the addon always passed explicit tuning. This lists the
|
|
1727
|
+
* fields the operator actually overrode — empty means pure registry.
|
|
1728
|
+
*/
|
|
1729
|
+
logProvisioning() {
|
|
1730
|
+
const p = this.resolvedProvisioning;
|
|
1731
|
+
this.log.info("Python inference pool", { meta: {
|
|
1732
|
+
deviceKey: this.deviceKey,
|
|
1733
|
+
backend: this.opts.engine.backend,
|
|
1734
|
+
device: this.opts.engine.device ?? null,
|
|
1735
|
+
runtime: p.runtime,
|
|
1736
|
+
concurrency: p.concurrency,
|
|
1737
|
+
numWorkers: p.numWorkers,
|
|
1738
|
+
overriddenFields: p.overriddenFields,
|
|
1739
|
+
tuning: {
|
|
1740
|
+
batchMode: p.batchMode,
|
|
1741
|
+
windowMs: p.windowMs,
|
|
1742
|
+
maxBatchSize: p.maxBatchSize,
|
|
1743
|
+
numStreams: p.numStreams,
|
|
1744
|
+
intraOpThreads: p.intraOpThreads
|
|
1745
|
+
}
|
|
1746
|
+
} });
|
|
1747
|
+
}
|
|
1404
1748
|
async initPythonPool(steps) {
|
|
1749
|
+
this.logProvisioning();
|
|
1405
1750
|
const pythonPath = this.opts.pythonPath;
|
|
1406
1751
|
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.");
|
|
1407
1752
|
const poolRuntime = BACKEND_TO_POOL_RUNTIME[this.opts.engine.backend];
|
|
1408
1753
|
if (!poolRuntime) throw new Error(`No pool runtime mapping for backend "${this.opts.engine.backend}"`);
|
|
1409
|
-
const concurrency = this.
|
|
1410
|
-
coreml: 1,
|
|
1411
|
-
openvino: 1,
|
|
1412
|
-
onnxruntime: 4,
|
|
1413
|
-
edgetpu: 1
|
|
1414
|
-
}[poolRuntime];
|
|
1415
|
-
const isAutoDefault = this.opts.concurrency === void 0;
|
|
1416
|
-
const defaultBatchMode = {
|
|
1417
|
-
coreml: "window",
|
|
1418
|
-
openvino: "none",
|
|
1419
|
-
onnxruntime: "list",
|
|
1420
|
-
edgetpu: "none"
|
|
1421
|
-
};
|
|
1422
|
-
const tuning = this.opts.tuning ?? {};
|
|
1754
|
+
const { concurrency, numWorkers } = this.resolvedProvisioning;
|
|
1423
1755
|
const resolvedTuning = {
|
|
1424
|
-
batchMode:
|
|
1425
|
-
windowMs:
|
|
1426
|
-
maxBatchSize:
|
|
1427
|
-
numStreams:
|
|
1428
|
-
intraOpThreads:
|
|
1756
|
+
batchMode: this.resolvedProvisioning.batchMode,
|
|
1757
|
+
windowMs: this.resolvedProvisioning.windowMs,
|
|
1758
|
+
maxBatchSize: this.resolvedProvisioning.maxBatchSize,
|
|
1759
|
+
numStreams: this.resolvedProvisioning.numStreams,
|
|
1760
|
+
intraOpThreads: this.resolvedProvisioning.intraOpThreads
|
|
1429
1761
|
};
|
|
1430
|
-
const numWorkers = Math.max(1, this.opts.numWorkers ?? 1);
|
|
1431
|
-
this.log.info("Python inference pool", { meta: {
|
|
1432
|
-
runtime: poolRuntime,
|
|
1433
|
-
concurrency,
|
|
1434
|
-
numWorkers,
|
|
1435
|
-
source: isAutoDefault ? "auto-default" : "user-override",
|
|
1436
|
-
tuning: resolvedTuning
|
|
1437
|
-
} });
|
|
1438
1762
|
this.pool = new SharedInferencePool(pythonPath, poolRuntime, this.log.child("pool"), {
|
|
1439
1763
|
concurrency,
|
|
1440
1764
|
tuning: resolvedTuning,
|
|
@@ -1490,28 +1814,6 @@ var EngineFactory = class {
|
|
|
1490
1814
|
}
|
|
1491
1815
|
};
|
|
1492
1816
|
//#endregion
|
|
1493
|
-
//#region src/detection-pipeline/engine/resolve-device-engine.ts
|
|
1494
|
-
/**
|
|
1495
|
-
* Resolve a Phase-1 device key (`<backend>:<device>`, or bare `cpu`) into the
|
|
1496
|
-
* {@link PipelineEngineChoice} used to build that device's inference pool.
|
|
1497
|
-
* Throws on an unknown backend token so a bad `deviceKey` fails loud rather
|
|
1498
|
-
* than silently building a wrong pool. Format comes from the shared
|
|
1499
|
-
* {@link DEVICE_BACKEND_TO_FORMAT} SSOT (`@camstack/types`) — no local copy.
|
|
1500
|
-
*/
|
|
1501
|
-
function resolveDeviceEngine(deviceKey) {
|
|
1502
|
-
const colon = deviceKey.indexOf(":");
|
|
1503
|
-
const backend = colon >= 0 ? deviceKey.slice(0, colon) : deviceKey;
|
|
1504
|
-
const device = colon >= 0 ? deviceKey.slice(colon + 1) : deviceKey;
|
|
1505
|
-
const format = DEVICE_BACKEND_TO_FORMAT[backend];
|
|
1506
|
-
if (format === void 0) throw new Error(`resolveDeviceEngine: unknown backend "${backend}" in deviceKey "${deviceKey}"`);
|
|
1507
|
-
return {
|
|
1508
|
-
runtime: "python",
|
|
1509
|
-
backend,
|
|
1510
|
-
format,
|
|
1511
|
-
device
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
//#endregion
|
|
1515
1817
|
//#region src/detection-pipeline/engine/idle-pool-reaper.ts
|
|
1516
1818
|
var IdlePoolReaper = class {
|
|
1517
1819
|
lastUsed = /* @__PURE__ */ new Map();
|
|
@@ -4953,8 +5255,10 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
4953
5255
|
modelsDir: this.modelsDir,
|
|
4954
5256
|
logger: this.log.child("engine-verify"),
|
|
4955
5257
|
pythonPath: this.executorOptions.pythonPath,
|
|
4956
|
-
|
|
4957
|
-
|
|
5258
|
+
provisioning: {
|
|
5259
|
+
concurrency: 1,
|
|
5260
|
+
numWorkers: 1
|
|
5261
|
+
}
|
|
4958
5262
|
});
|
|
4959
5263
|
try {
|
|
4960
5264
|
await probe.initialize([]);
|
|
@@ -5710,9 +6014,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5710
6014
|
modelsDir: this.modelsDir,
|
|
5711
6015
|
logger: this.log.child("engine-override"),
|
|
5712
6016
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5713
|
-
|
|
5714
|
-
tuning: this.executorOptions.tuning,
|
|
5715
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6017
|
+
provisioning: this.executorOptions.provisioning
|
|
5716
6018
|
});
|
|
5717
6019
|
const initPromise = newFactory.initialize([]);
|
|
5718
6020
|
this.overrideCache = {
|
|
@@ -5892,9 +6194,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
5892
6194
|
modelsDir: this.modelsDir,
|
|
5893
6195
|
logger: this.log.child("engine-override"),
|
|
5894
6196
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
5895
|
-
|
|
5896
|
-
tuning: this.executorOptions.tuning,
|
|
5897
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6197
|
+
provisioning: this.executorOptions.provisioning
|
|
5898
6198
|
});
|
|
5899
6199
|
const initPromise = newFactory.initialize([]);
|
|
5900
6200
|
this.overrideCache = {
|
|
@@ -6426,13 +6726,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6426
6726
|
if (!this.engineFactory) return;
|
|
6427
6727
|
await this.engineFactory.uncacheFrame(input.frameId);
|
|
6428
6728
|
}
|
|
6729
|
+
/**
|
|
6730
|
+
* The tuning of the node's DEFAULT pool. Per-device pools each resolve their
|
|
6731
|
+
* own runtime's row, so this is not "the node's tuning" — the per-pool truth
|
|
6732
|
+
* is on each pool's `Python inference pool` log line and in `listLoadedEngines`.
|
|
6733
|
+
*/
|
|
6429
6734
|
async getEffectiveTuning() {
|
|
6430
|
-
const
|
|
6735
|
+
const p = resolvePoolProvisioning(this.currentEngine, this.executorOptions.provisioning);
|
|
6431
6736
|
return {
|
|
6432
|
-
batchMode:
|
|
6433
|
-
windowMs:
|
|
6434
|
-
maxBatchSize:
|
|
6435
|
-
concurrency:
|
|
6737
|
+
batchMode: p.batchMode,
|
|
6738
|
+
windowMs: p.windowMs,
|
|
6739
|
+
maxBatchSize: p.maxBatchSize,
|
|
6740
|
+
concurrency: p.concurrency
|
|
6436
6741
|
};
|
|
6437
6742
|
}
|
|
6438
6743
|
/**
|
|
@@ -6450,9 +6755,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6450
6755
|
modelsDir: this.modelsDir,
|
|
6451
6756
|
logger: this.log.child("engine"),
|
|
6452
6757
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6453
|
-
|
|
6454
|
-
tuning: this.executorOptions.tuning,
|
|
6455
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6758
|
+
provisioning: this.executorOptions.provisioning
|
|
6456
6759
|
});
|
|
6457
6760
|
await this.engineFactory.initialize([]);
|
|
6458
6761
|
}
|
|
@@ -6491,13 +6794,18 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
|
|
|
6491
6794
|
modelsDir: this.modelsDir,
|
|
6492
6795
|
logger: this.log.child(`engine:${deviceKey}`),
|
|
6493
6796
|
pythonPath: this.executorOptions.pythonPath ?? "",
|
|
6494
|
-
|
|
6495
|
-
tuning: this.executorOptions.tuning,
|
|
6496
|
-
numWorkers: this.executorOptions.numWorkers
|
|
6797
|
+
provisioning: this.executorOptions.provisioning
|
|
6497
6798
|
});
|
|
6498
6799
|
try {
|
|
6499
6800
|
await factory.initialize([]);
|
|
6500
6801
|
} catch (err) {
|
|
6802
|
+
this.log.error("Device pool failed to initialize", { meta: {
|
|
6803
|
+
deviceKey,
|
|
6804
|
+
backend: engine.backend,
|
|
6805
|
+
device: engine.device ?? null,
|
|
6806
|
+
provisioning: factory.provisioning,
|
|
6807
|
+
error: err instanceof Error ? err.message : String(err)
|
|
6808
|
+
} });
|
|
6501
6809
|
await factory.dispose().catch(() => void 0);
|
|
6502
6810
|
throw err;
|
|
6503
6811
|
}
|
|
@@ -7108,21 +7416,22 @@ function toNodePlatform(value) {
|
|
|
7108
7416
|
return value === "darwin" || value === "linux" || value === "win32" ? value : void 0;
|
|
7109
7417
|
}
|
|
7110
7418
|
/**
|
|
7111
|
-
*
|
|
7112
|
-
*
|
|
7113
|
-
*
|
|
7114
|
-
* these and rebuilds the provider when they flip.
|
|
7419
|
+
* Slider keys on the "Inference executor" section that map 1:1 onto a
|
|
7420
|
+
* {@link RuntimePoolProfile} field. Used to inject the node-default runtime's
|
|
7421
|
+
* registry value as the field's suggested default in `getGlobalSettings`.
|
|
7115
7422
|
*/
|
|
7116
|
-
var
|
|
7423
|
+
var PROFILE_SLIDER_KEYS = [
|
|
7117
7424
|
"concurrency",
|
|
7118
|
-
"
|
|
7425
|
+
"numWorkers",
|
|
7119
7426
|
"windowMs",
|
|
7120
7427
|
"maxBatchSize",
|
|
7121
7428
|
"numStreams",
|
|
7122
|
-
"intraOpThreads"
|
|
7123
|
-
"numWorkers"
|
|
7429
|
+
"intraOpThreads"
|
|
7124
7430
|
];
|
|
7125
|
-
|
|
7431
|
+
function isProfileSliderKey(key) {
|
|
7432
|
+
return PROFILE_SLIDER_KEYS.some((k) => k === key);
|
|
7433
|
+
}
|
|
7434
|
+
var DetectionPipelineAddon = class extends BaseAddon {
|
|
7126
7435
|
provider = null;
|
|
7127
7436
|
/** Last non-null probed hardware PER NODE — reused when the probe transiently
|
|
7128
7437
|
* returns null so offered backends / device lists don't collapse. Keyed by
|
|
@@ -7137,14 +7446,17 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7137
7446
|
* ENGINE_METRICS_HEARTBEAT_MS for liveness. */
|
|
7138
7447
|
lastEmittedEngineSnapshot = null;
|
|
7139
7448
|
/**
|
|
7140
|
-
* Last-applied
|
|
7141
|
-
*
|
|
7142
|
-
*
|
|
7143
|
-
*
|
|
7144
|
-
*
|
|
7145
|
-
*
|
|
7449
|
+
* Last-applied OPERATOR OVERRIDE, taken at the end of `onInitialize` and
|
|
7450
|
+
* refreshed at the end of `onConfigChanged`. Lets the change handler diff the
|
|
7451
|
+
* current config against the snapshot and decide whether a pool respawn is
|
|
7452
|
+
* necessary — most config edits (engine cascade, audio settings) don't touch
|
|
7453
|
+
* the tuning fields and don't deserve a multi-second model reload.
|
|
7454
|
+
*
|
|
7455
|
+
* The per-runtime registry values are compile-time constants resolved inside
|
|
7456
|
+
* each pool, so they can never be the reason for a respawn; only an operator
|
|
7457
|
+
* edit can.
|
|
7146
7458
|
*/
|
|
7147
|
-
|
|
7459
|
+
lastAppliedOverride = {};
|
|
7148
7460
|
/**
|
|
7149
7461
|
* Embedded Python path resolved once at boot via
|
|
7150
7462
|
* `ctx.deps.ensurePython()`. Empty string means the download failed
|
|
@@ -7312,17 +7624,16 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7312
7624
|
...section,
|
|
7313
7625
|
fields: section.fields.map((field) => {
|
|
7314
7626
|
if (field.type === "select") {
|
|
7315
|
-
if (field.key === "batchMode"
|
|
7627
|
+
if (field.key === "batchMode") return {
|
|
7316
7628
|
...field,
|
|
7317
|
-
default: tuning
|
|
7629
|
+
default: tuning.batchMode
|
|
7318
7630
|
};
|
|
7319
7631
|
}
|
|
7320
7632
|
if (field.type === "slider" && "key" in field) {
|
|
7321
|
-
const tuned = tuning[field.key];
|
|
7322
7633
|
const sliderField = field;
|
|
7323
|
-
let patchedField =
|
|
7634
|
+
let patchedField = isProfileSliderKey(sliderField.key) ? {
|
|
7324
7635
|
...sliderField,
|
|
7325
|
-
default:
|
|
7636
|
+
default: tuning[sliderField.key]
|
|
7326
7637
|
} : sliderField;
|
|
7327
7638
|
if (sliderField.key === "concurrency" && backend === "coreml") patchedField = {
|
|
7328
7639
|
...patchedField,
|
|
@@ -7431,60 +7742,39 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7431
7742
|
return normalizeEngineNodeId(this.ctxIfReady?.kernel?.localNodeId ?? "hub");
|
|
7432
7743
|
}
|
|
7433
7744
|
/**
|
|
7434
|
-
*
|
|
7745
|
+
* The operator's explicit deviations from the per-runtime registry — and
|
|
7746
|
+
* NOTHING else.
|
|
7435
7747
|
*
|
|
7436
|
-
*
|
|
7437
|
-
*
|
|
7438
|
-
*
|
|
7439
|
-
*
|
|
7440
|
-
*
|
|
7748
|
+
* This used to be `resolveBackendTuning()`: it read `tuningFor(<the node's
|
|
7749
|
+
* default backend>)` and handed the whole resolved row to the provider, which
|
|
7750
|
+
* passed it verbatim to every pool it built. That is how an `openvino:npu`
|
|
7751
|
+
* pool on an onnx-default node came up with onnx's `concurrency: 4`. The
|
|
7752
|
+
* per-runtime values now resolve inside each pool
|
|
7753
|
+
* (`engine/pool-provisioning.ts`), keyed by that pool's own runtime, so
|
|
7754
|
+
* nothing backend-shaped may be computed here.
|
|
7441
7755
|
*
|
|
7442
|
-
*
|
|
7443
|
-
*
|
|
7444
|
-
*
|
|
7756
|
+
* Precedence, in full: an operator override below wins over the pool's
|
|
7757
|
+
* runtime registry row; an "Auto" field (slider `0`, select `''`) is not an
|
|
7758
|
+
* override. An override is node-wide by design — the operator set it against
|
|
7759
|
+
* a node, not against a runtime.
|
|
7445
7760
|
*/
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
const num = (v, dflt) => typeof v === "number" && v > 0 ? v : dflt;
|
|
7449
|
-
const batch = (v, dflt) => v === "none" || v === "list" || v === "window" ? v : dflt;
|
|
7450
|
-
return {
|
|
7451
|
-
concurrency: num(t["concurrency"], 1),
|
|
7452
|
-
batchMode: batch(t["batchMode"], "window"),
|
|
7453
|
-
windowMs: num(t["windowMs"], 2),
|
|
7454
|
-
maxBatchSize: num(t["maxBatchSize"], 8),
|
|
7455
|
-
numStreams: num(t["numStreams"], 0),
|
|
7456
|
-
intraOpThreads: num(t["intraOpThreads"], 0),
|
|
7457
|
-
numWorkers: num(t["numWorkers"], 1)
|
|
7458
|
-
};
|
|
7761
|
+
resolveOperatorOverride() {
|
|
7762
|
+
return readOperatorOverride(this.config);
|
|
7459
7763
|
}
|
|
7460
7764
|
/**
|
|
7461
|
-
* Construct a fresh provider
|
|
7462
|
-
* truth for the provider wiring — shared by `onInitialize
|
|
7463
|
-
*
|
|
7765
|
+
* Construct a fresh provider carrying the operator override. Single source of
|
|
7766
|
+
* truth for the provider wiring — shared by `onInitialize` and the
|
|
7767
|
+
* `onConfigChanged` pool respawn. NOTHING backend-derived is passed: each
|
|
7768
|
+
* pool resolves its own runtime's registry row.
|
|
7464
7769
|
*/
|
|
7465
|
-
buildProvider(modelsDir,
|
|
7770
|
+
buildProvider(modelsDir, override) {
|
|
7466
7771
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
7467
7772
|
return new DetectionPipelineProvider(this.ctx.settings, modelsDir, this.ctx.logger, this.ctx.eventBus ?? null, () => ({ sections: [] }), {
|
|
7468
|
-
|
|
7469
|
-
tuning: {
|
|
7470
|
-
batchMode: tuning.batchMode,
|
|
7471
|
-
windowMs: tuning.windowMs,
|
|
7472
|
-
maxBatchSize: tuning.maxBatchSize,
|
|
7473
|
-
numStreams: tuning.numStreams,
|
|
7474
|
-
intraOpThreads: tuning.intraOpThreads
|
|
7475
|
-
},
|
|
7476
|
-
numWorkers: tuning.numWorkers,
|
|
7773
|
+
provisioning: override,
|
|
7477
7774
|
pythonPath: this.pythonPath,
|
|
7478
7775
|
pythonAddonDir: this.pythonAddonDir
|
|
7479
7776
|
});
|
|
7480
7777
|
}
|
|
7481
|
-
/**
|
|
7482
|
-
* Pool-bound tuning keys of `a` and `b` are equal (used to decide whether a
|
|
7483
|
-
* post-resolve / config-change respawn is actually needed).
|
|
7484
|
-
*/
|
|
7485
|
-
static poolTuningEqual(a, b) {
|
|
7486
|
-
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;
|
|
7487
|
-
}
|
|
7488
7778
|
async onInitialize() {
|
|
7489
7779
|
const modelsDir = await this.resolveModelsDir();
|
|
7490
7780
|
if (!this.ctx.settings) throw new Error("DetectionPipelineAddon: ctx.settings not available");
|
|
@@ -7493,23 +7783,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7493
7783
|
if (py.ok && py.pythonPath) this.pythonPath = py.pythonPath;
|
|
7494
7784
|
else this.ctx.logger.warn("Detection engine boot continues without Python — selection will provision on demand", {});
|
|
7495
7785
|
await this.proactivelyInstallOpenvino();
|
|
7496
|
-
|
|
7497
|
-
this.provider = this.buildProvider(modelsDir, initialTuning);
|
|
7786
|
+
this.provider = this.buildProvider(modelsDir, this.resolveOperatorOverride());
|
|
7498
7787
|
await this.provider.init();
|
|
7499
7788
|
await this.provider.setApi(this.ctx);
|
|
7500
|
-
const resolvedTuning = this.resolveBackendTuning();
|
|
7501
|
-
if (!DetectionPipelineAddon.poolTuningEqual(initialTuning, resolvedTuning)) {
|
|
7502
|
-
this.ctx.logger.info("detection-pipeline tuning reconciled to resolved engine", { meta: {
|
|
7503
|
-
from: initialTuning,
|
|
7504
|
-
to: resolvedTuning
|
|
7505
|
-
} });
|
|
7506
|
-
this.provider = this.buildProvider(modelsDir, resolvedTuning);
|
|
7507
|
-
await this.provider.init();
|
|
7508
|
-
await this.provider.setApi(this.ctx);
|
|
7509
|
-
}
|
|
7510
7789
|
await this.provider.warmPool();
|
|
7511
7790
|
this.engineMetricsTimer = setInterval(() => this.emitEngineMetricsSnapshot(), ENGINE_METRICS_SNAPSHOT_INTERVAL_MS);
|
|
7512
|
-
this.
|
|
7791
|
+
this.lastAppliedOverride = this.resolveOperatorOverride();
|
|
7513
7792
|
return [{
|
|
7514
7793
|
capability: pipelineExecutorCapability,
|
|
7515
7794
|
provider: this.provider
|
|
@@ -7595,36 +7874,12 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7595
7874
|
}
|
|
7596
7875
|
}
|
|
7597
7876
|
/**
|
|
7598
|
-
* Snapshot the pool-bound subset of the EFFECTIVE tuning (post-
|
|
7599
|
-
* `resolveBackendTuning`). Stored config values for these fields are
|
|
7600
|
-
* ignored, so this snapshot only changes when `engineBackend` flips
|
|
7601
|
-
* onto a different `tuningFor` row.
|
|
7602
|
-
*/
|
|
7603
|
-
snapshotPoolConfig() {
|
|
7604
|
-
const t = this.resolveBackendTuning();
|
|
7605
|
-
return {
|
|
7606
|
-
concurrency: t.concurrency ?? 0,
|
|
7607
|
-
batchMode: t.batchMode,
|
|
7608
|
-
windowMs: t.windowMs,
|
|
7609
|
-
maxBatchSize: t.maxBatchSize,
|
|
7610
|
-
numStreams: t.numStreams,
|
|
7611
|
-
intraOpThreads: t.intraOpThreads,
|
|
7612
|
-
numWorkers: t.numWorkers
|
|
7613
|
-
};
|
|
7614
|
-
}
|
|
7615
|
-
poolConfigChanged() {
|
|
7616
|
-
const next = this.snapshotPoolConfig();
|
|
7617
|
-
const prev = this.lastAppliedPoolConfig;
|
|
7618
|
-
for (const k of POOL_BOUND_KEYS) if (next[k] !== prev[k]) return true;
|
|
7619
|
-
return false;
|
|
7620
|
-
}
|
|
7621
|
-
/**
|
|
7622
7877
|
* BaseAddon calls `onConfigChanged` after every settings write. This is the
|
|
7623
7878
|
* SOLE apply path for tuning changes — none of those fields declare
|
|
7624
7879
|
* `requiresRestart` anymore (an addon restart re-probes hardware + reloads
|
|
7625
|
-
* every model on every set, which is exactly what we want to avoid):
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7880
|
+
* every model on every set, which is exactly what we want to avoid): a changed
|
|
7881
|
+
* OPERATOR OVERRIDE drives an in-place pool respawn. The per-runtime registry
|
|
7882
|
+
* values cannot change at runtime, so they can never trigger one. The engine
|
|
7628
7883
|
* SELECTION is no longer operator-facing — the node-default backend is derived
|
|
7629
7884
|
* once at boot (`setApi` → `resolveAutoEngine`) from static hardware, so there
|
|
7630
7885
|
* is no per-write engine re-select. The respawn applies optimistically — the
|
|
@@ -7632,14 +7887,15 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7632
7887
|
* crashed), no addon bounce.
|
|
7633
7888
|
*/
|
|
7634
7889
|
async onConfigChanged() {
|
|
7635
|
-
|
|
7890
|
+
const next = this.resolveOperatorOverride();
|
|
7891
|
+
if (overridesEqual(next, this.lastAppliedOverride)) return;
|
|
7636
7892
|
if (!this.provider) {
|
|
7637
|
-
this.
|
|
7893
|
+
this.lastAppliedOverride = next;
|
|
7638
7894
|
return;
|
|
7639
7895
|
}
|
|
7640
|
-
this.ctx.logger.info("detection-pipeline tuning changed — respawning pool", { meta: {
|
|
7641
|
-
prev: this.
|
|
7642
|
-
next
|
|
7896
|
+
this.ctx.logger.info("detection-pipeline tuning override changed — respawning pool", { meta: {
|
|
7897
|
+
prev: this.lastAppliedOverride,
|
|
7898
|
+
next
|
|
7643
7899
|
} });
|
|
7644
7900
|
try {
|
|
7645
7901
|
await this.provider.shutdown();
|
|
@@ -7647,11 +7903,11 @@ var DetectionPipelineAddon = class DetectionPipelineAddon extends BaseAddon {
|
|
|
7647
7903
|
this.ctx.logger.warn("provider shutdown failed during tuning respawn", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
7648
7904
|
}
|
|
7649
7905
|
const modelsDir = await this.resolveModelsDir();
|
|
7650
|
-
this.provider = this.buildProvider(modelsDir,
|
|
7906
|
+
this.provider = this.buildProvider(modelsDir, next);
|
|
7651
7907
|
await this.provider.init();
|
|
7652
7908
|
await this.provider.setApi(this.ctx);
|
|
7653
7909
|
await this.provider.warmPool();
|
|
7654
|
-
this.
|
|
7910
|
+
this.lastAppliedOverride = next;
|
|
7655
7911
|
this.ctx.logger.info("detection-pipeline pool respawn complete");
|
|
7656
7912
|
}
|
|
7657
7913
|
};
|