@camstack/addon-pipeline 1.2.69 → 1.2.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +32 -30
- package/dist/detection-pipeline/index.mjs +32 -30
- package/dist/{dist-tJF2BzWi.js → dist-CtOVWTCi.js} +586 -41
- package/dist/{dist-DGLqaonv.mjs → dist-D1Xk5Tji.mjs} +586 -41
- package/dist/{event-loop-stall-monitor-D9d2e49f.js → event-loop-stall-monitor-BJs7Yws7.js} +1 -1
- package/dist/{event-loop-stall-monitor-C6_VDupx.mjs → event-loop-stall-monitor-Cmbt664E.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +100 -30
- package/dist/pipeline-runner/index.mjs +100 -30
- package/dist/recorder/index.js +734 -104
- package/dist/recorder/index.mjs +734 -104
- package/dist/session-decode/decode-worker-child.js +483 -52
- package/dist/session-decode/decode-worker-child.mjs +483 -52
- package/dist/stream-broker/_stub.js +660 -556
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DCyQ7RKH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BWf4H4Nl.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cu_kQpIy.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-CExO0piw.mjs +26 -0
- package/dist/stream-broker/{hostInit-DxqtIjAe.mjs → hostInit-DD5Z1U6p.mjs} +3 -3
- package/dist/stream-broker/index.js +501 -32
- package/dist/stream-broker/index.mjs +501 -32
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-D6tXz9Or.js → worker-protocol-DEV7g32b.js} +26 -19
- package/dist/{worker-protocol-DuftUFEI.mjs → worker-protocol-Dm6LQ9Ls.mjs} +26 -19
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ircnzt0s.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DU7u9EJt.mjs +0 -26
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-Dm6LQ9Ls.mjs";
|
|
2
|
+
import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import * as path$1 from "node:path";
|
|
@@ -535,20 +536,31 @@ var NativeFrameRing = class {
|
|
|
535
536
|
//#endregion
|
|
536
537
|
//#region src/session-decode/native-lease-store.ts
|
|
537
538
|
/**
|
|
538
|
-
* A
|
|
539
|
-
*
|
|
539
|
+
* A count- and byte-bounded, release-driven map from worker frameId → held RAM
|
|
540
|
+
* frame. Insertion-ordered (`Map`), so every eviction is FIFO by age.
|
|
540
541
|
*/
|
|
541
542
|
var NativeLeaseStore = class {
|
|
542
543
|
entries = /* @__PURE__ */ new Map();
|
|
543
544
|
budgetBytes;
|
|
544
|
-
|
|
545
|
+
maxFrames;
|
|
545
546
|
now;
|
|
546
547
|
bytes = 0;
|
|
548
|
+
/**
|
|
549
|
+
* Frames dropped because the hold bound or the byte ceiling was reached
|
|
550
|
+
* before their release arrived. Non-zero means the runner is not keeping up —
|
|
551
|
+
* a different sentence from "the crop was late", and the reason this is a
|
|
552
|
+
* counter and not a silent eviction.
|
|
553
|
+
*/
|
|
554
|
+
overflowed = 0;
|
|
547
555
|
constructor(options) {
|
|
548
556
|
this.budgetBytes = options.budgetBytes;
|
|
549
|
-
this.
|
|
557
|
+
this.maxFrames = Math.max(1, options.maxFrames);
|
|
550
558
|
this.now = options.now ?? Date.now;
|
|
551
559
|
}
|
|
560
|
+
/** Frames dropped by a bound rather than released (tests / metrics). */
|
|
561
|
+
get overflowCount() {
|
|
562
|
+
return this.overflowed;
|
|
563
|
+
}
|
|
552
564
|
/** `false` when the store is disabled (`budgetBytes <= 0`). */
|
|
553
565
|
get enabled() {
|
|
554
566
|
return this.budgetBytes > 0;
|
|
@@ -574,43 +586,42 @@ var NativeLeaseStore = class {
|
|
|
574
586
|
return false;
|
|
575
587
|
}
|
|
576
588
|
this.drop(frameId);
|
|
577
|
-
this.sweepExpired();
|
|
578
589
|
this.entries.set(frameId, {
|
|
579
590
|
frame,
|
|
580
591
|
insertedAt: this.now()
|
|
581
592
|
});
|
|
582
593
|
this.bytes += frame.byteLength;
|
|
583
|
-
while (this.bytes > this.budgetBytes && this.entries.size > 0) {
|
|
594
|
+
while ((this.entries.size > this.maxFrames || this.bytes > this.budgetBytes) && this.entries.size > 0) {
|
|
584
595
|
const oldest = this.entries.keys().next().value;
|
|
585
596
|
if (oldest === void 0) break;
|
|
597
|
+
if (oldest === frameId) break;
|
|
598
|
+
this.overflowed += 1;
|
|
586
599
|
this.drop(oldest);
|
|
587
600
|
}
|
|
588
601
|
return true;
|
|
589
602
|
}
|
|
590
603
|
/**
|
|
591
|
-
* The
|
|
592
|
-
* released/evicted
|
|
593
|
-
*
|
|
604
|
+
* The held frame for `frameId`, or `null` if never held or already
|
|
605
|
+
* released/evicted. No age check: a frame is held until an EVENT ends the
|
|
606
|
+
* hold, and a store that also expired on a clock would reintroduce the
|
|
607
|
+
* unreachable window this design removed.
|
|
594
608
|
*/
|
|
595
609
|
get(frameId) {
|
|
610
|
+
return this.entries.get(frameId)?.frame ?? null;
|
|
611
|
+
}
|
|
612
|
+
/** How long `frameId` has been held, or `null` when it is not held. */
|
|
613
|
+
ageMs(frameId) {
|
|
596
614
|
const entry = this.entries.get(frameId);
|
|
597
|
-
|
|
598
|
-
if (this.now() - entry.insertedAt > this.ttlMs) {
|
|
599
|
-
this.drop(frameId);
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
602
|
-
return entry.frame;
|
|
615
|
+
return entry ? this.now() - entry.insertedAt : null;
|
|
603
616
|
}
|
|
604
|
-
/**
|
|
617
|
+
/**
|
|
618
|
+
* Free + drop the hold for `frameId` — the runner has this frame's result and
|
|
619
|
+
* has taken what it wanted. Idempotent, and the ORDINARY exit.
|
|
620
|
+
*/
|
|
605
621
|
release(frameId) {
|
|
606
622
|
this.drop(frameId);
|
|
607
623
|
}
|
|
608
|
-
/** Free + drop every
|
|
609
|
-
sweepExpired() {
|
|
610
|
-
const cutoff = this.now() - this.ttlMs;
|
|
611
|
-
for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
|
|
612
|
-
}
|
|
613
|
-
/** Free + drop every retained lease. Idempotent (re-dial + teardown call it). */
|
|
624
|
+
/** Free + drop every held frame. Idempotent (re-dial + teardown call it). */
|
|
614
625
|
clear() {
|
|
615
626
|
for (const entry of this.entries.values()) entry.frame.free();
|
|
616
627
|
this.entries.clear();
|
|
@@ -626,6 +637,244 @@ var NativeLeaseStore = class {
|
|
|
626
637
|
}
|
|
627
638
|
};
|
|
628
639
|
//#endregion
|
|
640
|
+
//#region src/session-decode/native-tile-geometry.ts
|
|
641
|
+
/**
|
|
642
|
+
* Pure geometry for the **native subject tile** — the compressed, subject-sized
|
|
643
|
+
* container that replaces retaining a whole native frame.
|
|
644
|
+
*
|
|
645
|
+
* > **NOT WIRED YET.** This module is Phase 1 of
|
|
646
|
+
* > `docs/design/2026-08-13-native-lease-two-tier-redesign.md` and nothing
|
|
647
|
+
* > imports it. It landed with the design because it is the piece whose failure
|
|
648
|
+
* > is SILENT. If you find it still unimported after the next structural train,
|
|
649
|
+
* > delete it — a helper nobody calls reads as verification of a design that
|
|
650
|
+
* > was never built.
|
|
651
|
+
*
|
|
652
|
+
* ## What a tile is, and what it is emphatically not
|
|
653
|
+
*
|
|
654
|
+
* A tile is a **container**: the detection's box, padded generously, clamped
|
|
655
|
+
* inside the frame and even-aligned, cut from the native surface at the one
|
|
656
|
+
* instant the frame is still alive and kept as compressed bytes long after it
|
|
657
|
+
* is gone.
|
|
658
|
+
*
|
|
659
|
+
* It is NOT a model input and it is NOT a crop convention. The rectangle a
|
|
660
|
+
* model is fed is still derived in exactly one place — `deriveDetailCropRect`,
|
|
661
|
+
* reached only through `pipeline-runner/detail-subtree.ts`'s `cutFromFullFrame`
|
|
662
|
+
* ([D52](../../../../docs/decisions/adr-0052.md)) — and is then cut FROM the
|
|
663
|
+
* tile raster instead of from a frame that no longer exists. Two paddings are
|
|
664
|
+
* therefore never composed: the tile's padding decides only how much scene is
|
|
665
|
+
* kept available, and the convention's padding decides the rectangle, exactly
|
|
666
|
+
* as it does today against a full frame.
|
|
667
|
+
*
|
|
668
|
+
* ## The one rule this file exists to enforce
|
|
669
|
+
*
|
|
670
|
+
* **A tile serves a request only when it strictly CONTAINS the requested
|
|
671
|
+
* rectangle.** Not clamps it, not stretches it, not re-pads it — contains it.
|
|
672
|
+
* A rectangle clamped to fit inside a tile would enrich a *different* part of
|
|
673
|
+
* the scene, land in the same vector index as correctly-cut neighbours, and
|
|
674
|
+
* error nowhere: the exact shape of the defect D52 was written for. A
|
|
675
|
+
* non-contained rectangle is a MISS, and the caller falls to the next rung
|
|
676
|
+
* (re-decode from the recorded GOP).
|
|
677
|
+
*
|
|
678
|
+
* Geometry only: no pixels, no node-av, no process state.
|
|
679
|
+
*/
|
|
680
|
+
/**
|
|
681
|
+
* How much scene is kept around the detection box, as a fraction of the box's
|
|
682
|
+
* own width/height, applied on every side.
|
|
683
|
+
*
|
|
684
|
+
* Sized to sit comfortably above the detail-crop convention's own padding
|
|
685
|
+
* (0.15 today, `DEFAULT_DETAIL_CROP_CONVENTION`) so the derived model rectangle
|
|
686
|
+
* lands INSIDE the tile even after a track's box has drifted for a second or
|
|
687
|
+
* two — which is precisely the interval a tile has to survive. Raising it costs
|
|
688
|
+
* bytes quadratically; lowering it converts hits into misses. It is a default,
|
|
689
|
+
* not a convention: nothing about a vector's meaning depends on it, because
|
|
690
|
+
* nothing is ever encoded from the tile's own rectangle.
|
|
691
|
+
*/
|
|
692
|
+
var NATIVE_TILE_PADDING_RATIO = .35;
|
|
693
|
+
/**
|
|
694
|
+
* Resolve the tile container for one detection box.
|
|
695
|
+
*
|
|
696
|
+
* The box arrives NORMALISED against the analysis frame; the tile is expressed
|
|
697
|
+
* in NATIVE frame pixels, because that is the raster the worker cuts from. Both
|
|
698
|
+
* are the same scene at different scales, so the normalised box maps directly.
|
|
699
|
+
*
|
|
700
|
+
* Degenerate and out-of-frame inputs are handled by clamping to the frame — a
|
|
701
|
+
* detection touching the edge yields a smaller tile, never a read past the
|
|
702
|
+
* plane. A zero-area box widens to the 2px chroma minimum.
|
|
703
|
+
*/
|
|
704
|
+
function resolveNativeTileRegion(frameWidth, frameHeight, bbox, paddingRatio = NATIVE_TILE_PADDING_RATIO) {
|
|
705
|
+
const pad = Number.isFinite(paddingRatio) && paddingRatio > 0 ? paddingRatio : 0;
|
|
706
|
+
const boxLeft = bbox.x * frameWidth;
|
|
707
|
+
const boxTop = bbox.y * frameHeight;
|
|
708
|
+
const boxWidth = bbox.w * frameWidth;
|
|
709
|
+
const boxHeight = bbox.h * frameHeight;
|
|
710
|
+
const padX = boxWidth * pad;
|
|
711
|
+
const padY = boxHeight * pad;
|
|
712
|
+
const rawLeft = Math.max(0, boxLeft - padX);
|
|
713
|
+
const rawTop = Math.max(0, boxTop - padY);
|
|
714
|
+
const rawRight = Math.min(frameWidth, boxLeft + boxWidth + padX);
|
|
715
|
+
const rawBottom = Math.min(frameHeight, boxTop + boxHeight + padY);
|
|
716
|
+
const left = clampEven(Math.floor(rawLeft), 0, Math.max(0, frameWidth - 2));
|
|
717
|
+
const top = clampEven(Math.floor(rawTop), 0, Math.max(0, frameHeight - 2));
|
|
718
|
+
return {
|
|
719
|
+
region: {
|
|
720
|
+
left,
|
|
721
|
+
top,
|
|
722
|
+
width: clampEven(Math.ceil(rawRight) - left, 2, frameWidth - left),
|
|
723
|
+
height: clampEven(Math.ceil(rawBottom) - top, 2, frameHeight - top)
|
|
724
|
+
},
|
|
725
|
+
paddingRatio: pad
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Whether `rect` lies wholly inside `tile` — the serve gate.
|
|
730
|
+
*
|
|
731
|
+
* Deliberately strict and deliberately boolean: there is no "close enough"
|
|
732
|
+
* verdict, because the caller's only two honest options are to cut the exact
|
|
733
|
+
* rectangle or to report a miss. Touching the tile's edge counts as inside; a
|
|
734
|
+
* single pixel past it does not.
|
|
735
|
+
*/
|
|
736
|
+
function tileContains(tile, rect) {
|
|
737
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
738
|
+
return rect.left >= tile.left && rect.top >= tile.top && rect.left + rect.width <= tile.left + tile.width && rect.top + rect.height <= tile.top + tile.height;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Translate a FRAME-space rectangle into TILE-space, or `null` when the tile
|
|
742
|
+
* does not contain it.
|
|
743
|
+
*
|
|
744
|
+
* `null` is the miss the caller reports; it must never be turned into a clamp.
|
|
745
|
+
* The returned rectangle is a pure translation — no scaling, no re-alignment —
|
|
746
|
+
* so a caller that cut `rect` from the full frame and a caller that cuts the
|
|
747
|
+
* translated rectangle from the tile obtain the same pixels.
|
|
748
|
+
*/
|
|
749
|
+
function rectWithinTile(tile, rect) {
|
|
750
|
+
if (!tileContains(tile, rect)) return null;
|
|
751
|
+
return {
|
|
752
|
+
left: rect.left - tile.left,
|
|
753
|
+
top: rect.top - tile.top,
|
|
754
|
+
width: rect.width,
|
|
755
|
+
height: rect.height
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Pick the tile that can serve `rect`, or `null` when none contains it.
|
|
760
|
+
*
|
|
761
|
+
* Smallest containing tile wins: a tighter container means less scene around
|
|
762
|
+
* the subject and therefore a smaller decode, and among tiles that all satisfy
|
|
763
|
+
* the containment rule the choice cannot affect the pixels — every one of them
|
|
764
|
+
* would yield the same rectangle of the same frame.
|
|
765
|
+
*
|
|
766
|
+
* `null` is a MISS the caller reports and falls through on. It must never
|
|
767
|
+
* become "use the closest tile and clamp": see this module's header.
|
|
768
|
+
*/
|
|
769
|
+
function selectTileForRect(tiles, rect) {
|
|
770
|
+
let best = null;
|
|
771
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
772
|
+
for (const tile of tiles) {
|
|
773
|
+
const local = rectWithinTile(tile.region, rect);
|
|
774
|
+
if (local === null) continue;
|
|
775
|
+
const area = tile.region.width * tile.region.height;
|
|
776
|
+
if (area >= bestArea) continue;
|
|
777
|
+
bestArea = area;
|
|
778
|
+
best = {
|
|
779
|
+
tile,
|
|
780
|
+
local
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return best;
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/session-decode/native-tile-store.ts
|
|
787
|
+
/**
|
|
788
|
+
* A byte-bounded, TTL'd map from worker frameId → the subject tiles cut from
|
|
789
|
+
* that frame. Insertion-ordered (`Map`), so budget eviction is FIFO by age.
|
|
790
|
+
*/
|
|
791
|
+
var NativeTileStore = class {
|
|
792
|
+
entries = /* @__PURE__ */ new Map();
|
|
793
|
+
budgetBytes;
|
|
794
|
+
ttlMs;
|
|
795
|
+
now;
|
|
796
|
+
bytes = 0;
|
|
797
|
+
constructor(options) {
|
|
798
|
+
this.budgetBytes = options.budgetBytes;
|
|
799
|
+
this.ttlMs = options.ttlMs;
|
|
800
|
+
this.now = options.now ?? Date.now;
|
|
801
|
+
}
|
|
802
|
+
/** `false` when tiles are disabled (`budgetBytes <= 0`). */
|
|
803
|
+
get enabled() {
|
|
804
|
+
return this.budgetBytes > 0;
|
|
805
|
+
}
|
|
806
|
+
/** Number of frames with retained tiles (tests / metrics). */
|
|
807
|
+
get frameCount() {
|
|
808
|
+
return this.entries.size;
|
|
809
|
+
}
|
|
810
|
+
/** Total retained tiles across every frame (tests / metrics). */
|
|
811
|
+
get tileCount() {
|
|
812
|
+
let total = 0;
|
|
813
|
+
for (const entry of this.entries.values()) total += entry.tiles.length;
|
|
814
|
+
return total;
|
|
815
|
+
}
|
|
816
|
+
/** Total resident bytes (tests / metrics). */
|
|
817
|
+
get totalBytes() {
|
|
818
|
+
return this.bytes;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Retain `tiles` for `frameId`. Returns `false` when the store is disabled —
|
|
822
|
+
* the caller then simply has nothing to fall back on, exactly as before this
|
|
823
|
+
* store existed. Sweeps expired entries first, then evicts oldest-first while
|
|
824
|
+
* over budget; the entry just admitted is never the one evicted.
|
|
825
|
+
*/
|
|
826
|
+
put(frameId, tiles) {
|
|
827
|
+
if (!this.enabled || tiles.length === 0) return false;
|
|
828
|
+
this.drop(frameId);
|
|
829
|
+
this.sweepExpired();
|
|
830
|
+
let size = 0;
|
|
831
|
+
for (const tile of tiles) size += tile.bytes.byteLength;
|
|
832
|
+
this.entries.set(frameId, {
|
|
833
|
+
tiles,
|
|
834
|
+
bytes: size,
|
|
835
|
+
insertedAt: this.now()
|
|
836
|
+
});
|
|
837
|
+
this.bytes += size;
|
|
838
|
+
while (this.bytes > this.budgetBytes && this.entries.size > 1) {
|
|
839
|
+
const oldest = this.entries.keys().next().value;
|
|
840
|
+
if (oldest === void 0 || oldest === frameId) break;
|
|
841
|
+
this.drop(oldest);
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The tiles retained for `frameId`, or an empty array when there are none or
|
|
847
|
+
* they have aged out. An expired entry is dropped here so a caller reading
|
|
848
|
+
* just after expiry cannot resurrect it.
|
|
849
|
+
*/
|
|
850
|
+
get(frameId) {
|
|
851
|
+
const entry = this.entries.get(frameId);
|
|
852
|
+
if (!entry) return [];
|
|
853
|
+
if (this.now() - entry.insertedAt > this.ttlMs) {
|
|
854
|
+
this.drop(frameId);
|
|
855
|
+
return [];
|
|
856
|
+
}
|
|
857
|
+
return entry.tiles;
|
|
858
|
+
}
|
|
859
|
+
/** Drop every entry older than the TTL. */
|
|
860
|
+
sweepExpired() {
|
|
861
|
+
const cutoff = this.now() - this.ttlMs;
|
|
862
|
+
for (const [frameId, entry] of this.entries) if (entry.insertedAt < cutoff) this.drop(frameId);
|
|
863
|
+
}
|
|
864
|
+
/** Drop everything. Idempotent (re-dial + teardown call it). */
|
|
865
|
+
clear() {
|
|
866
|
+
this.entries.clear();
|
|
867
|
+
this.bytes = 0;
|
|
868
|
+
}
|
|
869
|
+
/** Remove one frame's tiles, keeping the byte tally exact. */
|
|
870
|
+
drop(frameId) {
|
|
871
|
+
const entry = this.entries.get(frameId);
|
|
872
|
+
if (!entry) return;
|
|
873
|
+
this.entries.delete(frameId);
|
|
874
|
+
this.bytes -= entry.bytes;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
//#endregion
|
|
629
878
|
//#region src/session-decode/pinned-rgb-crop.ts
|
|
630
879
|
/**
|
|
631
880
|
* Pure decision/plumbing helpers for the `pinnedRgbCrop` native-crop color fix
|
|
@@ -845,30 +1094,48 @@ var NATIVE_RING_CAP = (() => {
|
|
|
845
1094
|
*/
|
|
846
1095
|
var NATIVE_LEASE_KNOBS = resolveNativeLeaseKnobs(process.env);
|
|
847
1096
|
/**
|
|
848
|
-
* Hard RAM budget (bytes) for the native-frame
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* hits. The primary eviction is the {@link NATIVE_LEASE_TTL_MS} TTL — a 64MB
|
|
853
|
-
* budget held only ~5 native 4K frames (~0.2s), which the late crop still
|
|
854
|
-
* outran; the budget is a HIGH safety ceiling (default 1024MB) so the TTL is
|
|
855
|
-
* the effective cap and the ~40-200ms crop reliably lands within it. `0`
|
|
856
|
-
* DISABLES the lease and falls back to the {@link NATIVE_RING_CAP} GPU ring.
|
|
1097
|
+
* Hard RAM budget (bytes) for the native-frame HOLD store — the OOM ceiling,
|
|
1098
|
+
* and since 2026-08-13 nothing else. {@link NATIVE_LEASE_HOLD_FRAMES} is what
|
|
1099
|
+
* decides how much is held; this is the number above which something is wrong.
|
|
1100
|
+
* `0` DISABLES the hold and falls back to the {@link NATIVE_RING_CAP} GPU ring.
|
|
857
1101
|
*/
|
|
858
1102
|
var NATIVE_LEASE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.budgetMb * 1024 * 1024;
|
|
859
1103
|
/**
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
* tracking + the tRPC crop round-trip back — on a busy camera with many
|
|
865
|
-
* concurrent tracks that is well past the old 500 ms (which the busiest cameras'
|
|
866
|
-
* subject crops outran, missing to the ≤640 fallback). Default 1200 ms; still
|
|
867
|
-
* bounded by the per-session {@link NATIVE_LEASE_BUDGET_BYTES} ceiling and the
|
|
868
|
-
* {@link NATIVE_LEASE_ACTIVITY_WINDOW_MS} demand gate, so idle cameras retain
|
|
869
|
-
* nothing.
|
|
1104
|
+
* How many delivered frames are HELD at once, each waiting for its own
|
|
1105
|
+
* detection result. The ordinary exit is `cutTiles` / `releaseNativeLease`; this
|
|
1106
|
+
* bound only stops a runner that stopped answering from pinning RAM, and every
|
|
1107
|
+
* frame it drops is counted (`holdOverflow`).
|
|
870
1108
|
*/
|
|
871
|
-
var
|
|
1109
|
+
var NATIVE_LEASE_HOLD_FRAMES = NATIVE_LEASE_KNOBS.values.holdFrames;
|
|
1110
|
+
/**
|
|
1111
|
+
* Hard RAM budget (bytes) for the SUBJECT TILE store — what survives a frame.
|
|
1112
|
+
* `0` turns tiles off and restores the pre-2026-08-13 miss profile.
|
|
1113
|
+
*/
|
|
1114
|
+
var NATIVE_TILE_BUDGET_BYTES = NATIVE_LEASE_KNOBS.values.tileBudgetMb * 1024 * 1024;
|
|
1115
|
+
/**
|
|
1116
|
+
* How long a subject tile is served. A TTL is honest HERE and dishonest on the
|
|
1117
|
+
* hold: nothing is waiting on a tile, it is speculative retention against a
|
|
1118
|
+
* request that may never come. Sized an order of magnitude above the measured
|
|
1119
|
+
* ask age (2.2-9.9 s on this cluster) because it can be — a tile is ~60-120 KB.
|
|
1120
|
+
* Env-tunable for a bisect; not an operator knob, because the RAM it costs is
|
|
1121
|
+
* already the operator's knob.
|
|
1122
|
+
*/
|
|
1123
|
+
var NATIVE_TILE_TTL_MS = (() => {
|
|
1124
|
+
const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_TILE_TTL_MS"]);
|
|
1125
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 3e4;
|
|
1126
|
+
})();
|
|
1127
|
+
/**
|
|
1128
|
+
* Most tiles cut from ONE frame, highest-confidence first (the runner orders
|
|
1129
|
+
* them). The bound exists because the cut runs on the decode worker between two
|
|
1130
|
+
* pulls: a frame that detected twenty subjects must not turn one late crop into
|
|
1131
|
+
* twenty encodes. Anything past it is counted, not silently dropped.
|
|
1132
|
+
*/
|
|
1133
|
+
var NATIVE_TILE_MAX_PER_FRAME = (() => {
|
|
1134
|
+
const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_TILE_MAX_PER_FRAME"]);
|
|
1135
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
|
|
1136
|
+
})();
|
|
1137
|
+
/** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
|
|
1138
|
+
var NATIVE_TILE_JPEG_QUALITY = 90;
|
|
872
1139
|
/**
|
|
873
1140
|
* Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
|
|
874
1141
|
* per-frame native downloads run only within this window of the last
|
|
@@ -1175,8 +1442,31 @@ var DecodeWorkerChild = class {
|
|
|
1175
1442
|
*/
|
|
1176
1443
|
leaseStore = new NativeLeaseStore({
|
|
1177
1444
|
budgetBytes: NATIVE_LEASE_BUDGET_BYTES,
|
|
1178
|
-
|
|
1445
|
+
maxFrames: NATIVE_LEASE_HOLD_FRAMES
|
|
1446
|
+
});
|
|
1447
|
+
/**
|
|
1448
|
+
* What SURVIVES a held frame: one compressed native tile per subject, cut at
|
|
1449
|
+
* the instant the frame's detection result arrived. Every late native crop
|
|
1450
|
+
* that the hold can no longer serve is served from here — which measurement
|
|
1451
|
+
* says is the ordinary case, not the exception. See `native-tile-store.ts`.
|
|
1452
|
+
*/
|
|
1453
|
+
tileStore = new NativeTileStore({
|
|
1454
|
+
budgetBytes: NATIVE_TILE_BUDGET_BYTES,
|
|
1455
|
+
ttlMs: NATIVE_TILE_TTL_MS
|
|
1179
1456
|
});
|
|
1457
|
+
/** Tiles cut / served / refused-for-containment, on the throughput line. */
|
|
1458
|
+
tilesCut = 0;
|
|
1459
|
+
tileHits = 0;
|
|
1460
|
+
tileMisses = 0;
|
|
1461
|
+
tileCutFailures = 0;
|
|
1462
|
+
/**
|
|
1463
|
+
* Native frame dimensions the retained tiles were cut from. A session's
|
|
1464
|
+
* resolution does not change (a re-dial clears the store), so ONE pair is
|
|
1465
|
+
* enough — and it has to be remembered because a tile outlives the frame that
|
|
1466
|
+
* knew it.
|
|
1467
|
+
*/
|
|
1468
|
+
tileFrameWidth = 0;
|
|
1469
|
+
tileFrameHeight = 0;
|
|
1180
1470
|
/**
|
|
1181
1471
|
* Demand gate for lease capture — armed at dial start + on every native-crop
|
|
1182
1472
|
* request; while cold, {@link captureNativeFrame} declines retention so the
|
|
@@ -1302,6 +1592,9 @@ var DecodeWorkerChild = class {
|
|
|
1302
1592
|
case "nativeCrop":
|
|
1303
1593
|
this.handleNativeCrop(message.requestId, message.frameId, message.bbox, message.maxWidth);
|
|
1304
1594
|
return;
|
|
1595
|
+
case "cutTiles":
|
|
1596
|
+
this.handleCutTiles(message.frameId, message.bboxes);
|
|
1597
|
+
return;
|
|
1305
1598
|
case "releaseNativeLease":
|
|
1306
1599
|
this.leaseStore.release(message.frameId);
|
|
1307
1600
|
return;
|
|
@@ -1367,7 +1660,7 @@ var DecodeWorkerChild = class {
|
|
|
1367
1660
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
1368
1661
|
const rssMb = Math.round(process.memoryUsage().rss / 1048576);
|
|
1369
1662
|
const ageS = Math.round((now - this.startedAt) / 1e3);
|
|
1370
|
-
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
1663
|
+
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}, leaseOffered:${this.admission.offered}, leaseAdmitted:${this.admission.admitted}, leaseMarks:${this.admission.marks}, leaseUnmarkedCrops:${this.admission.unmarkedCrops}, leaseAdmission:${NATIVE_LEASE_KNOBS.values.admission}, leaseMb:${Math.round(this.leaseStore.totalBytes / 1048576)}, leaseFrames:${this.leaseStore.size}, holdOverflow:${this.leaseStore.overflowCount}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
1371
1664
|
this.lastMetricsAt = now;
|
|
1372
1665
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
1373
1666
|
if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
|
|
@@ -1436,12 +1729,7 @@ var DecodeWorkerChild = class {
|
|
|
1436
1729
|
if (this.admission.noteCropRequest(frameId)) this.emitStderr(`decode-worker-child: native crop named an UNINFERRED frame (frameId ${frameId}) — the 'inferred' lease-admission premise does not hold on this path; total=${this.admission.unmarkedCrops}\n`);
|
|
1437
1730
|
const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
|
|
1438
1731
|
if (!frame) {
|
|
1439
|
-
this.
|
|
1440
|
-
this.send({
|
|
1441
|
-
kind: "nativeCropMiss",
|
|
1442
|
-
requestId,
|
|
1443
|
-
reason: `frame ${frameId} not retained (lease/ring evicted before crop)`
|
|
1444
|
-
});
|
|
1732
|
+
this.serveTileFallback(requestId, frameId, bbox, maxWidth);
|
|
1445
1733
|
return;
|
|
1446
1734
|
}
|
|
1447
1735
|
try {
|
|
@@ -1466,11 +1754,151 @@ var DecodeWorkerChild = class {
|
|
|
1466
1754
|
});
|
|
1467
1755
|
}
|
|
1468
1756
|
}
|
|
1469
|
-
/**
|
|
1757
|
+
/**
|
|
1758
|
+
* Answer a native-crop request from the tile tier, or report the miss.
|
|
1759
|
+
*
|
|
1760
|
+
* Separate from {@link handleNativeCrop} because it is ASYNC (a JPEG decode)
|
|
1761
|
+
* while the hold path is synchronous, and because the reason strings differ:
|
|
1762
|
+
* `no tile contains the requested rectangle` and `no tiles retained` have
|
|
1763
|
+
* different fixes, and a single "not retained" hid both.
|
|
1764
|
+
*/
|
|
1765
|
+
async serveTileFallback(requestId, frameId, bbox, maxWidth) {
|
|
1766
|
+
try {
|
|
1767
|
+
const served = await this.serveFromTile(frameId, bbox, maxWidth);
|
|
1768
|
+
if (served) {
|
|
1769
|
+
this.tileHits += 1;
|
|
1770
|
+
this.nativeCropHits++;
|
|
1771
|
+
this.send({
|
|
1772
|
+
kind: "nativeCropResult",
|
|
1773
|
+
requestId,
|
|
1774
|
+
bytes: served.bytes,
|
|
1775
|
+
width: served.width,
|
|
1776
|
+
height: served.height
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
this.tileMisses += 1;
|
|
1781
|
+
this.nativeCropMisses++;
|
|
1782
|
+
this.send({
|
|
1783
|
+
kind: "nativeCropMiss",
|
|
1784
|
+
requestId,
|
|
1785
|
+
reason: this.tileStore.enabled ? `frame ${frameId} released and no retained subject tile contains the requested rectangle` : `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`
|
|
1786
|
+
});
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
this.tileMisses += 1;
|
|
1789
|
+
this.nativeCropMisses++;
|
|
1790
|
+
const reason = errMessage(err);
|
|
1791
|
+
this.emitStderr(`decode-worker-child: tile serve failed (frame ${frameId}) — ${reason}\n`);
|
|
1792
|
+
this.send({
|
|
1793
|
+
kind: "nativeCropMiss",
|
|
1794
|
+
requestId,
|
|
1795
|
+
reason: `tile serve failed — ${reason}`
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/** The GPU-ring or reserved-slot frame for `frameId` (hold-disabled / newest paths). */
|
|
1470
1800
|
resolveRetainedFrame(frameId) {
|
|
1471
1801
|
return this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
|
|
1472
1802
|
}
|
|
1473
1803
|
/**
|
|
1804
|
+
* The runner has this frame's detections. Cut one native TILE per subject and
|
|
1805
|
+
* end the hold.
|
|
1806
|
+
*
|
|
1807
|
+
* This is the hinge of the two-tier design: it runs at the last instant the
|
|
1808
|
+
* frame's native pixels exist, and what it leaves behind is a handful of
|
|
1809
|
+
* subject-sized JPEGs instead of a ~24.9 MB raster. A frame on which nothing
|
|
1810
|
+
* was detected leaves nothing at all, which is the asymmetry the old
|
|
1811
|
+
* per-frame lease could not express.
|
|
1812
|
+
*
|
|
1813
|
+
* Three properties are load-bearing:
|
|
1814
|
+
* - the hold ENDS regardless of what happens here (`finally`), including on
|
|
1815
|
+
* an encode failure — a tile is a bonus, a pinned frame is a leak;
|
|
1816
|
+
* - the tile is a CONTAINER, padded wider than any model-input rectangle the
|
|
1817
|
+
* cluster convention can produce. The rectangle a model is fed is still
|
|
1818
|
+
* derived once, by the runner, and cut out of the tile in
|
|
1819
|
+
* {@link serveFromTile} ([D52](../../../../docs/decisions/adr-0052.md));
|
|
1820
|
+
* - every drop is counted. A silently-skipped tile is a crop miss with no
|
|
1821
|
+
* explanation, which is the failure this whole redesign exists to remove.
|
|
1822
|
+
*/
|
|
1823
|
+
async handleCutTiles(frameId, bboxes) {
|
|
1824
|
+
try {
|
|
1825
|
+
if (!this.tileStore.enabled || bboxes.length === 0) return;
|
|
1826
|
+
const frame = this.leaseStore.get(frameId)?.frame ?? this.resolveRetainedFrame(frameId);
|
|
1827
|
+
if (!frame) {
|
|
1828
|
+
this.tileCutFailures += 1;
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
this.tileFrameWidth = frame.width;
|
|
1832
|
+
this.tileFrameHeight = frame.height;
|
|
1833
|
+
const wanted = bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME);
|
|
1834
|
+
if (bboxes.length > wanted.length) this.emitStderr(`decode-worker-child: tile cut capped at ${NATIVE_TILE_MAX_PER_FRAME} of ${bboxes.length} subjects on frame ${frameId} — the rest fall back to the ≤640 tier\n`);
|
|
1835
|
+
const tiles = [];
|
|
1836
|
+
for (const bbox of wanted) {
|
|
1837
|
+
const { region } = resolveNativeTileRegion(frame.width, frame.height, bbox, NATIVE_TILE_PADDING_RATIO);
|
|
1838
|
+
const resolved = {
|
|
1839
|
+
region,
|
|
1840
|
+
target: {
|
|
1841
|
+
width: region.width,
|
|
1842
|
+
height: region.height
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
try {
|
|
1846
|
+
const raw = frame.isHwFrame() ? this.nativeHwCropToBuffer(frame, resolved) : this.nativeScaleCropToBuffer(frame, resolved);
|
|
1847
|
+
const jpeg = await (await getSharp())(raw, { raw: {
|
|
1848
|
+
width: region.width,
|
|
1849
|
+
height: region.height,
|
|
1850
|
+
channels: 3
|
|
1851
|
+
} }).jpeg({ quality: NATIVE_TILE_JPEG_QUALITY }).toBuffer();
|
|
1852
|
+
tiles.push({
|
|
1853
|
+
region,
|
|
1854
|
+
bytes: new Uint8Array(jpeg)
|
|
1855
|
+
});
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
this.tileCutFailures += 1;
|
|
1858
|
+
this.emitStderr(`decode-worker-child: subject tile cut FAILED on frame ${frameId} — ${errMessage(err)}\n`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (tiles.length > 0) {
|
|
1862
|
+
this.tilesCut += tiles.length;
|
|
1863
|
+
this.tileStore.put(frameId, tiles);
|
|
1864
|
+
}
|
|
1865
|
+
} finally {
|
|
1866
|
+
this.leaseStore.release(frameId);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Serve a native crop from a retained subject TILE — the rung that covers the
|
|
1871
|
+
* ordinary case, where the request arrives seconds after the frame is gone.
|
|
1872
|
+
*
|
|
1873
|
+
* The rectangle is the SAME one {@link resolveNativeCrop} would have cut from
|
|
1874
|
+
* the full frame; only the raster it is taken from differs. A rectangle no
|
|
1875
|
+
* tile CONTAINS is a miss (`null`) — never a clamp into the nearest tile,
|
|
1876
|
+
* which would enrich a different part of the scene and error nowhere.
|
|
1877
|
+
*/
|
|
1878
|
+
async serveFromTile(frameId, bbox, maxWidth) {
|
|
1879
|
+
const tiles = this.tileStore.get(frameId);
|
|
1880
|
+
if (tiles.length === 0) return null;
|
|
1881
|
+
const width = this.tileFrameWidth;
|
|
1882
|
+
const height = this.tileFrameHeight;
|
|
1883
|
+
if (width <= 0 || height <= 0) return null;
|
|
1884
|
+
const { region, target } = resolveNativeCropGeometry(width, height, bbox, maxWidth);
|
|
1885
|
+
const served = selectTileForRect(tiles, region);
|
|
1886
|
+
if (!served) return null;
|
|
1887
|
+
let pipeline = (await getSharp())(served.tile.bytes).extract({
|
|
1888
|
+
left: served.local.left,
|
|
1889
|
+
top: served.local.top,
|
|
1890
|
+
width: served.local.width,
|
|
1891
|
+
height: served.local.height
|
|
1892
|
+
});
|
|
1893
|
+
if (target.width !== served.local.width || target.height !== served.local.height) pipeline = pipeline.resize(target.width, target.height, { fit: "fill" });
|
|
1894
|
+
const raw = await pipeline.removeAlpha().raw().toBuffer();
|
|
1895
|
+
return {
|
|
1896
|
+
bytes: new Uint8Array(raw),
|
|
1897
|
+
width: target.width,
|
|
1898
|
+
height: target.height
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1474
1902
|
* Resolve a decoded frame's `(colorSpace, colorRange)` tags to the EXPLICIT
|
|
1475
1903
|
* `(matrix, range)` every RGB conversion site pins — the RC-5 fix. `colorSpace`
|
|
1476
1904
|
* / `colorRange` are node-av branded `number` subtypes, so they pass to the
|
|
@@ -2124,6 +2552,9 @@ var DecodeWorkerChild = class {
|
|
|
2124
2552
|
closeInput() {
|
|
2125
2553
|
this.nativeRing.clear();
|
|
2126
2554
|
this.leaseStore.clear();
|
|
2555
|
+
this.tileStore.clear();
|
|
2556
|
+
this.tileFrameWidth = 0;
|
|
2557
|
+
this.tileFrameHeight = 0;
|
|
2127
2558
|
this.nativeLeaseDownloadFilter?.close();
|
|
2128
2559
|
this.nativeLeaseDownloadFilter = null;
|
|
2129
2560
|
this.nativeLeaseDownloadFilterKey = "";
|