@camstack/addon-pipeline 1.2.214 → 1.2.217
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 +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-C2vYSX3O.mjs → default-detection-model-BwNEV8Wl.mjs} +1 -1
- package/dist/{default-detection-model-f0PImZnh.js → default-detection-model-DzzuINe-.js} +1 -1
- package/dist/detection-pipeline/index.js +6 -7
- package/dist/detection-pipeline/index.mjs +5 -6
- package/dist/{dist-DoHcimdu.js → dist-D7hxiBnC.js} +237 -16
- package/dist/{dist-DttqKvsQ.mjs → dist-DgeXZfwn.mjs} +237 -16
- package/dist/{lazy-sharp-BeeQGCUu.js → lazy-sharp-BSkbiuz5.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-NtM7__8q.mjs → node-C9GcBrwk.mjs} +1 -1
- package/dist/{node-6uF72wri.js → node-DDhmPcOx.js} +1 -1
- package/dist/pipeline-runner/index.js +541 -26
- package/dist/pipeline-runner/index.mjs +539 -24
- package/dist/{process-memory-CddpCDWL.mjs → process-memory-Dw3CGS3D.mjs} +1 -1
- package/dist/{process-memory-BRdyl5fE.js → process-memory-ENqmiiis.js} +1 -1
- package/dist/recorder/index.js +158 -47
- package/dist/recorder/index.mjs +158 -47
- package/dist/{segment-demux-js-CsFtPVzH.mjs → segment-demux-js-BGbKlotF.mjs} +1 -1
- package/dist/{segment-demux-js-CkJPdyDH.js → segment-demux-js-sLs8mbZQ.js} +1 -1
- package/dist/session-decode/decode-worker-child.js +61 -180
- package/dist/session-decode/decode-worker-child.mjs +60 -179
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbBa4iV-.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BULotDCb.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-yJ2CyixO.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-Uxkwkgy5.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-CagSUJH7.mjs → hostInit-_RQxF72h.mjs} +3 -3
- package/dist/stream-broker/index.js +20 -21
- package/dist/stream-broker/index.mjs +20 -21
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BWO2WNjw.js → worker-protocol-DGql9R4g.js} +1 -1
- package/dist/{worker-protocol-BKwkLZWT.mjs → worker-protocol-SzmaLNnP.mjs} +1 -1
- package/package.json +1 -1
- package/dist/event-loop-stall-monitor-DXLNBMkY.mjs +0 -138
- package/dist/event-loop-stall-monitor-DaUBcb13.js +0 -149
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-0MF-KL1v.mjs +0 -26
|
@@ -2,14 +2,13 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
6
|
-
const require_node = require("../node-
|
|
7
|
-
const require_default_detection_model = require("../default-detection-model-
|
|
8
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
5
|
+
const require_dist = require("../dist-D7hxiBnC.js");
|
|
6
|
+
const require_node = require("../node-DDhmPcOx.js");
|
|
7
|
+
const require_default_detection_model = require("../default-detection-model-DzzuINe-.js");
|
|
8
|
+
const require_worker_protocol = require("../worker-protocol-DGql9R4g.js");
|
|
9
9
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
10
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
10
|
+
const require_lazy_sharp = require("../lazy-sharp-BSkbiuz5.js");
|
|
11
11
|
const require_restream_intent = require("../restream-intent-B1Difcnq.js");
|
|
12
|
-
const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-DaUBcb13.js");
|
|
13
12
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
14
13
|
let node_os = require("node:os");
|
|
15
14
|
node_os = require_dist.__toESM(node_os);
|
|
@@ -18,6 +17,7 @@ let sharp = require("sharp");
|
|
|
18
17
|
sharp = require_dist.__toESM(sharp);
|
|
19
18
|
let node_url = require("node:url");
|
|
20
19
|
let node_perf_hooks = require("node:perf_hooks");
|
|
20
|
+
let _camstack_system_addon_utils = require("@camstack/system/addon-utils");
|
|
21
21
|
//#region src/session-decode/fork-decode-worker.ts
|
|
22
22
|
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
23
23
|
var CHILD_ENTRY_PATH = (0, node_url.fileURLToPath)(new URL("../session-decode/decode-worker-child.js", require("url").pathToFileURL(__filename).href));
|
|
@@ -386,6 +386,84 @@ function positiveOrUndefined(value) {
|
|
|
386
386
|
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
387
387
|
}
|
|
388
388
|
//#endregion
|
|
389
|
+
//#region src/session-decode/native-crop-lease-age.ts
|
|
390
|
+
/**
|
|
391
|
+
* THE age bound on a native crop lease (ADR-0419).
|
|
392
|
+
*
|
|
393
|
+
* ## The measurement
|
|
394
|
+
*
|
|
395
|
+
* Mac agent node (`MinidiGianluca.localdomain`), 2026-09-09, a 54-minute
|
|
396
|
+
* window: 332 `native crop miss` lines, every one of them `worker-lease-gone`,
|
|
397
|
+
* at a MEDIAN `handleAgeMs` of 30 099 ms — thirty seconds. The node-wide
|
|
398
|
+
* {@link ../session-decode/native-crop-registry.NativeCropRegistry} sat pinned
|
|
399
|
+
* at `registrySize 4096 / registryCap 4096` for the whole window. Measured
|
|
400
|
+
* consequence: on track `d64ace88` (device 1443) the frontal-face frames never
|
|
401
|
+
* reached the face detector, because every native fetch missed.
|
|
402
|
+
*
|
|
403
|
+
* ## Why an AGE bound and not a bigger cap
|
|
404
|
+
*
|
|
405
|
+
* The pinned cap is what the miss line shows, so raising it is the instinctive
|
|
406
|
+
* move, and it is wrong twice. It keeps more full-resolution surfaces alive
|
|
407
|
+
* (real RAM, on a ring that has leaked a VAAPI surface pool before), and it
|
|
408
|
+
* only moves the threshold at which the same pinning returns. It also cannot
|
|
409
|
+
* work: the decode worker's own tile store expires a frame's tiles at
|
|
410
|
+
* `NATIVE_TILE_TTL_MS` (30 s), so a handle 30 s old has nothing behind it at
|
|
411
|
+
* ANY registry size. The operator's decision, 2026-09-09: **a native lease
|
|
412
|
+
* lives at most ten seconds**; a crop asked for later than that cannot be
|
|
413
|
+
* useful, and the caller's detection-frame fallback — which
|
|
414
|
+
* `native-frame-ring.ts` calls FREE and expected — is the right answer.
|
|
415
|
+
*
|
|
416
|
+
* ## Why the bound is enforced at the registry
|
|
417
|
+
*
|
|
418
|
+
* The registry is the only place in the system where a handle's registration
|
|
419
|
+
* wall-clock exists: `FrameHandle` carries no correlatable clock (`pts` is
|
|
420
|
+
* explicitly process-local and must never be compared against another), so
|
|
421
|
+
* neither the hub-side requester nor the decode worker can decide this
|
|
422
|
+
* question correctly. It is also the single node-wide chokepoint every native
|
|
423
|
+
* path funnels through — the `getNativeCrop` cap, `getNativeCropForDetail`,
|
|
424
|
+
* and `runDetailSubtree`'s rung 1 — so ONE rule covers all of them, and it
|
|
425
|
+
* sits in-process, in FRONT of the round trip that the 332 misses paid for.
|
|
426
|
+
* Refusing here makes the wasted work impossible rather than merely faster.
|
|
427
|
+
*
|
|
428
|
+
* Deliberately a constant and not an operator knob: it is a property of the
|
|
429
|
+
* pipeline, not of a node's hardware (same argument as the lease knobs in
|
|
430
|
+
* `@camstack/types` `pipeline/native-lease.ts`), and a per-node value would
|
|
431
|
+
* make the same camera produce different crop quality depending on where the
|
|
432
|
+
* balancer placed it.
|
|
433
|
+
*/
|
|
434
|
+
/**
|
|
435
|
+
* How long a registered handle may be asked for a native crop. Inclusive: an
|
|
436
|
+
* ask at exactly this age is still served, one millisecond later is refused.
|
|
437
|
+
*
|
|
438
|
+
* ## Ten seconds was tried on the fleet, and the fleet refused it
|
|
439
|
+
*
|
|
440
|
+
* The operator's number was ten seconds, chosen on the belief — stated in this
|
|
441
|
+
* file's first version, and sourced from `@camstack/types`
|
|
442
|
+
* `pipeline/native-lease.ts` sizing prose rather than from a measurement of
|
|
443
|
+
* THIS path — that a p90 ask lands at 4.3 s and a p50 at 367 ms. Deployed
|
|
444
|
+
* 2026-09-09 ~14:00, it produced, on the hub, **3 371 `no-registry-entry`
|
|
445
|
+
* misses in fifteen minutes** (2 036 of them on `salone` alone) against
|
|
446
|
+
* essentially ZERO in the half hour before the deploy: the registry held 46
|
|
447
|
+
* entries where it had held 4 096, and the asks arriving between 10 s and 30 s
|
|
448
|
+
* found nothing at all. The operator saw it as tracks with missing media.
|
|
449
|
+
*
|
|
450
|
+
* So the bound is the worker's own tile TTL (`NATIVE_TILE_TTL_MS`, 30 s,
|
|
451
|
+
* `decode-worker-child.ts`) — the age past which there is provably nothing
|
|
452
|
+
* behind the handle at ANY registry size. It still bounds the lease by AGE
|
|
453
|
+
* rather than by a 4 096-entry capacity eviction, which is what D419 set out
|
|
454
|
+
* to fix; it simply stops refusing asks the worker could still have answered.
|
|
455
|
+
*
|
|
456
|
+
* The real defect is caller-side and is not fixed here: a ~1 Hz
|
|
457
|
+
* `TrackDetailDispatcher` re-asks a frozen handle for the life of a coasted
|
|
458
|
+
* track, and `persistKeyFrames` / `offerGroupShot` are the only capture sites
|
|
459
|
+
* that omit `staleAfterMs`. ADR-0419 named both as open; until they are fixed
|
|
460
|
+
* a tighter bound cannot be told apart from a broken one.
|
|
461
|
+
*/
|
|
462
|
+
var NATIVE_CROP_LEASE_MAX_AGE_MS = 3e4;
|
|
463
|
+
function classifyNativeCropLeaseAge(ageMs, maxAgeMs = NATIVE_CROP_LEASE_MAX_AGE_MS) {
|
|
464
|
+
return ageMs > maxAgeMs ? "expired" : "live";
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
389
467
|
//#region src/session-decode/native-crop-registry.ts
|
|
390
468
|
var DEFAULT_NATIVE_CROP_REGISTRY_CAP = (() => {
|
|
391
469
|
const raw = Number(process.env["CAMSTACK_NATIVE_CROP_REGISTRY_CAP"]);
|
|
@@ -399,9 +477,11 @@ function safeRelease(request) {
|
|
|
399
477
|
}
|
|
400
478
|
var NativeCropRegistry = class {
|
|
401
479
|
capacity;
|
|
480
|
+
maxAgeMs;
|
|
402
481
|
entries = /* @__PURE__ */ new Map();
|
|
403
|
-
constructor(capacity = DEFAULT_NATIVE_CROP_REGISTRY_CAP) {
|
|
482
|
+
constructor(capacity = DEFAULT_NATIVE_CROP_REGISTRY_CAP, maxAgeMs = NATIVE_CROP_LEASE_MAX_AGE_MS) {
|
|
404
483
|
this.capacity = capacity;
|
|
484
|
+
this.maxAgeMs = maxAgeMs;
|
|
405
485
|
}
|
|
406
486
|
/** Number of registered handles (tests / metrics). */
|
|
407
487
|
get size() {
|
|
@@ -416,6 +496,7 @@ var NativeCropRegistry = class {
|
|
|
416
496
|
* its worker-side RAM lease so a lease never outlives its registry entry.
|
|
417
497
|
*/
|
|
418
498
|
register(handle, request, deviceId) {
|
|
499
|
+
this.sweepExpired();
|
|
419
500
|
const k = this.key(handle);
|
|
420
501
|
this.entries.delete(k);
|
|
421
502
|
this.entries.set(k, {
|
|
@@ -426,27 +507,76 @@ var NativeCropRegistry = class {
|
|
|
426
507
|
while (this.entries.size > this.capacity) {
|
|
427
508
|
const oldest = this.entries.keys().next().value;
|
|
428
509
|
if (oldest === void 0) break;
|
|
429
|
-
this.releaseEntry(oldest);
|
|
510
|
+
this.releaseEntry(oldest, "capacity");
|
|
430
511
|
}
|
|
431
512
|
}
|
|
432
|
-
/**
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Resolve `handle`, applying the age bound (ADR-0419).
|
|
515
|
+
*
|
|
516
|
+
* The bound is applied HERE and not at the caller or the worker because this
|
|
517
|
+
* is the only place `registeredAt` exists — see `native-crop-lease-age.ts`.
|
|
518
|
+
* An expired handle is refused and released without the requester ever being
|
|
519
|
+
* invoked, so the doomed cross-process fetch does not happen at all.
|
|
520
|
+
*/
|
|
521
|
+
lookup(handle) {
|
|
522
|
+
const key = this.key(handle);
|
|
523
|
+
const entry = this.entries.get(key);
|
|
524
|
+
if (entry === void 0) {
|
|
525
|
+
const swept = this.sweptByAge.get(key);
|
|
526
|
+
if (swept === void 0) return { kind: "unknown" };
|
|
527
|
+
return {
|
|
528
|
+
kind: "expired",
|
|
529
|
+
deviceId: swept.deviceId,
|
|
530
|
+
ageMs: Date.now() - swept.registeredAt
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const ageMs = Date.now() - entry.registeredAt;
|
|
534
|
+
if (classifyNativeCropLeaseAge(ageMs, this.maxAgeMs) === "expired") {
|
|
535
|
+
this.releaseEntry(key, "age");
|
|
536
|
+
return {
|
|
537
|
+
kind: "expired",
|
|
538
|
+
deviceId: entry.deviceId,
|
|
539
|
+
ageMs
|
|
540
|
+
};
|
|
541
|
+
}
|
|
436
542
|
return {
|
|
543
|
+
kind: "live",
|
|
437
544
|
request: entry.request,
|
|
438
545
|
deviceId: entry.deviceId,
|
|
439
|
-
ageMs
|
|
546
|
+
ageMs
|
|
440
547
|
};
|
|
441
548
|
}
|
|
442
549
|
/**
|
|
550
|
+
* Drop every entry past the age bound, oldest first.
|
|
551
|
+
*
|
|
552
|
+
* The map is insertion-ordered and insertion order IS registration order, so
|
|
553
|
+
* the head is the oldest and the walk stops at the first live entry. Run on
|
|
554
|
+
* every `register`, which is the only path that reliably ticks on a busy
|
|
555
|
+
* node — a timer here would be a second owner of the same lifetime.
|
|
556
|
+
*
|
|
557
|
+
* It can never drop an entry a request would have been served from: anything
|
|
558
|
+
* it releases would have been refused by {@link lookup} in the same instant.
|
|
559
|
+
* What it buys is that the worker's lease is released when it stops being
|
|
560
|
+
* useful (10 s) instead of when 4 095 later handles have pushed it out of a
|
|
561
|
+
* capacity eviction (~30 s on this cluster) — the reason the registry sat
|
|
562
|
+
* pinned at `4096/4096`.
|
|
563
|
+
*/
|
|
564
|
+
sweepExpired() {
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
for (const [key, entry] of this.entries) {
|
|
567
|
+
if (classifyNativeCropLeaseAge(now - entry.registeredAt, this.maxAgeMs) === "live") return;
|
|
568
|
+
this.releaseEntry(key, "age");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
443
572
|
* The deviceId a handle was registered under, even AFTER its entry was
|
|
444
573
|
* evicted — kept in a small parallel ring so a `no-registry-entry` miss can
|
|
445
574
|
* still be attributed to a camera (a miss line without `deviceId` cannot
|
|
446
575
|
* answer "why is 617 worse than 615?").
|
|
447
576
|
*/
|
|
448
577
|
deviceIdFor(handle) {
|
|
449
|
-
|
|
578
|
+
const key = this.key(handle);
|
|
579
|
+
return this.evictedDeviceIds.get(key) ?? this.sweptByAge.get(key)?.deviceId ?? this.entries.get(key)?.deviceId ?? null;
|
|
450
580
|
}
|
|
451
581
|
/**
|
|
452
582
|
* Release the worker-side RAM native lease for `handle` and drop its entry —
|
|
@@ -461,13 +591,24 @@ var NativeCropRegistry = class {
|
|
|
461
591
|
for (const entry of this.entries.values()) safeRelease(entry.request);
|
|
462
592
|
this.entries.clear();
|
|
463
593
|
this.evictedDeviceIds.clear();
|
|
594
|
+
this.sweptByAge.clear();
|
|
464
595
|
}
|
|
465
|
-
/**
|
|
466
|
-
|
|
596
|
+
/**
|
|
597
|
+
* Release + drop one entry by its map key, remembering WHY it left.
|
|
598
|
+
*
|
|
599
|
+
* `age` — past the lease bound (a `lookup` refusal or a `register` sweep):
|
|
600
|
+
* remembered with its registration clock, so a later ask for the same
|
|
601
|
+
* handle is still answered `expired` with a real age. `capacity` — the cap
|
|
602
|
+
* bound and pushed it out; `released` — the caller was done with it. Those
|
|
603
|
+
* two stay `unknown` on a later ask: neither is a late ask, and folding
|
|
604
|
+
* them into the refusal count would make the bound look tighter than it is.
|
|
605
|
+
*/
|
|
606
|
+
releaseEntry(key, cause = "released") {
|
|
467
607
|
const entry = this.entries.get(key);
|
|
468
608
|
if (!entry) return;
|
|
469
609
|
this.entries.delete(key);
|
|
470
|
-
this.
|
|
610
|
+
if (cause === "age") this.rememberSweptByAge(key, entry);
|
|
611
|
+
else this.rememberEvicted(key, entry.deviceId);
|
|
471
612
|
safeRelease(entry.request);
|
|
472
613
|
}
|
|
473
614
|
/** Bounded memory of evicted handles → deviceId, for miss attribution. */
|
|
@@ -480,6 +621,24 @@ var NativeCropRegistry = class {
|
|
|
480
621
|
this.evictedDeviceIds.delete(oldest);
|
|
481
622
|
}
|
|
482
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Bounded memory of handles the AGE bound dropped, with the clock the
|
|
626
|
+
* refusal is measured from. Same capacity as the live map: at the busiest
|
|
627
|
+
* node's rate that is one more lease window of memory, after which a
|
|
628
|
+
* refusal degrades to `unknown` — an ask that late is not a bound question.
|
|
629
|
+
*/
|
|
630
|
+
sweptByAge = /* @__PURE__ */ new Map();
|
|
631
|
+
rememberSweptByAge(key, entry) {
|
|
632
|
+
this.sweptByAge.set(key, {
|
|
633
|
+
deviceId: entry.deviceId,
|
|
634
|
+
registeredAt: entry.registeredAt
|
|
635
|
+
});
|
|
636
|
+
while (this.sweptByAge.size > this.capacity) {
|
|
637
|
+
const oldest = this.sweptByAge.keys().next().value;
|
|
638
|
+
if (oldest === void 0) break;
|
|
639
|
+
this.sweptByAge.delete(oldest);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
483
642
|
};
|
|
484
643
|
//#endregion
|
|
485
644
|
//#region src/session-decode/node-decode-budget.ts
|
|
@@ -574,6 +733,160 @@ function createRetainedContentFallback(registry, ref) {
|
|
|
574
733
|
}
|
|
575
734
|
} };
|
|
576
735
|
}
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/session-decode/parked-frame-store.ts
|
|
738
|
+
/** Per-camera parcel bound. 24 live tracks × two kinds on one camera is a
|
|
739
|
+
* crowd scene; past that the camera's oldest parcel leaves first. Read ONCE
|
|
740
|
+
* from `CAMSTACK_PARKED_FRAMES_PER_DEVICE`. */
|
|
741
|
+
var PARKED_FRAMES_PER_DEVICE_MAX = (() => {
|
|
742
|
+
const raw = Number(process.env["CAMSTACK_PARKED_FRAMES_PER_DEVICE"]);
|
|
743
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 48;
|
|
744
|
+
})();
|
|
745
|
+
/** {@link PARKED_FRAME_BUDGET_MB} in bytes. */
|
|
746
|
+
var PARKED_FRAME_BUDGET_BYTES = (() => {
|
|
747
|
+
const raw = Number(process.env["CAMSTACK_PARKED_FRAME_MB"]);
|
|
748
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 64;
|
|
749
|
+
})() * 1024 * 1024;
|
|
750
|
+
function keyOf$1(deviceId, trackId, kind) {
|
|
751
|
+
return `${deviceId}|${trackId}|${kind}`;
|
|
752
|
+
}
|
|
753
|
+
var ParkedFrameStore = class {
|
|
754
|
+
budgetBytes;
|
|
755
|
+
perDeviceMax;
|
|
756
|
+
maxAgeMs;
|
|
757
|
+
now;
|
|
758
|
+
onEvict;
|
|
759
|
+
/** Insertion-ordered → the head is the globally oldest park. A replace
|
|
760
|
+
* re-inserts, so order IS park order. */
|
|
761
|
+
entries = /* @__PURE__ */ new Map();
|
|
762
|
+
/** Per-device keys in park order, mirroring {@link entries} for the bound. */
|
|
763
|
+
perDeviceKeys = /* @__PURE__ */ new Map();
|
|
764
|
+
totalBytes = 0;
|
|
765
|
+
constructor(options = {}) {
|
|
766
|
+
this.budgetBytes = options.budgetBytes ?? PARKED_FRAME_BUDGET_BYTES;
|
|
767
|
+
this.perDeviceMax = options.perDeviceMax ?? PARKED_FRAMES_PER_DEVICE_MAX;
|
|
768
|
+
this.maxAgeMs = options.maxAgeMs ?? 6e5;
|
|
769
|
+
this.now = options.now ?? Date.now;
|
|
770
|
+
this.onEvict = options.onEvict;
|
|
771
|
+
}
|
|
772
|
+
/** Number of parcels held (tests / metrics). */
|
|
773
|
+
get size() {
|
|
774
|
+
return this.entries.size;
|
|
775
|
+
}
|
|
776
|
+
/** Bytes of JPEG held (tests / metrics). */
|
|
777
|
+
get bytes() {
|
|
778
|
+
return this.totalBytes;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Park one parcel. Replaces an existing parcel for the key unless the new
|
|
782
|
+
* one shows an OLDER frame instant (a racing loser). Sweeps the age ceiling,
|
|
783
|
+
* then enforces the per-device bound and the node budget — evicting the
|
|
784
|
+
* oldest of the offending scope, never refusing the new parcel.
|
|
785
|
+
*/
|
|
786
|
+
park(deviceId, trackId, kind, frame) {
|
|
787
|
+
const nowMs = this.now();
|
|
788
|
+
this.sweepExpired(nowMs);
|
|
789
|
+
const key = keyOf$1(deviceId, trackId, kind);
|
|
790
|
+
const existing = this.entries.get(key);
|
|
791
|
+
if (existing !== void 0 && frame.timestamp < existing.frame.timestamp) return "older-ignored";
|
|
792
|
+
if (existing !== void 0) this.remove(key);
|
|
793
|
+
this.entries.set(key, {
|
|
794
|
+
deviceId,
|
|
795
|
+
trackId,
|
|
796
|
+
kind,
|
|
797
|
+
frame,
|
|
798
|
+
parkedAt: nowMs
|
|
799
|
+
});
|
|
800
|
+
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
801
|
+
if (deviceKeys) deviceKeys.push(key);
|
|
802
|
+
else this.perDeviceKeys.set(deviceId, [key]);
|
|
803
|
+
this.totalBytes += frame.jpeg.byteLength;
|
|
804
|
+
this.enforceDeviceBound(deviceId);
|
|
805
|
+
this.enforceBudget();
|
|
806
|
+
return existing === void 0 ? "stored" : "replaced";
|
|
807
|
+
}
|
|
808
|
+
/** The parcel for a key, or `null` — never anything else, never by handle. */
|
|
809
|
+
get(deviceId, trackId, kind) {
|
|
810
|
+
const entry = this.entries.get(keyOf$1(deviceId, trackId, kind));
|
|
811
|
+
if (entry === void 0) return null;
|
|
812
|
+
if (this.now() - entry.parkedAt > this.maxAgeMs) {
|
|
813
|
+
this.evict(keyOf$1(deviceId, trackId, kind), "age");
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
...entry.frame,
|
|
818
|
+
parkedAt: entry.parkedAt
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/** Drop every parcel of a track (the hub closed it). Returns how many left. */
|
|
822
|
+
release(deviceId, trackId) {
|
|
823
|
+
const keys = (this.perDeviceKeys.get(deviceId) ?? []).filter((key) => this.entries.get(key)?.trackId === trackId);
|
|
824
|
+
for (const key of keys) this.remove(key);
|
|
825
|
+
return keys.length;
|
|
826
|
+
}
|
|
827
|
+
/** What one camera is costing right now. */
|
|
828
|
+
statsByDevice(deviceId) {
|
|
829
|
+
const keys = this.perDeviceKeys.get(deviceId) ?? [];
|
|
830
|
+
let bytes = 0;
|
|
831
|
+
for (const key of keys) bytes += this.entries.get(key)?.frame.jpeg.byteLength ?? 0;
|
|
832
|
+
return {
|
|
833
|
+
parcels: keys.length,
|
|
834
|
+
bytes
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** Drop everything (addon shutdown). */
|
|
838
|
+
clear() {
|
|
839
|
+
this.entries.clear();
|
|
840
|
+
this.perDeviceKeys.clear();
|
|
841
|
+
this.totalBytes = 0;
|
|
842
|
+
}
|
|
843
|
+
/** Age sweep from the head — insertion order is park order, so it stops at
|
|
844
|
+
* the first parcel still inside the ceiling. */
|
|
845
|
+
sweepExpired(nowMs) {
|
|
846
|
+
for (const [key, entry] of this.entries) {
|
|
847
|
+
if (nowMs - entry.parkedAt <= this.maxAgeMs) return;
|
|
848
|
+
this.evict(key, "age");
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
enforceDeviceBound(deviceId) {
|
|
852
|
+
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
853
|
+
while (deviceKeys !== void 0 && deviceKeys.length > this.perDeviceMax) {
|
|
854
|
+
const oldest = deviceKeys[0];
|
|
855
|
+
if (oldest === void 0) break;
|
|
856
|
+
this.evict(oldest, "device-bound");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/** Globally oldest first, whatever its device; always keeps ≥ 1. */
|
|
860
|
+
enforceBudget() {
|
|
861
|
+
for (const key of [...this.entries.keys()]) {
|
|
862
|
+
if (this.totalBytes <= this.budgetBytes || this.entries.size <= 1) return;
|
|
863
|
+
this.evict(key, "node-budget");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
evict(key, cause) {
|
|
867
|
+
const entry = this.entries.get(key);
|
|
868
|
+
if (entry === void 0) return;
|
|
869
|
+
this.remove(key);
|
|
870
|
+
this.onEvict?.({
|
|
871
|
+
deviceId: entry.deviceId,
|
|
872
|
+
trackId: entry.trackId,
|
|
873
|
+
kind: entry.kind,
|
|
874
|
+
cause
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
remove(key) {
|
|
878
|
+
const entry = this.entries.get(key);
|
|
879
|
+
if (entry === void 0) return;
|
|
880
|
+
this.entries.delete(key);
|
|
881
|
+
this.totalBytes -= entry.frame.jpeg.byteLength;
|
|
882
|
+
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
883
|
+
if (deviceKeys) {
|
|
884
|
+
const idx = deviceKeys.indexOf(key);
|
|
885
|
+
if (idx >= 0) deviceKeys.splice(idx, 1);
|
|
886
|
+
if (deviceKeys.length === 0) this.perDeviceKeys.delete(entry.deviceId);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
577
890
|
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
578
891
|
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
579
892
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
@@ -2451,7 +2764,7 @@ var EMPTY_SAMPLE = {
|
|
|
2451
2764
|
*/
|
|
2452
2765
|
function applyStallTick(sample, lateByMs, gcWindow, reportThresholdMs) {
|
|
2453
2766
|
if (lateByMs < reportThresholdMs) return sample;
|
|
2454
|
-
const verdict =
|
|
2767
|
+
const verdict = (0, _camstack_system_addon_utils.attributeStall)(lateByMs, Math.round(gcWindow.totalMs), gcWindow.count, Math.round(gcWindow.longestMs));
|
|
2455
2768
|
return {
|
|
2456
2769
|
count: sample.count + 1,
|
|
2457
2770
|
maxMs: Math.max(sample.maxMs, lateByMs),
|
|
@@ -6038,6 +6351,90 @@ function isUsable(bbox) {
|
|
|
6038
6351
|
return bbox.width > 0 && bbox.height > 0;
|
|
6039
6352
|
}
|
|
6040
6353
|
//#endregion
|
|
6354
|
+
//#region src/pipeline-runner/track-frame-park.ts
|
|
6355
|
+
/** Park one frame for one track. Never throws; a refusal is an answer. */
|
|
6356
|
+
async function parkTrackFrame(deps, input) {
|
|
6357
|
+
const { deviceId, trackId, kind } = input;
|
|
6358
|
+
const handleKey = `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`;
|
|
6359
|
+
const resolved = await deps.resolvePixels({
|
|
6360
|
+
handle: input.handle,
|
|
6361
|
+
bbox: input.bbox,
|
|
6362
|
+
...input.maxWidth !== void 0 ? { maxWidth: input.maxWidth } : {}
|
|
6363
|
+
});
|
|
6364
|
+
if (resolved.pixels === null || resolved.tier !== "native") {
|
|
6365
|
+
const missReason = resolved.missReason ?? "worker-lease-gone";
|
|
6366
|
+
const detail = resolved.workerReason !== void 0 ? `${missReason}: ${resolved.workerReason}` : missReason;
|
|
6367
|
+
deps.logger.warn("track frame park refused — the handle no longer resolves at park time", {
|
|
6368
|
+
tags: { deviceId },
|
|
6369
|
+
meta: {
|
|
6370
|
+
trackId,
|
|
6371
|
+
kind,
|
|
6372
|
+
reason: "handle-gone",
|
|
6373
|
+
missReason,
|
|
6374
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6375
|
+
servedTier: resolved.tier,
|
|
6376
|
+
handle: handleKey,
|
|
6377
|
+
...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
|
|
6378
|
+
}
|
|
6379
|
+
});
|
|
6380
|
+
return {
|
|
6381
|
+
parked: false,
|
|
6382
|
+
nodeId: deps.nodeId,
|
|
6383
|
+
reason: "handle-gone",
|
|
6384
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6385
|
+
detail
|
|
6386
|
+
};
|
|
6387
|
+
}
|
|
6388
|
+
const jpeg = await deps.encodeJpeg(resolved.pixels);
|
|
6389
|
+
if (jpeg === null) {
|
|
6390
|
+
deps.logger.warn("track frame park refused — JPEG encode failed", {
|
|
6391
|
+
tags: { deviceId },
|
|
6392
|
+
meta: {
|
|
6393
|
+
trackId,
|
|
6394
|
+
kind,
|
|
6395
|
+
reason: "encode-failed",
|
|
6396
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6397
|
+
handle: handleKey,
|
|
6398
|
+
width: resolved.pixels.width,
|
|
6399
|
+
height: resolved.pixels.height
|
|
6400
|
+
}
|
|
6401
|
+
});
|
|
6402
|
+
return {
|
|
6403
|
+
parked: false,
|
|
6404
|
+
nodeId: deps.nodeId,
|
|
6405
|
+
reason: "encode-failed",
|
|
6406
|
+
handleAgeMs: resolved.handleAgeMs
|
|
6407
|
+
};
|
|
6408
|
+
}
|
|
6409
|
+
const verdict = deps.store.park(deviceId, trackId, kind, {
|
|
6410
|
+
jpeg,
|
|
6411
|
+
width: resolved.pixels.width,
|
|
6412
|
+
height: resolved.pixels.height,
|
|
6413
|
+
timestamp: input.timestamp
|
|
6414
|
+
});
|
|
6415
|
+
deps.logger.debug("track frame parked", {
|
|
6416
|
+
tags: { deviceId },
|
|
6417
|
+
meta: {
|
|
6418
|
+
trackId,
|
|
6419
|
+
kind,
|
|
6420
|
+
verdict,
|
|
6421
|
+
bytes: jpeg.byteLength,
|
|
6422
|
+
width: resolved.pixels.width,
|
|
6423
|
+
height: resolved.pixels.height,
|
|
6424
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6425
|
+
source: resolved.pixels.source ?? "lease"
|
|
6426
|
+
}
|
|
6427
|
+
});
|
|
6428
|
+
return {
|
|
6429
|
+
parked: true,
|
|
6430
|
+
nodeId: deps.nodeId,
|
|
6431
|
+
width: resolved.pixels.width,
|
|
6432
|
+
height: resolved.pixels.height,
|
|
6433
|
+
bytes: jpeg.byteLength,
|
|
6434
|
+
replaced: verdict === "replaced"
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
//#endregion
|
|
6041
6438
|
//#region src/pipeline-runner/index.ts
|
|
6042
6439
|
/**
|
|
6043
6440
|
* addon-pipeline-runner — per-node detection scheduler.
|
|
@@ -6107,6 +6504,8 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
|
|
|
6107
6504
|
var MAX_BOUND_FRAME_CUTTERS = 256;
|
|
6108
6505
|
/** How often a camera reports its undispatched cuts. */
|
|
6109
6506
|
var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
|
|
6507
|
+
/** How often a camera reports parcels that left without a release (D422). */
|
|
6508
|
+
var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
|
|
6110
6509
|
/**
|
|
6111
6510
|
* How often a camera reports what carrying its birth evidence COST (D379).
|
|
6112
6511
|
*
|
|
@@ -6136,7 +6535,6 @@ var DEFAULT_CONFIG = {
|
|
|
6136
6535
|
* this window REUSES the warm worker instead of re-forking it — killing the
|
|
6137
6536
|
* spawn/teardown churn (and its cold-start latency spike) that on-motion
|
|
6138
6537
|
* cameras produced when every gate open/close forked and killed a worker.
|
|
6139
|
-
* Mirrors Scrypted's decode-fork ~15s idle-timeout.
|
|
6140
6538
|
*/
|
|
6141
6539
|
/** JPEG quality for the runner→executor detection-frame hop. q82 keeps
|
|
6142
6540
|
* detector-relevant detail (source is h264 anyway) at ~10-15× less payload
|
|
@@ -6494,6 +6892,17 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
6494
6892
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
6495
6893
|
*/
|
|
6496
6894
|
retainedFrames = null;
|
|
6895
|
+
/**
|
|
6896
|
+
* Per-track PARCELS (D422): one JPEG per `(deviceId, trackId, kind)`, cut
|
|
6897
|
+
* from the native surface at the instant the hub judged the frame relevant,
|
|
6898
|
+
* retrieved later by that key — never by handle, never by age. Bounded per
|
|
6899
|
+
* device and per node in `parked-frame-store.ts`, where the numbers are
|
|
6900
|
+
* stated. Initialised in {@link onInitialize} beside the retained store.
|
|
6901
|
+
*/
|
|
6902
|
+
parkedFrames = null;
|
|
6903
|
+
/** Per-camera tally of parcels that left WITHOUT a release, by cause. */
|
|
6904
|
+
parkedEvictions = /* @__PURE__ */ new Map();
|
|
6905
|
+
parkedEvictionLoggedAt = /* @__PURE__ */ new Map();
|
|
6497
6906
|
frameLazy = new FrameLazyTelemetry();
|
|
6498
6907
|
retention = null;
|
|
6499
6908
|
attached = /* @__PURE__ */ new Map();
|
|
@@ -6724,6 +7133,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
6724
7133
|
nodeId: this.nodeId,
|
|
6725
7134
|
continuousRetain: isContinuousRetainEnabled()
|
|
6726
7135
|
});
|
|
7136
|
+
this.parkedFrames = new ParkedFrameStore({ onEvict: (eviction) => this.noteParkedEviction(eviction.deviceId, eviction.cause) });
|
|
6727
7137
|
this.runner = new PipelineRunner({
|
|
6728
7138
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
6729
7139
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -6875,6 +7285,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
6875
7285
|
this.nativeCropRegistry.clear();
|
|
6876
7286
|
this.retainedFrames?.clear();
|
|
6877
7287
|
this.retainedFrames = null;
|
|
7288
|
+
this.parkedFrames?.clear();
|
|
7289
|
+
this.parkedFrames = null;
|
|
6878
7290
|
this.retention = null;
|
|
6879
7291
|
}
|
|
6880
7292
|
async cacheBenchFrame(input) {
|
|
@@ -7669,6 +8081,96 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7669
8081
|
}, resolved.tier, input.encodeJpeg);
|
|
7670
8082
|
}
|
|
7671
8083
|
/**
|
|
8084
|
+
* PARK a frame for a track (D422) — the hub judged it relevant, and this node
|
|
8085
|
+
* still holds its pixels. Same native-lease resolve `getNativeCrop` uses,
|
|
8086
|
+
* same single encoder; the parcel lives in {@link parkedFrames} under
|
|
8087
|
+
* `(deviceId, trackId, kind)`. A refusal is an answer and a `deviceId`-tagged
|
|
8088
|
+
* log line, never a retry.
|
|
8089
|
+
*/
|
|
8090
|
+
async parkTrackFrame(input) {
|
|
8091
|
+
const store = this.parkedFrames;
|
|
8092
|
+
if (store === null) return {
|
|
8093
|
+
parked: false,
|
|
8094
|
+
nodeId: this.nodeId,
|
|
8095
|
+
reason: "handle-gone",
|
|
8096
|
+
handleAgeMs: null,
|
|
8097
|
+
detail: "runner not initialised"
|
|
8098
|
+
};
|
|
8099
|
+
return parkTrackFrame({
|
|
8100
|
+
nodeId: this.nodeId,
|
|
8101
|
+
store,
|
|
8102
|
+
resolvePixels: (request) => this.resolveNativeCropPixels(request),
|
|
8103
|
+
encodeJpeg: async (pixels) => {
|
|
8104
|
+
const encoded = await this.encodeNativeCropResult(pixels, "native", true);
|
|
8105
|
+
return encoded?.jpeg !== void 0 ? Buffer.from(encoded.jpeg, "base64") : null;
|
|
8106
|
+
},
|
|
8107
|
+
logger: this.ctx.logger
|
|
8108
|
+
}, input);
|
|
8109
|
+
}
|
|
8110
|
+
/** A parcel by `(deviceId, trackId, kind)`; `null` = nothing parked here. */
|
|
8111
|
+
async getParkedTrackFrame(input) {
|
|
8112
|
+
const parcel = this.parkedFrames?.get(input.deviceId, input.trackId, input.kind) ?? null;
|
|
8113
|
+
if (parcel === null) {
|
|
8114
|
+
this.ctxIfReady?.logger.debug("parked frame miss — nothing under this key", {
|
|
8115
|
+
tags: { deviceId: input.deviceId },
|
|
8116
|
+
meta: {
|
|
8117
|
+
trackId: input.trackId,
|
|
8118
|
+
kind: input.kind,
|
|
8119
|
+
...this.parkedFrames?.statsByDevice(input.deviceId) ?? {
|
|
8120
|
+
parcels: 0,
|
|
8121
|
+
bytes: 0
|
|
8122
|
+
}
|
|
8123
|
+
}
|
|
8124
|
+
});
|
|
8125
|
+
return null;
|
|
8126
|
+
}
|
|
8127
|
+
return {
|
|
8128
|
+
jpeg: parcel.jpeg.toString("base64"),
|
|
8129
|
+
width: parcel.width,
|
|
8130
|
+
height: parcel.height,
|
|
8131
|
+
timestamp: parcel.timestamp,
|
|
8132
|
+
parkedAt: parcel.parkedAt
|
|
8133
|
+
};
|
|
8134
|
+
}
|
|
8135
|
+
/** The hub closed a track: drop its parcels. Idempotent. */
|
|
8136
|
+
async releaseParkedTrackFrames(input) {
|
|
8137
|
+
return { released: this.parkedFrames?.release(input.deviceId, input.trackId) ?? 0 };
|
|
8138
|
+
}
|
|
8139
|
+
/**
|
|
8140
|
+
* A parcel left without a release. Counted per camera by cause and reported
|
|
8141
|
+
* one line per camera per minute: a camera whose parcels keep leaving under
|
|
8142
|
+
* the device bound is a crowd, under the node budget is a neighbour's
|
|
8143
|
+
* traffic, and under the age ceiling is a hub that stopped releasing.
|
|
8144
|
+
*/
|
|
8145
|
+
noteParkedEviction(deviceId, cause) {
|
|
8146
|
+
const tally = this.parkedEvictions.get(deviceId) ?? {
|
|
8147
|
+
deviceBound: 0,
|
|
8148
|
+
nodeBudget: 0,
|
|
8149
|
+
age: 0
|
|
8150
|
+
};
|
|
8151
|
+
if (cause === "device-bound") tally.deviceBound += 1;
|
|
8152
|
+
else if (cause === "node-budget") tally.nodeBudget += 1;
|
|
8153
|
+
else tally.age += 1;
|
|
8154
|
+
this.parkedEvictions.set(deviceId, tally);
|
|
8155
|
+
const now = Date.now();
|
|
8156
|
+
if (now - (this.parkedEvictionLoggedAt.get(deviceId) ?? 0) < PARKED_EVICTION_LOG_INTERVAL_MS) return;
|
|
8157
|
+
this.parkedEvictionLoggedAt.set(deviceId, now);
|
|
8158
|
+
this.ctxIfReady?.logger.info("parked frames evicted without a release", {
|
|
8159
|
+
tags: { deviceId },
|
|
8160
|
+
meta: {
|
|
8161
|
+
...tally,
|
|
8162
|
+
lastCause: cause,
|
|
8163
|
+
parcelsHeld: this.parkedFrames?.size ?? 0,
|
|
8164
|
+
bytesHeld: this.parkedFrames?.bytes ?? 0
|
|
8165
|
+
}
|
|
8166
|
+
});
|
|
8167
|
+
this.parkedEvictions.set(deviceId, {
|
|
8168
|
+
deviceBound: 0,
|
|
8169
|
+
nodeBudget: 0,
|
|
8170
|
+
age: 0
|
|
8171
|
+
});
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
7672
8174
|
* Turn a finished frame into subject TILES and end its hold — the runner half
|
|
7673
8175
|
* of R2 (`docs/design/2026-08-13-native-lease-two-tier-redesign.md`).
|
|
7674
8176
|
*
|
|
@@ -7938,11 +8440,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7938
8440
|
let nativeResult = null;
|
|
7939
8441
|
let missReason = null;
|
|
7940
8442
|
let handleAgeMs = null;
|
|
8443
|
+
/** The camera the lookup attributed the handle to, live or expired. */
|
|
8444
|
+
let deviceIdForMiss = null;
|
|
7941
8445
|
let workerReason;
|
|
7942
8446
|
if (!isUnboundedFullFrame) {
|
|
7943
|
-
const found = this.nativeCropRegistry.
|
|
7944
|
-
handleAgeMs = found
|
|
7945
|
-
|
|
8447
|
+
const found = this.nativeCropRegistry.lookup(input.handle);
|
|
8448
|
+
handleAgeMs = found.kind === "unknown" ? null : found.ageMs;
|
|
8449
|
+
deviceIdForMiss = found.kind === "unknown" ? null : found.deviceId;
|
|
8450
|
+
if (found.kind === "live") {
|
|
7946
8451
|
const attempt = await found.request(input.bbox, input.maxWidth);
|
|
7947
8452
|
if (attempt.kind === "crop") nativeResult = {
|
|
7948
8453
|
bytes: attempt.crop.bytes,
|
|
@@ -7954,11 +8459,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7954
8459
|
missReason = "worker-lease-gone";
|
|
7955
8460
|
workerReason = attempt.workerReason;
|
|
7956
8461
|
}
|
|
7957
|
-
} else missReason = "
|
|
8462
|
+
} else if (found.kind === "expired") missReason = "handle-expired";
|
|
8463
|
+
else missReason = "no-registry-entry";
|
|
7958
8464
|
if (missReason !== null) {
|
|
7959
8465
|
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
7960
8466
|
const fields = {
|
|
7961
|
-
tags: { deviceId:
|
|
8467
|
+
tags: { deviceId: deviceIdForMiss ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
7962
8468
|
meta: {
|
|
7963
8469
|
reason: missReason,
|
|
7964
8470
|
servedTier: tierOnMiss,
|
|
@@ -7970,8 +8476,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7970
8476
|
...workerReason !== void 0 ? { workerReason } : {}
|
|
7971
8477
|
}
|
|
7972
8478
|
};
|
|
7973
|
-
|
|
7974
|
-
|
|
8479
|
+
const message = missReason === "handle-expired" ? "native crop refused — the handle is past the lease age bound" : "native crop miss";
|
|
8480
|
+
const withBound = missReason === "handle-expired" ? {
|
|
8481
|
+
...fields,
|
|
8482
|
+
meta: {
|
|
8483
|
+
...fields.meta,
|
|
8484
|
+
leaseMaxAgeMs: NATIVE_CROP_LEASE_MAX_AGE_MS
|
|
8485
|
+
}
|
|
8486
|
+
} : fields;
|
|
8487
|
+
if (tierOnMiss === "miss") this.ctx.logger.warn(message, withBound);
|
|
8488
|
+
else this.ctx.logger.debug(message, withBound);
|
|
7975
8489
|
}
|
|
7976
8490
|
}
|
|
7977
8491
|
return {
|
|
@@ -9887,6 +10401,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
9887
10401
|
frame: result,
|
|
9888
10402
|
nodeId: this.nodeId,
|
|
9889
10403
|
frameHandle: handle,
|
|
10404
|
+
emittedAt: Date.now(),
|
|
9890
10405
|
...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
|
|
9891
10406
|
...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
|
|
9892
10407
|
};
|