@camstack/addon-post-analysis 1.2.118 → 1.2.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-Gn2fFMov.mjs → dist-CsMG-R4E.mjs} +76 -22
- package/dist/{dist-CO6pe7bi.js → dist-jHCxxZ2_.js} +76 -22
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BYzwylnw.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-u9dpdQPa.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Kj0oid5Z.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-TlKH3NQ8.mjs → hostInit-nR5c9hhI.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +261 -15
- package/dist/pipeline-analytics/index.mjs +261 -15
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DlTk9-4b.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-jHCxxZ2_.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -4398,10 +4398,28 @@ var SCENE_LATCH_INDEXES = [{
|
|
|
4398
4398
|
function sceneKey(deviceId, monitorId) {
|
|
4399
4399
|
return `${deviceId}:${monitorId}`;
|
|
4400
4400
|
}
|
|
4401
|
+
/**
|
|
4402
|
+
* Reseed cap for BOTH scene ledgers.
|
|
4403
|
+
*
|
|
4404
|
+
* `DurableLedger` defaults to 100_000 and its own docblock says the owner "sets
|
|
4405
|
+
* it from the row set it expects". Neither scene ledger did, so the 30s
|
|
4406
|
+
* reconcile asked SQLite — through hub-main's event loop, which is the
|
|
4407
|
+
* cluster's only queue (D181) — for up to a hundred thousand rows twice a tick,
|
|
4408
|
+
* on a hub holding one scene and one latch (measured live 2026-08-25). A bound
|
|
4409
|
+
* nobody chose is not a bound.
|
|
4410
|
+
*
|
|
4411
|
+
* A scene is an operator-drawn monitor on ONE camera and a latch is at most one
|
|
4412
|
+
* row per scene, so the row set is `cameras x monitors-per-camera`. 5_000 is
|
|
4413
|
+
* ~5 monitors on each of a thousand cameras — far past any installation this
|
|
4414
|
+
* ships to, and still small enough that a runaway write loop is CAUGHT by the
|
|
4415
|
+
* bound instead of being paged in whole.
|
|
4416
|
+
*/
|
|
4417
|
+
var SCENE_LEDGER_LOAD_LIMIT = 5e3;
|
|
4401
4418
|
var SCENE_CONFIG_SPEC = {
|
|
4402
4419
|
collection: SCENE_COLLECTION,
|
|
4403
4420
|
columns: SCENE_COLUMNS,
|
|
4404
4421
|
indexes: SCENE_INDEXES,
|
|
4422
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4405
4423
|
writeMode: "write-behind",
|
|
4406
4424
|
keyOf: (row) => row.key,
|
|
4407
4425
|
toValue: (row) => ({
|
|
@@ -4417,6 +4435,7 @@ var SCENE_LATCH_SPEC = {
|
|
|
4417
4435
|
collection: SCENE_LATCH_COLLECTION,
|
|
4418
4436
|
columns: SCENE_LATCH_COLUMNS,
|
|
4419
4437
|
indexes: SCENE_LATCH_INDEXES,
|
|
4438
|
+
loadLimit: SCENE_LEDGER_LOAD_LIMIT,
|
|
4420
4439
|
writeMode: "write-through",
|
|
4421
4440
|
keyOf: (row) => row.key,
|
|
4422
4441
|
toValue: (row) => ({
|
|
@@ -4446,6 +4465,9 @@ var SceneStore = class {
|
|
|
4446
4465
|
config;
|
|
4447
4466
|
latch;
|
|
4448
4467
|
logger;
|
|
4468
|
+
/** `scenes/latches` as last announced. Null until the first load, so the
|
|
4469
|
+
* first one always speaks. */
|
|
4470
|
+
lastLoggedCounts = null;
|
|
4449
4471
|
constructor(deps) {
|
|
4450
4472
|
this.logger = deps.logger;
|
|
4451
4473
|
this.config = new DurableLedger({
|
|
@@ -4470,10 +4492,20 @@ var SceneStore = class {
|
|
|
4470
4492
|
* a container recreate, "loaded 0" is the one line that explains why every
|
|
4471
4493
|
* scene suddenly reads `unknown`, and its absence is what turns a data loss
|
|
4472
4494
|
* into a three-hour hunt.
|
|
4495
|
+
*
|
|
4496
|
+
* ON CHANGE ONLY, though — including the first load, where there is nothing
|
|
4497
|
+
* to compare against. This runs on the 30s `SCENE_RELOAD_INTERVAL_MS` tick on
|
|
4498
|
+
* every node, and it used to print unconditionally: measured live 2026-08-25,
|
|
4499
|
+
* `scene state loaded {scenes=1 latches=1}` every 30 seconds forever. A count
|
|
4500
|
+
* reprinted unchanged twice a minute is exactly how the "loaded 0" this line
|
|
4501
|
+
* exists for gets scrolled past.
|
|
4473
4502
|
*/
|
|
4474
4503
|
async load() {
|
|
4475
4504
|
const scenes = await this.config.load();
|
|
4476
4505
|
const latches = await this.latch.load();
|
|
4506
|
+
const counts = `${scenes.length}/${latches.length}`;
|
|
4507
|
+
if (counts === this.lastLoggedCounts) return;
|
|
4508
|
+
this.lastLoggedCounts = counts;
|
|
4477
4509
|
this.logger.info("scene state loaded", { meta: {
|
|
4478
4510
|
scenes: scenes.length,
|
|
4479
4511
|
latches: latches.length
|
|
@@ -8539,6 +8571,49 @@ var NC_LANGUAGES = ["en", "it"];
|
|
|
8539
8571
|
* surface's view all validate against this one, so a third language is added
|
|
8540
8572
|
* to `NC_LANGUAGES` and nowhere else. */
|
|
8541
8573
|
var NcLanguageSchema = require_dist._enum(NC_LANGUAGES);
|
|
8574
|
+
/**
|
|
8575
|
+
* Whether two snapshots say the same thing.
|
|
8576
|
+
*
|
|
8577
|
+
* ## Why this is a function and not `===`
|
|
8578
|
+
*
|
|
8579
|
+
* The reload guard in `reloadTexts()` used to be
|
|
8580
|
+
* `before.overrides === next.overrides`, and `next` comes from
|
|
8581
|
+
* `NcTextCatalogStore.load()`, which `parse()`s the row through zod on every
|
|
8582
|
+
* call. The right-hand side is therefore a NEW object on every tick by
|
|
8583
|
+
* construction: the comparison was `false` at byte-identical content, forever.
|
|
8584
|
+
* Measured on the live hub 2026-08-25: 58 "notification texts loaded" lines in
|
|
8585
|
+
* 30 minutes on a hub whose language had not changed, from a line whose own
|
|
8586
|
+
* docblock promises it fires on CHANGE only.
|
|
8587
|
+
*
|
|
8588
|
+
* The document is two levels deep and installation-small (at most one entry per
|
|
8589
|
+
* language, each a flat key→string map), so the comparison is written out
|
|
8590
|
+
* rather than reached for through a deep-equal dependency — and written out it
|
|
8591
|
+
* can state the direction that matters: a REMOVED key must not read as equal,
|
|
8592
|
+
* which a one-directional key walk would get wrong.
|
|
8593
|
+
*/
|
|
8594
|
+
function ncTextSnapshotsEqual(a, b) {
|
|
8595
|
+
if (a.language !== b.language) return false;
|
|
8596
|
+
return overrideMapsEqual(a.overrides, b.overrides);
|
|
8597
|
+
}
|
|
8598
|
+
function overrideMapsEqual(a, b) {
|
|
8599
|
+
const languages = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
8600
|
+
for (const language of languages) {
|
|
8601
|
+
const left = a[language];
|
|
8602
|
+
const right = b[language];
|
|
8603
|
+
if (left === void 0 || right === void 0) {
|
|
8604
|
+
if (Object.keys(left ?? right ?? {}).length > 0) return false;
|
|
8605
|
+
continue;
|
|
8606
|
+
}
|
|
8607
|
+
if (!flatMapsEqual(left, right)) return false;
|
|
8608
|
+
}
|
|
8609
|
+
return true;
|
|
8610
|
+
}
|
|
8611
|
+
function flatMapsEqual(a, b) {
|
|
8612
|
+
const keys = Object.keys(a);
|
|
8613
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
8614
|
+
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
8615
|
+
return true;
|
|
8616
|
+
}
|
|
8542
8617
|
/** The shipped files, keyed by language. Data, never mutated. */
|
|
8543
8618
|
var NC_TEXT_BUNDLES = {
|
|
8544
8619
|
en: en_default,
|
|
@@ -22484,7 +22559,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
22484
22559
|
const before = this.texts.snapshot;
|
|
22485
22560
|
const next = await this.textStore.load();
|
|
22486
22561
|
this.texts.apply(next);
|
|
22487
|
-
if (before
|
|
22562
|
+
if (ncTextSnapshotsEqual(before, next)) return;
|
|
22488
22563
|
this.logger.info("notification texts loaded", { meta: {
|
|
22489
22564
|
language: next.language,
|
|
22490
22565
|
overriddenKeys: Object.values(next.overrides).reduce((sum, keys) => sum + Object.keys(keys ?? {}).length, 0)
|
|
@@ -35459,6 +35534,18 @@ var EmbeddingDispatcher = class {
|
|
|
35459
35534
|
logger;
|
|
35460
35535
|
getRemoteFrame;
|
|
35461
35536
|
lastEmbedTime = /* @__PURE__ */ new Map();
|
|
35537
|
+
/**
|
|
35538
|
+
* trackId -> the frame pinned for it. Each entry holds a WHOLE decoded RGB
|
|
35539
|
+
* frame (`PendingCrop.frameData`, ~0.7 MB), not a crop.
|
|
35540
|
+
*
|
|
35541
|
+
* Under the shipped `cropStrategy: 'first'` this doubles as the in-flight
|
|
35542
|
+
* DEDUPE FLAG — `'first'` gates on `!pendingCrops.has(trackId)` — and there
|
|
35543
|
+
* is no flush timer for that strategy, so nothing ever sweeps it. Until
|
|
35544
|
+
* 2026-08-25 the delete lived on `processOne`'s success path only: an
|
|
35545
|
+
* `extractCrop` throw or an `encoder.encode` rejection (a cross-process cap
|
|
35546
|
+
* over UDS, with no timeout) stranded the frame permanently AND made that
|
|
35547
|
+
* trackId un-embeddable forever. `processOne` now releases in a `finally`.
|
|
35548
|
+
*/
|
|
35462
35549
|
pendingCrops = /* @__PURE__ */ new Map();
|
|
35463
35550
|
flushTimer = null;
|
|
35464
35551
|
unsubscribe = null;
|
|
@@ -35616,7 +35703,6 @@ var EmbeddingDispatcher = class {
|
|
|
35616
35703
|
this._processedCount++;
|
|
35617
35704
|
this._totalInferenceMs += inferenceMs;
|
|
35618
35705
|
this.lastEmbedTime.set(pending.deviceId, Date.now());
|
|
35619
|
-
this.pendingCrops.delete(pending.trackId);
|
|
35620
35706
|
const embeddingId = `${pending.deviceId}/${pending.trackId}/${Date.now()}`;
|
|
35621
35707
|
const payload = {
|
|
35622
35708
|
deviceId: Number(pending.deviceId),
|
|
@@ -35648,8 +35734,21 @@ var EmbeddingDispatcher = class {
|
|
|
35648
35734
|
error: String(err)
|
|
35649
35735
|
}
|
|
35650
35736
|
});
|
|
35737
|
+
} finally {
|
|
35738
|
+
if (this.pendingCrops.get(pending.trackId) === pending) this.pendingCrops.delete(pending.trackId);
|
|
35651
35739
|
}
|
|
35652
35740
|
}
|
|
35741
|
+
/**
|
|
35742
|
+
* Decoded frames currently pinned in {@link pendingCrops}.
|
|
35743
|
+
*
|
|
35744
|
+
* ~0.7 MB each (a whole RGB detection frame, not a crop), so this number IS
|
|
35745
|
+
* the dispatcher's memory footprint. Exposed because the entries are
|
|
35746
|
+
* invisible in every functional read: a stranded one looks exactly like a
|
|
35747
|
+
* track that was never detected.
|
|
35748
|
+
*/
|
|
35749
|
+
pendingFrameCount() {
|
|
35750
|
+
return this.pendingCrops.size;
|
|
35751
|
+
}
|
|
35653
35752
|
};
|
|
35654
35753
|
/**
|
|
35655
35754
|
* Re-embed every listed track from its key frame.
|
|
@@ -51407,10 +51506,16 @@ var BindingCache = class {
|
|
|
51407
51506
|
* `confirm` (S4) is the confirmation-gate re-detect crop: it GATES track birth
|
|
51408
51507
|
* (the frame path awaits it), so it ranks WITH `firstFrame` at 0 — it must
|
|
51409
51508
|
* jump every queued lower-priority capture and its call site never sets a
|
|
51410
|
-
* `staleAfterMs` (a late confirmation still confirms
|
|
51411
|
-
* fail-open a birth the operator asked to be gated). `face`/`plate` (S4) are
|
|
51509
|
+
* `staleAfterMs` (a late confirmation still confirms). `face`/`plate` (S4) are
|
|
51412
51510
|
* enrichment gallery crops — same rank as `embed`: nice-to-have media that
|
|
51413
51511
|
* must never displace the track-media ladder above.
|
|
51512
|
+
*
|
|
51513
|
+
* The admission cap sheds from the TAIL, so a `confirm` can only be shed when
|
|
51514
|
+
* every queued entry is priority 0 — and even then the outcome is safe rather
|
|
51515
|
+
* than fail-open: `fetchCrop` resolving `null` makes `evaluateBirth` return
|
|
51516
|
+
* `undecided(candidate, 'no-crop')`, which is a birth NOT confirmed. This note
|
|
51517
|
+
* used to say a dropped confirm would fail open; it does not, and that is what
|
|
51518
|
+
* makes shedding admissible at all.
|
|
51414
51519
|
*/
|
|
51415
51520
|
var DEFAULT_CAPTURE_PRIORITY = {
|
|
51416
51521
|
firstFrame: 0,
|
|
@@ -51444,9 +51549,14 @@ function deriveFrameDispatchKind(content) {
|
|
|
51444
51549
|
}
|
|
51445
51550
|
var DEFAULT_MAX_IN_FLIGHT_PER_DEVICE = 3;
|
|
51446
51551
|
var DEFAULT_LOG_WINDOW_MS = 6e4;
|
|
51552
|
+
/** One `capture queue full` line per device per this interval. The condition
|
|
51553
|
+
* holds for as long as the lane is saturated; one line per shed request would
|
|
51554
|
+
* be thousands. */
|
|
51555
|
+
var OVERFLOW_WARN_THROTTLE_MS = 6e4;
|
|
51447
51556
|
var CaptureScheduler = class {
|
|
51448
51557
|
logger;
|
|
51449
51558
|
budget;
|
|
51559
|
+
maxQueued;
|
|
51450
51560
|
windowMs;
|
|
51451
51561
|
now;
|
|
51452
51562
|
lanes = /* @__PURE__ */ new Map();
|
|
@@ -51455,9 +51565,12 @@ var CaptureScheduler = class {
|
|
|
51455
51565
|
/** holdKey → refcount of queued/running requests holding it. */
|
|
51456
51566
|
heldKeys = /* @__PURE__ */ new Map();
|
|
51457
51567
|
windows = /* @__PURE__ */ new Map();
|
|
51568
|
+
/** Per-device throttle for the `capture queue full` warn. */
|
|
51569
|
+
lastOverflowWarnAt = /* @__PURE__ */ new Map();
|
|
51458
51570
|
constructor(options) {
|
|
51459
51571
|
this.logger = options.logger;
|
|
51460
51572
|
this.budget = options.maxInFlightPerDevice ?? DEFAULT_MAX_IN_FLIGHT_PER_DEVICE;
|
|
51573
|
+
this.maxQueued = Math.max(1, options.maxQueuedPerDevice ?? 24);
|
|
51461
51574
|
this.windowMs = options.logWindowMs ?? DEFAULT_LOG_WINDOW_MS;
|
|
51462
51575
|
this.now = options.now ?? Date.now;
|
|
51463
51576
|
}
|
|
@@ -51527,6 +51640,7 @@ var CaptureScheduler = class {
|
|
|
51527
51640
|
failed: w.failed,
|
|
51528
51641
|
coalesced: w.coalesced,
|
|
51529
51642
|
droppedStale: w.droppedStale,
|
|
51643
|
+
droppedOverflow: w.droppedOverflow,
|
|
51530
51644
|
cancelled: w.cancelled,
|
|
51531
51645
|
maxQueueDepth: w.maxQueueDepth
|
|
51532
51646
|
};
|
|
@@ -51575,7 +51689,37 @@ var CaptureScheduler = class {
|
|
|
51575
51689
|
throw err;
|
|
51576
51690
|
});
|
|
51577
51691
|
}
|
|
51578
|
-
/**
|
|
51692
|
+
/**
|
|
51693
|
+
* Queue an over-budget request (priority-ordered, FIFO within priority),
|
|
51694
|
+
* under a BOUND enforced here rather than at dequeue.
|
|
51695
|
+
*
|
|
51696
|
+
* ## Why the queue needs its own bound
|
|
51697
|
+
*
|
|
51698
|
+
* `staleAfterMs` is evaluated in {@link pump}, and `pump` only touches the
|
|
51699
|
+
* queue while `inFlight < budget`. A lane that stays saturated therefore
|
|
51700
|
+
* never inspects its queue at all — nothing expires, nothing is shed, and the
|
|
51701
|
+
* queue grows for as long as the pressure lasts. The budget bounds concurrent
|
|
51702
|
+
* WORK; it has never bounded queued work.
|
|
51703
|
+
*
|
|
51704
|
+
* That gap is not theoretical: the per-frame batch dispatch PINS a ~0.7 MB
|
|
51705
|
+
* copy of the detection frame BEFORE queueing (so ring recycling cannot
|
|
51706
|
+
* invalidate it), and the queued closure holds that copy alive. Measured on
|
|
51707
|
+
* the live hub, `hub/pipeline-analytics` ran at rss=3911MB with heapUsed only
|
|
51708
|
+
* 153MB and arrayBuffers=2281MB — 2.28 GB of JS-reachable Buffers, roughly
|
|
51709
|
+
* 3 200 pinned frames, in a runner whose heap limit is 1216 MB. The call site
|
|
51710
|
+
* that pins them says the cost is "bounded by the lane"; it was not.
|
|
51711
|
+
*
|
|
51712
|
+
* Two things happen on the way in, in this order:
|
|
51713
|
+
*
|
|
51714
|
+
* 1. **expire what is already dead.** Entries past their `staleAfterMs` are
|
|
51715
|
+
* dropped now. They were doomed the moment they expired; `pump` simply
|
|
51716
|
+
* never got to look. This is the reclaim that costs nothing.
|
|
51717
|
+
* 2. **shed the least important.** Still over the cap, the TAIL goes — the
|
|
51718
|
+
* queue is ordered by priority, so the tail is by construction the work
|
|
51719
|
+
* this scheduler already ranked last. Whether that is the newcomer or an
|
|
51720
|
+
* older low-priority entry is decided by the same ordering that decides
|
|
51721
|
+
* execution, so a `firstFrame` never loses its place to an `embed`.
|
|
51722
|
+
*/
|
|
51579
51723
|
enqueue(lane, input) {
|
|
51580
51724
|
return new Promise((resolve, reject) => {
|
|
51581
51725
|
const entry = {
|
|
@@ -51588,11 +51732,53 @@ var CaptureScheduler = class {
|
|
|
51588
51732
|
},
|
|
51589
51733
|
drop: () => resolve(null)
|
|
51590
51734
|
};
|
|
51735
|
+
this.dropExpired(input.deviceId, lane);
|
|
51591
51736
|
this.insertByPriority(lane, entry);
|
|
51737
|
+
this.shedToCap(input.deviceId, lane);
|
|
51592
51738
|
const w = this.window(input.deviceId);
|
|
51593
51739
|
if (lane.queue.length > w.maxQueueDepth) w.maxQueueDepth = lane.queue.length;
|
|
51594
51740
|
});
|
|
51595
51741
|
}
|
|
51742
|
+
/** Drop every queued entry already past its `staleAfterMs`. Entries without
|
|
51743
|
+
* one (e.g. `confirm`, which gates track birth) are never expired here. */
|
|
51744
|
+
dropExpired(deviceId, lane) {
|
|
51745
|
+
const nowMs = this.now();
|
|
51746
|
+
for (let i = lane.queue.length - 1; i >= 0; i -= 1) {
|
|
51747
|
+
const entry = lane.queue[i];
|
|
51748
|
+
if (entry === void 0) continue;
|
|
51749
|
+
if (entry.staleAfterMs === void 0 || nowMs - entry.enqueuedAt <= entry.staleAfterMs) continue;
|
|
51750
|
+
lane.queue.splice(i, 1);
|
|
51751
|
+
this.bump(deviceId, "droppedStale");
|
|
51752
|
+
entry.drop();
|
|
51753
|
+
}
|
|
51754
|
+
}
|
|
51755
|
+
/** Shed from the tail (lowest priority, newest within it) until the lane is
|
|
51756
|
+
* at its cap. Loud: a capture that is dropped rather than merely late is a
|
|
51757
|
+
* missing thumbnail or first frame, and silence would read as "never asked". */
|
|
51758
|
+
shedToCap(deviceId, lane) {
|
|
51759
|
+
let shed = 0;
|
|
51760
|
+
while (lane.queue.length > this.maxQueued) {
|
|
51761
|
+
const victim = lane.queue.pop();
|
|
51762
|
+
if (victim === void 0) break;
|
|
51763
|
+
this.bump(deviceId, "droppedOverflow");
|
|
51764
|
+
victim.drop();
|
|
51765
|
+
shed += 1;
|
|
51766
|
+
}
|
|
51767
|
+
if (shed === 0) return;
|
|
51768
|
+
const nowMs = this.now();
|
|
51769
|
+
if (nowMs - (this.lastOverflowWarnAt.get(deviceId) ?? Number.NEGATIVE_INFINITY) < OVERFLOW_WARN_THROTTLE_MS) return;
|
|
51770
|
+
this.lastOverflowWarnAt.set(deviceId, nowMs);
|
|
51771
|
+
this.logger.warn("capture queue full — shedding the lowest-priority captures for this camera", {
|
|
51772
|
+
tags: { deviceId },
|
|
51773
|
+
meta: {
|
|
51774
|
+
shed,
|
|
51775
|
+
cap: this.maxQueued,
|
|
51776
|
+
queueDepth: lane.queue.length,
|
|
51777
|
+
inFlight: lane.inFlight,
|
|
51778
|
+
budget: this.budget
|
|
51779
|
+
}
|
|
51780
|
+
});
|
|
51781
|
+
}
|
|
51596
51782
|
insertByPriority(lane, entry) {
|
|
51597
51783
|
let index = lane.queue.length;
|
|
51598
51784
|
while (index > 0) {
|
|
@@ -51639,6 +51825,7 @@ var CaptureScheduler = class {
|
|
|
51639
51825
|
failed: 0,
|
|
51640
51826
|
coalesced: 0,
|
|
51641
51827
|
droppedStale: 0,
|
|
51828
|
+
droppedOverflow: 0,
|
|
51642
51829
|
cancelled: 0,
|
|
51643
51830
|
maxQueueDepth: 0
|
|
51644
51831
|
};
|
|
@@ -51663,6 +51850,7 @@ var CaptureScheduler = class {
|
|
|
51663
51850
|
failed: w.failed,
|
|
51664
51851
|
coalesced: w.coalesced,
|
|
51665
51852
|
droppedStale: w.droppedStale,
|
|
51853
|
+
droppedOverflow: w.droppedOverflow,
|
|
51666
51854
|
cancelled: w.cancelled,
|
|
51667
51855
|
maxQueueDepth: w.maxQueueDepth,
|
|
51668
51856
|
queueDepth: this.queueDepth(deviceId),
|
|
@@ -52751,6 +52939,36 @@ function buildAudioLabels(input) {
|
|
|
52751
52939
|
}];
|
|
52752
52940
|
}
|
|
52753
52941
|
//#endregion
|
|
52942
|
+
//#region src/pipeline-analytics/services/event-kinds-prefetch.ts
|
|
52943
|
+
/**
|
|
52944
|
+
* Resolve linked devices and bindings for `deviceIds` in two calls.
|
|
52945
|
+
*
|
|
52946
|
+
* Sequential by necessity, not by oversight: the binding set is cameras PLUS
|
|
52947
|
+
* everything linked to them, and the linked set is what the first call
|
|
52948
|
+
* returns.
|
|
52949
|
+
*
|
|
52950
|
+
* A camera or linked device missing from a batch answer resolves EMPTY rather
|
|
52951
|
+
* than throwing. Both batch endpoints answer for every id they were given, so
|
|
52952
|
+
* a gap here is a hub that answered a shorter array than it was asked for —
|
|
52953
|
+
* one camera loses its sensor lanes, never the whole request.
|
|
52954
|
+
*/
|
|
52955
|
+
async function prefetchDeviceTopology(api, deviceIds) {
|
|
52956
|
+
const ids = [...new Set(deviceIds)];
|
|
52957
|
+
const linkedRows = await api.getLinkedDevicesBatch({ deviceIds: ids });
|
|
52958
|
+
const linkedByDevice = new Map(linkedRows.map((row) => [row.deviceId, row]));
|
|
52959
|
+
const bindingTargets = new Set(ids);
|
|
52960
|
+
for (const row of linkedRows) for (const linked of row.devices) bindingTargets.add(linked.deviceId);
|
|
52961
|
+
const bindingRows = await api.getBindingsBatch({ deviceIds: [...bindingTargets] });
|
|
52962
|
+
const bindingsByDevice = new Map(bindingRows.map((row) => [row.deviceId, row]));
|
|
52963
|
+
return {
|
|
52964
|
+
linkedDevices: { getLinkedDevices: async ({ deviceId }) => linkedByDevice.get(deviceId) ?? {
|
|
52965
|
+
mode: "auto",
|
|
52966
|
+
devices: []
|
|
52967
|
+
} },
|
|
52968
|
+
bindings: { getBindings: async ({ deviceId }) => bindingsByDevice.get(deviceId) ?? { entries: [] } }
|
|
52969
|
+
};
|
|
52970
|
+
}
|
|
52971
|
+
//#endregion
|
|
52754
52972
|
//#region src/pipeline-analytics/services/event-kinds.ts
|
|
52755
52973
|
/**
|
|
52756
52974
|
* Extensible per-device event kinds (Part B).
|
|
@@ -53233,8 +53451,14 @@ var EventMediaDispatcher = class {
|
|
|
53233
53451
|
* scheduler request against a 3-slot device lane and can start seconds late,
|
|
53234
53452
|
* by which time the handle's ring slot is usually recycled and the by-handle
|
|
53235
53453
|
* resolve in {@link captureForFrame} finds nothing (the 2026-07-23 boxed-tile
|
|
53236
|
-
* regression). ~0.7MB per pinned frame, freed with the batch
|
|
53237
|
-
*
|
|
53454
|
+
* regression). ~0.7MB per pinned frame, freed with the batch.
|
|
53455
|
+
*
|
|
53456
|
+
* THE COPY IS ONLY AS BOUNDED AS THE QUEUE THAT HOLDS IT. This said "bounded
|
|
53457
|
+
* by the lane" while the lane bounded in-flight execs (3) and nothing at all
|
|
53458
|
+
* bounded the queue behind them, so a saturated camera accumulated pinned
|
|
53459
|
+
* frames without limit — 2.28 GB of `arrayBuffers` measured on the live hub.
|
|
53460
|
+
* Every caller must therefore pass `staleAfterMs`, and `CaptureScheduler`
|
|
53461
|
+
* caps queue depth per device. Returns `null` on a genuine live-window miss.
|
|
53238
53462
|
*
|
|
53239
53463
|
* Both miss paths are WARN, throttled to one line per device per minute. They
|
|
53240
53464
|
* are the FIRST half of a pair whose second half (`capture batch lost`) has
|
|
@@ -60708,6 +60932,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60708
60932
|
hasSnapshot: snapshotTargets.some((t) => t.appendSnapshot)
|
|
60709
60933
|
}),
|
|
60710
60934
|
holdKeys: heldCaptureKeys,
|
|
60935
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
60711
60936
|
exec: () => pinnedFramePromise.then((pinnedFrame) => dispatcher.captureForFrame({
|
|
60712
60937
|
deviceId,
|
|
60713
60938
|
frameHandle,
|
|
@@ -61637,7 +61862,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61637
61862
|
this.captureScheduler.request({
|
|
61638
61863
|
deviceId,
|
|
61639
61864
|
kind: "thumbnail",
|
|
61865
|
+
trackId,
|
|
61640
61866
|
holdKeys: [captureCoalesceKey(deviceId, "thumbnail", trackId)],
|
|
61867
|
+
staleAfterMs: ENRICHMENT_CAPTURE_STALE_MS,
|
|
61641
61868
|
exec: async () => {
|
|
61642
61869
|
const [pinnedFrame, media] = await Promise.all([pinnedFramePromise, this.resolveDeviceMediaSettings(deviceId)]);
|
|
61643
61870
|
const res = await dispatcher.captureForFrame({
|
|
@@ -63218,11 +63445,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63218
63445
|
/**
|
|
63219
63446
|
* `listEventKinds` for many cameras, in one call.
|
|
63220
63447
|
*
|
|
63221
|
-
* The saving is not
|
|
63222
|
-
*
|
|
63223
|
-
*
|
|
63224
|
-
*
|
|
63225
|
-
*
|
|
63448
|
+
* The saving is not the round-trips the CALLER avoids — it is the fan-out
|
|
63449
|
+
* this method used to do on its own. Composing one camera's vocabulary asks
|
|
63450
|
+
* device-manager for its status, its linked devices, and the bindings of
|
|
63451
|
+
* itself plus each linked device; asked per camera that was 1 + N + (N +
|
|
63452
|
+
* linked) cross-process calls. Measured on the live hub 2026-08-25 over this
|
|
63453
|
+
* deployment's 30 cameras: 106 calls, of which 30 concurrent
|
|
63454
|
+
* `getLinkedDevices` alone took 4110 ms — each one runs a full-fleet
|
|
63455
|
+
* `listAll` inside device-manager and thirty of them QUEUE.
|
|
63456
|
+
*
|
|
63457
|
+
* Now three calls for the whole set, whatever N is: one
|
|
63458
|
+
* `getCameraStatuses`, then `prefetchDeviceTopology`'s
|
|
63459
|
+
* `getLinkedDevicesBatch` + `getBindingsBatch`. `composeEventKinds` is
|
|
63460
|
+
* unchanged — it is handed the same per-device client interfaces, backed by
|
|
63461
|
+
* maps instead of RPC.
|
|
63226
63462
|
*
|
|
63227
63463
|
* A camera whose composition fails yields an EMPTY list rather than dropping
|
|
63228
63464
|
* out of the response: a caller that asked for twelve and got eleven cannot
|
|
@@ -63240,13 +63476,23 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63240
63476
|
});
|
|
63241
63477
|
const statusByDevice = new Map(statuses.map((st) => [st.deviceId, st]));
|
|
63242
63478
|
const detectionMacros = require_dist.MACRO_LABELS.map((l) => l.id).filter((id) => id !== "package");
|
|
63479
|
+
const topology = await prefetchDeviceTopology({
|
|
63480
|
+
getLinkedDevicesBatch: (i) => api.deviceManager.getLinkedDevicesBatch.query(i),
|
|
63481
|
+
getBindingsBatch: (i) => api.deviceManager.getBindingsBatch.query(i)
|
|
63482
|
+
}, deviceIds).catch((err) => {
|
|
63483
|
+
this.ctx.logger.warn("listEventKindsBatch: batched device topology unavailable — per-device fallback", { meta: {
|
|
63484
|
+
deviceCount: deviceIds.length,
|
|
63485
|
+
error: require_dist.errMsg(err)
|
|
63486
|
+
} });
|
|
63487
|
+
return null;
|
|
63488
|
+
});
|
|
63243
63489
|
return composeEventKindsForDevices(deviceIds, async (deviceId) => {
|
|
63244
63490
|
const status = statusByDevice.get(deviceId) ?? null;
|
|
63245
63491
|
const detectionOn = status?.detection != null;
|
|
63246
63492
|
const audioOn = status?.audio?.enabled === true;
|
|
63247
63493
|
return composeEventKinds({
|
|
63248
|
-
linkedDevices: { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
63249
|
-
bindings: { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
63494
|
+
linkedDevices: topology?.linkedDevices ?? { getLinkedDevices: (i) => api.deviceManager.getLinkedDevices.query(i) },
|
|
63495
|
+
bindings: topology?.bindings ?? { getBindings: (i) => api.deviceManager.getBindings.query(i) },
|
|
63250
63496
|
enabledMacroClasses: async () => detectionOn ? detectionMacros : [],
|
|
63251
63497
|
audioEnabled: async () => audioOn,
|
|
63252
63498
|
packageZonesEnabled: async (id) => {
|