@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
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-
|
|
2
|
-
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
3
|
-
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-
|
|
4
|
-
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-
|
|
1
|
+
import { $ as failureContributionCapability, It as errMsg, Qt as nodePin, R as customAction, U as deriveDetailCropRect, Ut as BaseAddon, V as defineCustomActions, _ as FailureCounters, _t as pickDetailCropConvention, an as boolean, bt as pipelineRunnerCapability, cn as literal, hn as EventCategory, in as array, it as loadContributionCapability, ln as number, m as DEVICE_BACKEND_TO_FORMAT, p as DEFAULT_NATIVE_LEASE_SETTINGS, pn as string, qt as createEvent, rn as _enum, sn as lazy, u as DEFAULT_DETAIL_CROP_CONVENTION, un as object, vt as pickNativeLeaseOverride } from "../dist-DgeXZfwn.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-C9GcBrwk.mjs";
|
|
3
|
+
import { i as resolveFrameViewGeometry, l as getStepDefinition, r as localFrameRegistry } from "../default-detection-model-BwNEV8Wl.mjs";
|
|
4
|
+
import { a as nativeLeaseSettingEnv, t as isWorkerReply } from "../worker-protocol-SzmaLNnP.mjs";
|
|
5
5
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
6
6
|
import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
|
|
7
7
|
import { a as preRollRequestOf, o as withDetectionIntent, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DWe2kInY.mjs";
|
|
8
|
-
import { t as attributeStall } from "../event-loop-stall-monitor-DXLNBMkY.mjs";
|
|
9
8
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
10
9
|
import * as os from "node:os";
|
|
11
10
|
import { fork } from "node:child_process";
|
|
12
11
|
import sharp from "sharp";
|
|
13
12
|
import { fileURLToPath } from "node:url";
|
|
14
13
|
import { PerformanceObserver } from "node:perf_hooks";
|
|
14
|
+
import { attributeStall } from "@camstack/system/addon-utils";
|
|
15
15
|
//#region src/session-decode/fork-decode-worker.ts
|
|
16
16
|
/** Absolute path to the built decode-worker child entry (a vite build entry). */
|
|
17
17
|
var CHILD_ENTRY_PATH = fileURLToPath(new URL("../session-decode/decode-worker-child.js", import.meta.url));
|
|
@@ -380,6 +380,84 @@ function positiveOrUndefined(value) {
|
|
|
380
380
|
return Number.isFinite(value) && value > 0 ? value : void 0;
|
|
381
381
|
}
|
|
382
382
|
//#endregion
|
|
383
|
+
//#region src/session-decode/native-crop-lease-age.ts
|
|
384
|
+
/**
|
|
385
|
+
* THE age bound on a native crop lease (ADR-0419).
|
|
386
|
+
*
|
|
387
|
+
* ## The measurement
|
|
388
|
+
*
|
|
389
|
+
* Mac agent node (`MinidiGianluca.localdomain`), 2026-09-09, a 54-minute
|
|
390
|
+
* window: 332 `native crop miss` lines, every one of them `worker-lease-gone`,
|
|
391
|
+
* at a MEDIAN `handleAgeMs` of 30 099 ms — thirty seconds. The node-wide
|
|
392
|
+
* {@link ../session-decode/native-crop-registry.NativeCropRegistry} sat pinned
|
|
393
|
+
* at `registrySize 4096 / registryCap 4096` for the whole window. Measured
|
|
394
|
+
* consequence: on track `d64ace88` (device 1443) the frontal-face frames never
|
|
395
|
+
* reached the face detector, because every native fetch missed.
|
|
396
|
+
*
|
|
397
|
+
* ## Why an AGE bound and not a bigger cap
|
|
398
|
+
*
|
|
399
|
+
* The pinned cap is what the miss line shows, so raising it is the instinctive
|
|
400
|
+
* move, and it is wrong twice. It keeps more full-resolution surfaces alive
|
|
401
|
+
* (real RAM, on a ring that has leaked a VAAPI surface pool before), and it
|
|
402
|
+
* only moves the threshold at which the same pinning returns. It also cannot
|
|
403
|
+
* work: the decode worker's own tile store expires a frame's tiles at
|
|
404
|
+
* `NATIVE_TILE_TTL_MS` (30 s), so a handle 30 s old has nothing behind it at
|
|
405
|
+
* ANY registry size. The operator's decision, 2026-09-09: **a native lease
|
|
406
|
+
* lives at most ten seconds**; a crop asked for later than that cannot be
|
|
407
|
+
* useful, and the caller's detection-frame fallback — which
|
|
408
|
+
* `native-frame-ring.ts` calls FREE and expected — is the right answer.
|
|
409
|
+
*
|
|
410
|
+
* ## Why the bound is enforced at the registry
|
|
411
|
+
*
|
|
412
|
+
* The registry is the only place in the system where a handle's registration
|
|
413
|
+
* wall-clock exists: `FrameHandle` carries no correlatable clock (`pts` is
|
|
414
|
+
* explicitly process-local and must never be compared against another), so
|
|
415
|
+
* neither the hub-side requester nor the decode worker can decide this
|
|
416
|
+
* question correctly. It is also the single node-wide chokepoint every native
|
|
417
|
+
* path funnels through — the `getNativeCrop` cap, `getNativeCropForDetail`,
|
|
418
|
+
* and `runDetailSubtree`'s rung 1 — so ONE rule covers all of them, and it
|
|
419
|
+
* sits in-process, in FRONT of the round trip that the 332 misses paid for.
|
|
420
|
+
* Refusing here makes the wasted work impossible rather than merely faster.
|
|
421
|
+
*
|
|
422
|
+
* Deliberately a constant and not an operator knob: it is a property of the
|
|
423
|
+
* pipeline, not of a node's hardware (same argument as the lease knobs in
|
|
424
|
+
* `@camstack/types` `pipeline/native-lease.ts`), and a per-node value would
|
|
425
|
+
* make the same camera produce different crop quality depending on where the
|
|
426
|
+
* balancer placed it.
|
|
427
|
+
*/
|
|
428
|
+
/**
|
|
429
|
+
* How long a registered handle may be asked for a native crop. Inclusive: an
|
|
430
|
+
* ask at exactly this age is still served, one millisecond later is refused.
|
|
431
|
+
*
|
|
432
|
+
* ## Ten seconds was tried on the fleet, and the fleet refused it
|
|
433
|
+
*
|
|
434
|
+
* The operator's number was ten seconds, chosen on the belief — stated in this
|
|
435
|
+
* file's first version, and sourced from `@camstack/types`
|
|
436
|
+
* `pipeline/native-lease.ts` sizing prose rather than from a measurement of
|
|
437
|
+
* THIS path — that a p90 ask lands at 4.3 s and a p50 at 367 ms. Deployed
|
|
438
|
+
* 2026-09-09 ~14:00, it produced, on the hub, **3 371 `no-registry-entry`
|
|
439
|
+
* misses in fifteen minutes** (2 036 of them on `salone` alone) against
|
|
440
|
+
* essentially ZERO in the half hour before the deploy: the registry held 46
|
|
441
|
+
* entries where it had held 4 096, and the asks arriving between 10 s and 30 s
|
|
442
|
+
* found nothing at all. The operator saw it as tracks with missing media.
|
|
443
|
+
*
|
|
444
|
+
* So the bound is the worker's own tile TTL (`NATIVE_TILE_TTL_MS`, 30 s,
|
|
445
|
+
* `decode-worker-child.ts`) — the age past which there is provably nothing
|
|
446
|
+
* behind the handle at ANY registry size. It still bounds the lease by AGE
|
|
447
|
+
* rather than by a 4 096-entry capacity eviction, which is what D419 set out
|
|
448
|
+
* to fix; it simply stops refusing asks the worker could still have answered.
|
|
449
|
+
*
|
|
450
|
+
* The real defect is caller-side and is not fixed here: a ~1 Hz
|
|
451
|
+
* `TrackDetailDispatcher` re-asks a frozen handle for the life of a coasted
|
|
452
|
+
* track, and `persistKeyFrames` / `offerGroupShot` are the only capture sites
|
|
453
|
+
* that omit `staleAfterMs`. ADR-0419 named both as open; until they are fixed
|
|
454
|
+
* a tighter bound cannot be told apart from a broken one.
|
|
455
|
+
*/
|
|
456
|
+
var NATIVE_CROP_LEASE_MAX_AGE_MS = 3e4;
|
|
457
|
+
function classifyNativeCropLeaseAge(ageMs, maxAgeMs = NATIVE_CROP_LEASE_MAX_AGE_MS) {
|
|
458
|
+
return ageMs > maxAgeMs ? "expired" : "live";
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
383
461
|
//#region src/session-decode/native-crop-registry.ts
|
|
384
462
|
var DEFAULT_NATIVE_CROP_REGISTRY_CAP = (() => {
|
|
385
463
|
const raw = Number(process.env["CAMSTACK_NATIVE_CROP_REGISTRY_CAP"]);
|
|
@@ -393,9 +471,11 @@ function safeRelease(request) {
|
|
|
393
471
|
}
|
|
394
472
|
var NativeCropRegistry = class {
|
|
395
473
|
capacity;
|
|
474
|
+
maxAgeMs;
|
|
396
475
|
entries = /* @__PURE__ */ new Map();
|
|
397
|
-
constructor(capacity = DEFAULT_NATIVE_CROP_REGISTRY_CAP) {
|
|
476
|
+
constructor(capacity = DEFAULT_NATIVE_CROP_REGISTRY_CAP, maxAgeMs = NATIVE_CROP_LEASE_MAX_AGE_MS) {
|
|
398
477
|
this.capacity = capacity;
|
|
478
|
+
this.maxAgeMs = maxAgeMs;
|
|
399
479
|
}
|
|
400
480
|
/** Number of registered handles (tests / metrics). */
|
|
401
481
|
get size() {
|
|
@@ -410,6 +490,7 @@ var NativeCropRegistry = class {
|
|
|
410
490
|
* its worker-side RAM lease so a lease never outlives its registry entry.
|
|
411
491
|
*/
|
|
412
492
|
register(handle, request, deviceId) {
|
|
493
|
+
this.sweepExpired();
|
|
413
494
|
const k = this.key(handle);
|
|
414
495
|
this.entries.delete(k);
|
|
415
496
|
this.entries.set(k, {
|
|
@@ -420,27 +501,76 @@ var NativeCropRegistry = class {
|
|
|
420
501
|
while (this.entries.size > this.capacity) {
|
|
421
502
|
const oldest = this.entries.keys().next().value;
|
|
422
503
|
if (oldest === void 0) break;
|
|
423
|
-
this.releaseEntry(oldest);
|
|
504
|
+
this.releaseEntry(oldest, "capacity");
|
|
424
505
|
}
|
|
425
506
|
}
|
|
426
|
-
/**
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Resolve `handle`, applying the age bound (ADR-0419).
|
|
509
|
+
*
|
|
510
|
+
* The bound is applied HERE and not at the caller or the worker because this
|
|
511
|
+
* is the only place `registeredAt` exists — see `native-crop-lease-age.ts`.
|
|
512
|
+
* An expired handle is refused and released without the requester ever being
|
|
513
|
+
* invoked, so the doomed cross-process fetch does not happen at all.
|
|
514
|
+
*/
|
|
515
|
+
lookup(handle) {
|
|
516
|
+
const key = this.key(handle);
|
|
517
|
+
const entry = this.entries.get(key);
|
|
518
|
+
if (entry === void 0) {
|
|
519
|
+
const swept = this.sweptByAge.get(key);
|
|
520
|
+
if (swept === void 0) return { kind: "unknown" };
|
|
521
|
+
return {
|
|
522
|
+
kind: "expired",
|
|
523
|
+
deviceId: swept.deviceId,
|
|
524
|
+
ageMs: Date.now() - swept.registeredAt
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const ageMs = Date.now() - entry.registeredAt;
|
|
528
|
+
if (classifyNativeCropLeaseAge(ageMs, this.maxAgeMs) === "expired") {
|
|
529
|
+
this.releaseEntry(key, "age");
|
|
530
|
+
return {
|
|
531
|
+
kind: "expired",
|
|
532
|
+
deviceId: entry.deviceId,
|
|
533
|
+
ageMs
|
|
534
|
+
};
|
|
535
|
+
}
|
|
430
536
|
return {
|
|
537
|
+
kind: "live",
|
|
431
538
|
request: entry.request,
|
|
432
539
|
deviceId: entry.deviceId,
|
|
433
|
-
ageMs
|
|
540
|
+
ageMs
|
|
434
541
|
};
|
|
435
542
|
}
|
|
436
543
|
/**
|
|
544
|
+
* Drop every entry past the age bound, oldest first.
|
|
545
|
+
*
|
|
546
|
+
* The map is insertion-ordered and insertion order IS registration order, so
|
|
547
|
+
* the head is the oldest and the walk stops at the first live entry. Run on
|
|
548
|
+
* every `register`, which is the only path that reliably ticks on a busy
|
|
549
|
+
* node — a timer here would be a second owner of the same lifetime.
|
|
550
|
+
*
|
|
551
|
+
* It can never drop an entry a request would have been served from: anything
|
|
552
|
+
* it releases would have been refused by {@link lookup} in the same instant.
|
|
553
|
+
* What it buys is that the worker's lease is released when it stops being
|
|
554
|
+
* useful (10 s) instead of when 4 095 later handles have pushed it out of a
|
|
555
|
+
* capacity eviction (~30 s on this cluster) — the reason the registry sat
|
|
556
|
+
* pinned at `4096/4096`.
|
|
557
|
+
*/
|
|
558
|
+
sweepExpired() {
|
|
559
|
+
const now = Date.now();
|
|
560
|
+
for (const [key, entry] of this.entries) {
|
|
561
|
+
if (classifyNativeCropLeaseAge(now - entry.registeredAt, this.maxAgeMs) === "live") return;
|
|
562
|
+
this.releaseEntry(key, "age");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
437
566
|
* The deviceId a handle was registered under, even AFTER its entry was
|
|
438
567
|
* evicted — kept in a small parallel ring so a `no-registry-entry` miss can
|
|
439
568
|
* still be attributed to a camera (a miss line without `deviceId` cannot
|
|
440
569
|
* answer "why is 617 worse than 615?").
|
|
441
570
|
*/
|
|
442
571
|
deviceIdFor(handle) {
|
|
443
|
-
|
|
572
|
+
const key = this.key(handle);
|
|
573
|
+
return this.evictedDeviceIds.get(key) ?? this.sweptByAge.get(key)?.deviceId ?? this.entries.get(key)?.deviceId ?? null;
|
|
444
574
|
}
|
|
445
575
|
/**
|
|
446
576
|
* Release the worker-side RAM native lease for `handle` and drop its entry —
|
|
@@ -455,13 +585,24 @@ var NativeCropRegistry = class {
|
|
|
455
585
|
for (const entry of this.entries.values()) safeRelease(entry.request);
|
|
456
586
|
this.entries.clear();
|
|
457
587
|
this.evictedDeviceIds.clear();
|
|
588
|
+
this.sweptByAge.clear();
|
|
458
589
|
}
|
|
459
|
-
/**
|
|
460
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Release + drop one entry by its map key, remembering WHY it left.
|
|
592
|
+
*
|
|
593
|
+
* `age` — past the lease bound (a `lookup` refusal or a `register` sweep):
|
|
594
|
+
* remembered with its registration clock, so a later ask for the same
|
|
595
|
+
* handle is still answered `expired` with a real age. `capacity` — the cap
|
|
596
|
+
* bound and pushed it out; `released` — the caller was done with it. Those
|
|
597
|
+
* two stay `unknown` on a later ask: neither is a late ask, and folding
|
|
598
|
+
* them into the refusal count would make the bound look tighter than it is.
|
|
599
|
+
*/
|
|
600
|
+
releaseEntry(key, cause = "released") {
|
|
461
601
|
const entry = this.entries.get(key);
|
|
462
602
|
if (!entry) return;
|
|
463
603
|
this.entries.delete(key);
|
|
464
|
-
this.
|
|
604
|
+
if (cause === "age") this.rememberSweptByAge(key, entry);
|
|
605
|
+
else this.rememberEvicted(key, entry.deviceId);
|
|
465
606
|
safeRelease(entry.request);
|
|
466
607
|
}
|
|
467
608
|
/** Bounded memory of evicted handles → deviceId, for miss attribution. */
|
|
@@ -474,6 +615,24 @@ var NativeCropRegistry = class {
|
|
|
474
615
|
this.evictedDeviceIds.delete(oldest);
|
|
475
616
|
}
|
|
476
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Bounded memory of handles the AGE bound dropped, with the clock the
|
|
620
|
+
* refusal is measured from. Same capacity as the live map: at the busiest
|
|
621
|
+
* node's rate that is one more lease window of memory, after which a
|
|
622
|
+
* refusal degrades to `unknown` — an ask that late is not a bound question.
|
|
623
|
+
*/
|
|
624
|
+
sweptByAge = /* @__PURE__ */ new Map();
|
|
625
|
+
rememberSweptByAge(key, entry) {
|
|
626
|
+
this.sweptByAge.set(key, {
|
|
627
|
+
deviceId: entry.deviceId,
|
|
628
|
+
registeredAt: entry.registeredAt
|
|
629
|
+
});
|
|
630
|
+
while (this.sweptByAge.size > this.capacity) {
|
|
631
|
+
const oldest = this.sweptByAge.keys().next().value;
|
|
632
|
+
if (oldest === void 0) break;
|
|
633
|
+
this.sweptByAge.delete(oldest);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
477
636
|
};
|
|
478
637
|
//#endregion
|
|
479
638
|
//#region src/session-decode/node-decode-budget.ts
|
|
@@ -568,6 +727,160 @@ function createRetainedContentFallback(registry, ref) {
|
|
|
568
727
|
}
|
|
569
728
|
} };
|
|
570
729
|
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/session-decode/parked-frame-store.ts
|
|
732
|
+
/** Per-camera parcel bound. 24 live tracks × two kinds on one camera is a
|
|
733
|
+
* crowd scene; past that the camera's oldest parcel leaves first. Read ONCE
|
|
734
|
+
* from `CAMSTACK_PARKED_FRAMES_PER_DEVICE`. */
|
|
735
|
+
var PARKED_FRAMES_PER_DEVICE_MAX = (() => {
|
|
736
|
+
const raw = Number(process.env["CAMSTACK_PARKED_FRAMES_PER_DEVICE"]);
|
|
737
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 48;
|
|
738
|
+
})();
|
|
739
|
+
/** {@link PARKED_FRAME_BUDGET_MB} in bytes. */
|
|
740
|
+
var PARKED_FRAME_BUDGET_BYTES = (() => {
|
|
741
|
+
const raw = Number(process.env["CAMSTACK_PARKED_FRAME_MB"]);
|
|
742
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 64;
|
|
743
|
+
})() * 1024 * 1024;
|
|
744
|
+
function keyOf$1(deviceId, trackId, kind) {
|
|
745
|
+
return `${deviceId}|${trackId}|${kind}`;
|
|
746
|
+
}
|
|
747
|
+
var ParkedFrameStore = class {
|
|
748
|
+
budgetBytes;
|
|
749
|
+
perDeviceMax;
|
|
750
|
+
maxAgeMs;
|
|
751
|
+
now;
|
|
752
|
+
onEvict;
|
|
753
|
+
/** Insertion-ordered → the head is the globally oldest park. A replace
|
|
754
|
+
* re-inserts, so order IS park order. */
|
|
755
|
+
entries = /* @__PURE__ */ new Map();
|
|
756
|
+
/** Per-device keys in park order, mirroring {@link entries} for the bound. */
|
|
757
|
+
perDeviceKeys = /* @__PURE__ */ new Map();
|
|
758
|
+
totalBytes = 0;
|
|
759
|
+
constructor(options = {}) {
|
|
760
|
+
this.budgetBytes = options.budgetBytes ?? PARKED_FRAME_BUDGET_BYTES;
|
|
761
|
+
this.perDeviceMax = options.perDeviceMax ?? PARKED_FRAMES_PER_DEVICE_MAX;
|
|
762
|
+
this.maxAgeMs = options.maxAgeMs ?? 6e5;
|
|
763
|
+
this.now = options.now ?? Date.now;
|
|
764
|
+
this.onEvict = options.onEvict;
|
|
765
|
+
}
|
|
766
|
+
/** Number of parcels held (tests / metrics). */
|
|
767
|
+
get size() {
|
|
768
|
+
return this.entries.size;
|
|
769
|
+
}
|
|
770
|
+
/** Bytes of JPEG held (tests / metrics). */
|
|
771
|
+
get bytes() {
|
|
772
|
+
return this.totalBytes;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Park one parcel. Replaces an existing parcel for the key unless the new
|
|
776
|
+
* one shows an OLDER frame instant (a racing loser). Sweeps the age ceiling,
|
|
777
|
+
* then enforces the per-device bound and the node budget — evicting the
|
|
778
|
+
* oldest of the offending scope, never refusing the new parcel.
|
|
779
|
+
*/
|
|
780
|
+
park(deviceId, trackId, kind, frame) {
|
|
781
|
+
const nowMs = this.now();
|
|
782
|
+
this.sweepExpired(nowMs);
|
|
783
|
+
const key = keyOf$1(deviceId, trackId, kind);
|
|
784
|
+
const existing = this.entries.get(key);
|
|
785
|
+
if (existing !== void 0 && frame.timestamp < existing.frame.timestamp) return "older-ignored";
|
|
786
|
+
if (existing !== void 0) this.remove(key);
|
|
787
|
+
this.entries.set(key, {
|
|
788
|
+
deviceId,
|
|
789
|
+
trackId,
|
|
790
|
+
kind,
|
|
791
|
+
frame,
|
|
792
|
+
parkedAt: nowMs
|
|
793
|
+
});
|
|
794
|
+
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
795
|
+
if (deviceKeys) deviceKeys.push(key);
|
|
796
|
+
else this.perDeviceKeys.set(deviceId, [key]);
|
|
797
|
+
this.totalBytes += frame.jpeg.byteLength;
|
|
798
|
+
this.enforceDeviceBound(deviceId);
|
|
799
|
+
this.enforceBudget();
|
|
800
|
+
return existing === void 0 ? "stored" : "replaced";
|
|
801
|
+
}
|
|
802
|
+
/** The parcel for a key, or `null` — never anything else, never by handle. */
|
|
803
|
+
get(deviceId, trackId, kind) {
|
|
804
|
+
const entry = this.entries.get(keyOf$1(deviceId, trackId, kind));
|
|
805
|
+
if (entry === void 0) return null;
|
|
806
|
+
if (this.now() - entry.parkedAt > this.maxAgeMs) {
|
|
807
|
+
this.evict(keyOf$1(deviceId, trackId, kind), "age");
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
...entry.frame,
|
|
812
|
+
parkedAt: entry.parkedAt
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/** Drop every parcel of a track (the hub closed it). Returns how many left. */
|
|
816
|
+
release(deviceId, trackId) {
|
|
817
|
+
const keys = (this.perDeviceKeys.get(deviceId) ?? []).filter((key) => this.entries.get(key)?.trackId === trackId);
|
|
818
|
+
for (const key of keys) this.remove(key);
|
|
819
|
+
return keys.length;
|
|
820
|
+
}
|
|
821
|
+
/** What one camera is costing right now. */
|
|
822
|
+
statsByDevice(deviceId) {
|
|
823
|
+
const keys = this.perDeviceKeys.get(deviceId) ?? [];
|
|
824
|
+
let bytes = 0;
|
|
825
|
+
for (const key of keys) bytes += this.entries.get(key)?.frame.jpeg.byteLength ?? 0;
|
|
826
|
+
return {
|
|
827
|
+
parcels: keys.length,
|
|
828
|
+
bytes
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/** Drop everything (addon shutdown). */
|
|
832
|
+
clear() {
|
|
833
|
+
this.entries.clear();
|
|
834
|
+
this.perDeviceKeys.clear();
|
|
835
|
+
this.totalBytes = 0;
|
|
836
|
+
}
|
|
837
|
+
/** Age sweep from the head — insertion order is park order, so it stops at
|
|
838
|
+
* the first parcel still inside the ceiling. */
|
|
839
|
+
sweepExpired(nowMs) {
|
|
840
|
+
for (const [key, entry] of this.entries) {
|
|
841
|
+
if (nowMs - entry.parkedAt <= this.maxAgeMs) return;
|
|
842
|
+
this.evict(key, "age");
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
enforceDeviceBound(deviceId) {
|
|
846
|
+
const deviceKeys = this.perDeviceKeys.get(deviceId);
|
|
847
|
+
while (deviceKeys !== void 0 && deviceKeys.length > this.perDeviceMax) {
|
|
848
|
+
const oldest = deviceKeys[0];
|
|
849
|
+
if (oldest === void 0) break;
|
|
850
|
+
this.evict(oldest, "device-bound");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/** Globally oldest first, whatever its device; always keeps ≥ 1. */
|
|
854
|
+
enforceBudget() {
|
|
855
|
+
for (const key of [...this.entries.keys()]) {
|
|
856
|
+
if (this.totalBytes <= this.budgetBytes || this.entries.size <= 1) return;
|
|
857
|
+
this.evict(key, "node-budget");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
evict(key, cause) {
|
|
861
|
+
const entry = this.entries.get(key);
|
|
862
|
+
if (entry === void 0) return;
|
|
863
|
+
this.remove(key);
|
|
864
|
+
this.onEvict?.({
|
|
865
|
+
deviceId: entry.deviceId,
|
|
866
|
+
trackId: entry.trackId,
|
|
867
|
+
kind: entry.kind,
|
|
868
|
+
cause
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
remove(key) {
|
|
872
|
+
const entry = this.entries.get(key);
|
|
873
|
+
if (entry === void 0) return;
|
|
874
|
+
this.entries.delete(key);
|
|
875
|
+
this.totalBytes -= entry.frame.jpeg.byteLength;
|
|
876
|
+
const deviceKeys = this.perDeviceKeys.get(entry.deviceId);
|
|
877
|
+
if (deviceKeys) {
|
|
878
|
+
const idx = deviceKeys.indexOf(key);
|
|
879
|
+
if (idx >= 0) deviceKeys.splice(idx, 1);
|
|
880
|
+
if (deviceKeys.length === 0) this.perDeviceKeys.delete(entry.deviceId);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
571
884
|
/** {@link RETAINED_FRAME_BUDGET_MB} in bytes. */
|
|
572
885
|
var RETAINED_FRAME_BUDGET_BYTES = (() => {
|
|
573
886
|
const raw = Number(process.env["CAMSTACK_SESSION_RETAIN_MB"]);
|
|
@@ -6031,6 +6344,90 @@ function isUsable(bbox) {
|
|
|
6031
6344
|
return bbox.width > 0 && bbox.height > 0;
|
|
6032
6345
|
}
|
|
6033
6346
|
//#endregion
|
|
6347
|
+
//#region src/pipeline-runner/track-frame-park.ts
|
|
6348
|
+
/** Park one frame for one track. Never throws; a refusal is an answer. */
|
|
6349
|
+
async function parkTrackFrame(deps, input) {
|
|
6350
|
+
const { deviceId, trackId, kind } = input;
|
|
6351
|
+
const handleKey = `${input.handle.shmId}#${input.handle.slot}#${input.handle.seq}`;
|
|
6352
|
+
const resolved = await deps.resolvePixels({
|
|
6353
|
+
handle: input.handle,
|
|
6354
|
+
bbox: input.bbox,
|
|
6355
|
+
...input.maxWidth !== void 0 ? { maxWidth: input.maxWidth } : {}
|
|
6356
|
+
});
|
|
6357
|
+
if (resolved.pixels === null || resolved.tier !== "native") {
|
|
6358
|
+
const missReason = resolved.missReason ?? "worker-lease-gone";
|
|
6359
|
+
const detail = resolved.workerReason !== void 0 ? `${missReason}: ${resolved.workerReason}` : missReason;
|
|
6360
|
+
deps.logger.warn("track frame park refused — the handle no longer resolves at park time", {
|
|
6361
|
+
tags: { deviceId },
|
|
6362
|
+
meta: {
|
|
6363
|
+
trackId,
|
|
6364
|
+
kind,
|
|
6365
|
+
reason: "handle-gone",
|
|
6366
|
+
missReason,
|
|
6367
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6368
|
+
servedTier: resolved.tier,
|
|
6369
|
+
handle: handleKey,
|
|
6370
|
+
...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
|
|
6371
|
+
}
|
|
6372
|
+
});
|
|
6373
|
+
return {
|
|
6374
|
+
parked: false,
|
|
6375
|
+
nodeId: deps.nodeId,
|
|
6376
|
+
reason: "handle-gone",
|
|
6377
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6378
|
+
detail
|
|
6379
|
+
};
|
|
6380
|
+
}
|
|
6381
|
+
const jpeg = await deps.encodeJpeg(resolved.pixels);
|
|
6382
|
+
if (jpeg === null) {
|
|
6383
|
+
deps.logger.warn("track frame park refused — JPEG encode failed", {
|
|
6384
|
+
tags: { deviceId },
|
|
6385
|
+
meta: {
|
|
6386
|
+
trackId,
|
|
6387
|
+
kind,
|
|
6388
|
+
reason: "encode-failed",
|
|
6389
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6390
|
+
handle: handleKey,
|
|
6391
|
+
width: resolved.pixels.width,
|
|
6392
|
+
height: resolved.pixels.height
|
|
6393
|
+
}
|
|
6394
|
+
});
|
|
6395
|
+
return {
|
|
6396
|
+
parked: false,
|
|
6397
|
+
nodeId: deps.nodeId,
|
|
6398
|
+
reason: "encode-failed",
|
|
6399
|
+
handleAgeMs: resolved.handleAgeMs
|
|
6400
|
+
};
|
|
6401
|
+
}
|
|
6402
|
+
const verdict = deps.store.park(deviceId, trackId, kind, {
|
|
6403
|
+
jpeg,
|
|
6404
|
+
width: resolved.pixels.width,
|
|
6405
|
+
height: resolved.pixels.height,
|
|
6406
|
+
timestamp: input.timestamp
|
|
6407
|
+
});
|
|
6408
|
+
deps.logger.debug("track frame parked", {
|
|
6409
|
+
tags: { deviceId },
|
|
6410
|
+
meta: {
|
|
6411
|
+
trackId,
|
|
6412
|
+
kind,
|
|
6413
|
+
verdict,
|
|
6414
|
+
bytes: jpeg.byteLength,
|
|
6415
|
+
width: resolved.pixels.width,
|
|
6416
|
+
height: resolved.pixels.height,
|
|
6417
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
6418
|
+
source: resolved.pixels.source ?? "lease"
|
|
6419
|
+
}
|
|
6420
|
+
});
|
|
6421
|
+
return {
|
|
6422
|
+
parked: true,
|
|
6423
|
+
nodeId: deps.nodeId,
|
|
6424
|
+
width: resolved.pixels.width,
|
|
6425
|
+
height: resolved.pixels.height,
|
|
6426
|
+
bytes: jpeg.byteLength,
|
|
6427
|
+
replaced: verdict === "replaced"
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
//#endregion
|
|
6034
6431
|
//#region src/pipeline-runner/index.ts
|
|
6035
6432
|
/**
|
|
6036
6433
|
* addon-pipeline-runner — per-node detection scheduler.
|
|
@@ -6100,6 +6497,8 @@ var PACKAGE_ZONE_UNION_TTL_MS = 6e4;
|
|
|
6100
6497
|
var MAX_BOUND_FRAME_CUTTERS = 256;
|
|
6101
6498
|
/** How often a camera reports its undispatched cuts. */
|
|
6102
6499
|
var TILE_CUT_DROP_LOG_INTERVAL_MS = 6e4;
|
|
6500
|
+
/** How often a camera reports parcels that left without a release (D422). */
|
|
6501
|
+
var PARKED_EVICTION_LOG_INTERVAL_MS = 6e4;
|
|
6103
6502
|
/**
|
|
6104
6503
|
* How often a camera reports what carrying its birth evidence COST (D379).
|
|
6105
6504
|
*
|
|
@@ -6129,7 +6528,6 @@ var DEFAULT_CONFIG = {
|
|
|
6129
6528
|
* this window REUSES the warm worker instead of re-forking it — killing the
|
|
6130
6529
|
* spawn/teardown churn (and its cold-start latency spike) that on-motion
|
|
6131
6530
|
* cameras produced when every gate open/close forked and killed a worker.
|
|
6132
|
-
* Mirrors Scrypted's decode-fork ~15s idle-timeout.
|
|
6133
6531
|
*/
|
|
6134
6532
|
/** JPEG quality for the runner→executor detection-frame hop. q82 keeps
|
|
6135
6533
|
* detector-relevant detail (source is h264 anyway) at ~10-15× less payload
|
|
@@ -6487,6 +6885,17 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6487
6885
|
* Initialised in {@link onInitialize} once `nodeId` is known.
|
|
6488
6886
|
*/
|
|
6489
6887
|
retainedFrames = null;
|
|
6888
|
+
/**
|
|
6889
|
+
* Per-track PARCELS (D422): one JPEG per `(deviceId, trackId, kind)`, cut
|
|
6890
|
+
* from the native surface at the instant the hub judged the frame relevant,
|
|
6891
|
+
* retrieved later by that key — never by handle, never by age. Bounded per
|
|
6892
|
+
* device and per node in `parked-frame-store.ts`, where the numbers are
|
|
6893
|
+
* stated. Initialised in {@link onInitialize} beside the retained store.
|
|
6894
|
+
*/
|
|
6895
|
+
parkedFrames = null;
|
|
6896
|
+
/** Per-camera tally of parcels that left WITHOUT a release, by cause. */
|
|
6897
|
+
parkedEvictions = /* @__PURE__ */ new Map();
|
|
6898
|
+
parkedEvictionLoggedAt = /* @__PURE__ */ new Map();
|
|
6490
6899
|
frameLazy = new FrameLazyTelemetry();
|
|
6491
6900
|
retention = null;
|
|
6492
6901
|
attached = /* @__PURE__ */ new Map();
|
|
@@ -6717,6 +7126,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6717
7126
|
nodeId: this.nodeId,
|
|
6718
7127
|
continuousRetain: isContinuousRetainEnabled()
|
|
6719
7128
|
});
|
|
7129
|
+
this.parkedFrames = new ParkedFrameStore({ onEvict: (eviction) => this.noteParkedEviction(eviction.deviceId, eviction.cause) });
|
|
6720
7130
|
this.runner = new PipelineRunner({
|
|
6721
7131
|
maxQueueDepth: this.config.maxQueueDepth,
|
|
6722
7132
|
maxConcurrentInferences: this.config.maxConcurrentInferences,
|
|
@@ -6868,6 +7278,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
6868
7278
|
this.nativeCropRegistry.clear();
|
|
6869
7279
|
this.retainedFrames?.clear();
|
|
6870
7280
|
this.retainedFrames = null;
|
|
7281
|
+
this.parkedFrames?.clear();
|
|
7282
|
+
this.parkedFrames = null;
|
|
6871
7283
|
this.retention = null;
|
|
6872
7284
|
}
|
|
6873
7285
|
async cacheBenchFrame(input) {
|
|
@@ -7662,6 +8074,96 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7662
8074
|
}, resolved.tier, input.encodeJpeg);
|
|
7663
8075
|
}
|
|
7664
8076
|
/**
|
|
8077
|
+
* PARK a frame for a track (D422) — the hub judged it relevant, and this node
|
|
8078
|
+
* still holds its pixels. Same native-lease resolve `getNativeCrop` uses,
|
|
8079
|
+
* same single encoder; the parcel lives in {@link parkedFrames} under
|
|
8080
|
+
* `(deviceId, trackId, kind)`. A refusal is an answer and a `deviceId`-tagged
|
|
8081
|
+
* log line, never a retry.
|
|
8082
|
+
*/
|
|
8083
|
+
async parkTrackFrame(input) {
|
|
8084
|
+
const store = this.parkedFrames;
|
|
8085
|
+
if (store === null) return {
|
|
8086
|
+
parked: false,
|
|
8087
|
+
nodeId: this.nodeId,
|
|
8088
|
+
reason: "handle-gone",
|
|
8089
|
+
handleAgeMs: null,
|
|
8090
|
+
detail: "runner not initialised"
|
|
8091
|
+
};
|
|
8092
|
+
return parkTrackFrame({
|
|
8093
|
+
nodeId: this.nodeId,
|
|
8094
|
+
store,
|
|
8095
|
+
resolvePixels: (request) => this.resolveNativeCropPixels(request),
|
|
8096
|
+
encodeJpeg: async (pixels) => {
|
|
8097
|
+
const encoded = await this.encodeNativeCropResult(pixels, "native", true);
|
|
8098
|
+
return encoded?.jpeg !== void 0 ? Buffer.from(encoded.jpeg, "base64") : null;
|
|
8099
|
+
},
|
|
8100
|
+
logger: this.ctx.logger
|
|
8101
|
+
}, input);
|
|
8102
|
+
}
|
|
8103
|
+
/** A parcel by `(deviceId, trackId, kind)`; `null` = nothing parked here. */
|
|
8104
|
+
async getParkedTrackFrame(input) {
|
|
8105
|
+
const parcel = this.parkedFrames?.get(input.deviceId, input.trackId, input.kind) ?? null;
|
|
8106
|
+
if (parcel === null) {
|
|
8107
|
+
this.ctxIfReady?.logger.debug("parked frame miss — nothing under this key", {
|
|
8108
|
+
tags: { deviceId: input.deviceId },
|
|
8109
|
+
meta: {
|
|
8110
|
+
trackId: input.trackId,
|
|
8111
|
+
kind: input.kind,
|
|
8112
|
+
...this.parkedFrames?.statsByDevice(input.deviceId) ?? {
|
|
8113
|
+
parcels: 0,
|
|
8114
|
+
bytes: 0
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
});
|
|
8118
|
+
return null;
|
|
8119
|
+
}
|
|
8120
|
+
return {
|
|
8121
|
+
jpeg: parcel.jpeg.toString("base64"),
|
|
8122
|
+
width: parcel.width,
|
|
8123
|
+
height: parcel.height,
|
|
8124
|
+
timestamp: parcel.timestamp,
|
|
8125
|
+
parkedAt: parcel.parkedAt
|
|
8126
|
+
};
|
|
8127
|
+
}
|
|
8128
|
+
/** The hub closed a track: drop its parcels. Idempotent. */
|
|
8129
|
+
async releaseParkedTrackFrames(input) {
|
|
8130
|
+
return { released: this.parkedFrames?.release(input.deviceId, input.trackId) ?? 0 };
|
|
8131
|
+
}
|
|
8132
|
+
/**
|
|
8133
|
+
* A parcel left without a release. Counted per camera by cause and reported
|
|
8134
|
+
* one line per camera per minute: a camera whose parcels keep leaving under
|
|
8135
|
+
* the device bound is a crowd, under the node budget is a neighbour's
|
|
8136
|
+
* traffic, and under the age ceiling is a hub that stopped releasing.
|
|
8137
|
+
*/
|
|
8138
|
+
noteParkedEviction(deviceId, cause) {
|
|
8139
|
+
const tally = this.parkedEvictions.get(deviceId) ?? {
|
|
8140
|
+
deviceBound: 0,
|
|
8141
|
+
nodeBudget: 0,
|
|
8142
|
+
age: 0
|
|
8143
|
+
};
|
|
8144
|
+
if (cause === "device-bound") tally.deviceBound += 1;
|
|
8145
|
+
else if (cause === "node-budget") tally.nodeBudget += 1;
|
|
8146
|
+
else tally.age += 1;
|
|
8147
|
+
this.parkedEvictions.set(deviceId, tally);
|
|
8148
|
+
const now = Date.now();
|
|
8149
|
+
if (now - (this.parkedEvictionLoggedAt.get(deviceId) ?? 0) < PARKED_EVICTION_LOG_INTERVAL_MS) return;
|
|
8150
|
+
this.parkedEvictionLoggedAt.set(deviceId, now);
|
|
8151
|
+
this.ctxIfReady?.logger.info("parked frames evicted without a release", {
|
|
8152
|
+
tags: { deviceId },
|
|
8153
|
+
meta: {
|
|
8154
|
+
...tally,
|
|
8155
|
+
lastCause: cause,
|
|
8156
|
+
parcelsHeld: this.parkedFrames?.size ?? 0,
|
|
8157
|
+
bytesHeld: this.parkedFrames?.bytes ?? 0
|
|
8158
|
+
}
|
|
8159
|
+
});
|
|
8160
|
+
this.parkedEvictions.set(deviceId, {
|
|
8161
|
+
deviceBound: 0,
|
|
8162
|
+
nodeBudget: 0,
|
|
8163
|
+
age: 0
|
|
8164
|
+
});
|
|
8165
|
+
}
|
|
8166
|
+
/**
|
|
7665
8167
|
* Turn a finished frame into subject TILES and end its hold — the runner half
|
|
7666
8168
|
* of R2 (`docs/design/2026-08-13-native-lease-two-tier-redesign.md`).
|
|
7667
8169
|
*
|
|
@@ -7931,11 +8433,14 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7931
8433
|
let nativeResult = null;
|
|
7932
8434
|
let missReason = null;
|
|
7933
8435
|
let handleAgeMs = null;
|
|
8436
|
+
/** The camera the lookup attributed the handle to, live or expired. */
|
|
8437
|
+
let deviceIdForMiss = null;
|
|
7934
8438
|
let workerReason;
|
|
7935
8439
|
if (!isUnboundedFullFrame) {
|
|
7936
|
-
const found = this.nativeCropRegistry.
|
|
7937
|
-
handleAgeMs = found
|
|
7938
|
-
|
|
8440
|
+
const found = this.nativeCropRegistry.lookup(input.handle);
|
|
8441
|
+
handleAgeMs = found.kind === "unknown" ? null : found.ageMs;
|
|
8442
|
+
deviceIdForMiss = found.kind === "unknown" ? null : found.deviceId;
|
|
8443
|
+
if (found.kind === "live") {
|
|
7939
8444
|
const attempt = await found.request(input.bbox, input.maxWidth);
|
|
7940
8445
|
if (attempt.kind === "crop") nativeResult = {
|
|
7941
8446
|
bytes: attempt.crop.bytes,
|
|
@@ -7947,11 +8452,12 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7947
8452
|
missReason = "worker-lease-gone";
|
|
7948
8453
|
workerReason = attempt.workerReason;
|
|
7949
8454
|
}
|
|
7950
|
-
} else missReason = "
|
|
8455
|
+
} else if (found.kind === "expired") missReason = "handle-expired";
|
|
8456
|
+
else missReason = "no-registry-entry";
|
|
7951
8457
|
if (missReason !== null) {
|
|
7952
8458
|
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
7953
8459
|
const fields = {
|
|
7954
|
-
tags: { deviceId:
|
|
8460
|
+
tags: { deviceId: deviceIdForMiss ?? this.nativeCropRegistry.deviceIdFor(input.handle) ?? -1 },
|
|
7955
8461
|
meta: {
|
|
7956
8462
|
reason: missReason,
|
|
7957
8463
|
servedTier: tierOnMiss,
|
|
@@ -7963,8 +8469,16 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7963
8469
|
...workerReason !== void 0 ? { workerReason } : {}
|
|
7964
8470
|
}
|
|
7965
8471
|
};
|
|
7966
|
-
|
|
7967
|
-
|
|
8472
|
+
const message = missReason === "handle-expired" ? "native crop refused — the handle is past the lease age bound" : "native crop miss";
|
|
8473
|
+
const withBound = missReason === "handle-expired" ? {
|
|
8474
|
+
...fields,
|
|
8475
|
+
meta: {
|
|
8476
|
+
...fields.meta,
|
|
8477
|
+
leaseMaxAgeMs: NATIVE_CROP_LEASE_MAX_AGE_MS
|
|
8478
|
+
}
|
|
8479
|
+
} : fields;
|
|
8480
|
+
if (tierOnMiss === "miss") this.ctx.logger.warn(message, withBound);
|
|
8481
|
+
else this.ctx.logger.debug(message, withBound);
|
|
7968
8482
|
}
|
|
7969
8483
|
}
|
|
7970
8484
|
return {
|
|
@@ -9880,6 +10394,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
9880
10394
|
frame: result,
|
|
9881
10395
|
nodeId: this.nodeId,
|
|
9882
10396
|
frameHandle: handle,
|
|
10397
|
+
emittedAt: Date.now(),
|
|
9883
10398
|
...typeof capturedAt === "number" && capturedAt > 0 ? { capturedAt } : {},
|
|
9884
10399
|
...detailSteps && detailSteps.length > 0 ? { detailSteps } : {}
|
|
9885
10400
|
};
|