@trillboards/edge-sdk 0.2.61 → 0.2.63
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/deploy/litertlm/test_trillboards_litertlm_shim.py +132 -0
- package/deploy/litertlm/trillboards-litertlm-shim.pth +1 -0
- package/deploy/litertlm/trillboards_litertlm_shim.py +214 -0
- package/deploy/macos/bench-local-vlm.sh +8 -4
- package/deploy/macos/build-macos-pkg.sh +4 -0
- package/deploy/macos/scripts/setup-litertlm.sh +99 -2
- package/deploy/systemd/setup-litertlm.sh +99 -2
- package/deploy/windows/build-windows.ps1 +4 -0
- package/deploy/windows/register-litertlm-task.ps1 +22 -0
- package/deploy/windows/setup-litertlm.ps1 +131 -3
- package/deploy/windows/trillboards-edge.cmd +19 -0
- package/dist/CloudSensingManager.d.ts +61 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/CloudSensingManager.js +52 -30
- package/dist/CloudSensingManager.js.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +74 -7
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/MosaicSource.d.ts +18 -0
- package/dist/MosaicSource.d.ts.map +1 -1
- package/dist/MosaicSource.js +23 -0
- package/dist/MosaicSource.js.map +1 -1
- package/dist/analyzeCostObserver.d.ts +96 -0
- package/dist/analyzeCostObserver.d.ts.map +1 -0
- package/dist/analyzeCostObserver.js +130 -0
- package/dist/analyzeCostObserver.js.map +1 -0
- package/dist/connectorCadence.d.ts +286 -57
- package/dist/connectorCadence.d.ts.map +1 -1
- package/dist/connectorCadence.js +313 -61
- package/dist/connectorCadence.js.map +1 -1
- package/dist/deployedCadence.d.ts +8 -2
- package/dist/deployedCadence.d.ts.map +1 -1
- package/dist/deployedCadence.js +6 -3
- package/dist/deployedCadence.js.map +1 -1
- package/dist/deployedCadenceSource.d.ts +10 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -1
- package/dist/deployedCadenceSource.js +11 -0
- package/dist/deployedCadenceSource.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localInferenceSchema.d.ts +8 -3
- package/dist/localInferenceSchema.d.ts.map +1 -1
- package/dist/localInferenceSchema.js +8 -3
- package/dist/localInferenceSchema.js.map +1 -1
- package/dist/localSensing.d.ts +22 -1
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +101 -7
- package/dist/localSensing.js.map +1 -1
- package/dist/localVlmBench.d.ts +82 -0
- package/dist/localVlmBench.d.ts.map +1 -1
- package/dist/localVlmBench.js +88 -0
- package/dist/localVlmBench.js.map +1 -1
- package/dist/sensingCatchRate.d.ts.map +1 -1
- package/dist/sensingCatchRate.js +75 -10
- package/dist/sensingCatchRate.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +26 -2
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* analyzeCostObserver — what a frame costs ON THIS BOX, measured by this box.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* `MEASURED_LOCAL_ANALYZE_MS` is ONE constant, and the fleet it sizes does not
|
|
7
|
+
* vary by a little. As of 2026-08-19 the measured spread on the SHIPPED path is
|
|
8
|
+
* an Apple M4 Max at 0.45 s/frame against a GPU-less 2-vCPU x86 box at tens of
|
|
9
|
+
* seconds — and `periodicIntervalMsForFeedCount` turns that constant directly
|
|
10
|
+
* into the still-scene heartbeat, which IS the resolution of the hazard
|
|
11
|
+
* persistence timeline the SOP product's legal argument turns on. So a single
|
|
12
|
+
* global number is wrong in both directions at once: it asks the slow box for a
|
|
13
|
+
* cadence it cannot serve, and it denies the fast box resolution it has already
|
|
14
|
+
* paid for.
|
|
15
|
+
*
|
|
16
|
+
* The fix is not a better constant. There is no constant that is right for a
|
|
17
|
+
* fleet with that spread, and picking the fast desk is the exact error
|
|
18
|
+
* `.claude/lessons/2026-08-18-a-benchmark-with-no-machine-axis-is-a-spec-for-one-desk.md`
|
|
19
|
+
* was written about. The fix is that the box measures ITSELF — it is already
|
|
20
|
+
* running the very inference the number describes, once per frame, forever.
|
|
21
|
+
*
|
|
22
|
+
* ── Why the constant stays, and stays at 7.30 s ─────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `MEASURED_LOCAL_ANALYZE_MS` becomes the BOOTSTRAP: what to assume before this
|
|
25
|
+
* machine has measured itself. It should therefore be conservative rather than
|
|
26
|
+
* accurate, and 7.30 s is exactly that — the docblock on it already says it is
|
|
27
|
+
* not a measurement of any machine. A connector that guessed FAST on a slow box
|
|
28
|
+
* would oversubscribe the single admission slot from its first minute; guessing
|
|
29
|
+
* slow costs only heartbeat resolution, and only until this observer settles.
|
|
30
|
+
*
|
|
31
|
+
* ── Three decisions worth not re-deriving ───────────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* 1. MEDIAN, never mean. One degenerate generation runs to the token cap
|
|
34
|
+
* (measured 15-23 s against a sub-second median). A mean would let a single
|
|
35
|
+
* bad frame re-tune the cadence for every camera on the host; a median over
|
|
36
|
+
* a 16-frame window cannot be moved by fewer than 8 of them.
|
|
37
|
+
*
|
|
38
|
+
* 2. Only a SUCCESSFUL analysis is recorded. A throw is not a frame cost — the
|
|
39
|
+
* same reasoning that makes `localInferencesCompleted` count past every
|
|
40
|
+
* throw, and the same reasoning `keptForTiming` applies in the bench. A
|
|
41
|
+
* request that failed fast would otherwise look like a very quick frame.
|
|
42
|
+
*
|
|
43
|
+
* 3. `medianMs()` is NULL until MIN_SAMPLES, and that is not a nicety. The first
|
|
44
|
+
* frames on a cold box carry the model load and the slower settle behind it —
|
|
45
|
+
* the bench spends WARMUP_CALLS_BEFORE_TIMING (16) untimed calls excluding
|
|
46
|
+
* exactly that, having measured a 36% monotonic drift over the frames after a
|
|
47
|
+
* single warm-up. A connector cannot refuse to sense while it warms up, so it
|
|
48
|
+
* does the other half instead: it keeps the conservative bootstrap until it
|
|
49
|
+
* has enough samples, and because the window ROLLS, the cold frames age out
|
|
50
|
+
* on their own. Convergence is therefore downward, which is the safe
|
|
51
|
+
* direction — a box never claims to be faster than it has proved.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Frames behind the rolling median. Matched to the bench's
|
|
55
|
+
* `WARMUP_CALLS_BEFORE_TIMING` on purpose: that is the measured order of
|
|
56
|
+
* magnitude on which this runtime settles, so a window shorter than it would
|
|
57
|
+
* still be reporting the settle.
|
|
58
|
+
*/
|
|
59
|
+
export declare const ANALYZE_COST_WINDOW = 16;
|
|
60
|
+
/**
|
|
61
|
+
* Samples before this observer will answer at all. Half the window: enough that
|
|
62
|
+
* the median is a median, few enough that a connector converges off its
|
|
63
|
+
* bootstrap within minutes rather than hours at a 30 s analyze floor.
|
|
64
|
+
*/
|
|
65
|
+
export declare const ANALYZE_COST_MIN_SAMPLES = 8;
|
|
66
|
+
/**
|
|
67
|
+
* A bounded ring of recent per-frame costs. Bounded because a connector runs for
|
|
68
|
+
* months: an unbounded array here is a slow leak on a box we are also telling
|
|
69
|
+
* customers needs only 8 GB.
|
|
70
|
+
*/
|
|
71
|
+
export declare class AnalyzeCostObserver {
|
|
72
|
+
private readonly window;
|
|
73
|
+
private readonly minSamples;
|
|
74
|
+
private readonly ring;
|
|
75
|
+
private next;
|
|
76
|
+
private seen;
|
|
77
|
+
constructor(options?: {
|
|
78
|
+
window?: number;
|
|
79
|
+
minSamples?: number;
|
|
80
|
+
});
|
|
81
|
+
/** Record ONE completed analysis. Anything that is not a duration is dropped. */
|
|
82
|
+
record(ms: unknown): void;
|
|
83
|
+
/** Completed analyses ever recorded — not the ring size. */
|
|
84
|
+
get sampleCount(): number;
|
|
85
|
+
/** Has this box measured itself enough to overrule the bootstrap constant? */
|
|
86
|
+
settled(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* The median of the rolling window, or `null` while the bootstrap still
|
|
89
|
+
* governs. `null` is deliberately not `MEASURED_LOCAL_ANALYZE_MS`: the caller
|
|
90
|
+
* must be able to tell "this box has not measured itself yet" from "this box
|
|
91
|
+
* measured itself and got the default", because only the first should be
|
|
92
|
+
* re-derived later.
|
|
93
|
+
*/
|
|
94
|
+
medianMs(): number | null;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=analyzeCostObserver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzeCostObserver.d.ts","sourceRoot":"","sources":["../src/analyzeCostObserver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAU1C;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IAErC,OAAO,CAAC,IAAI,CAAK;IAEjB,OAAO,CAAC,IAAI,CAAK;gBAEL,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO;IASlE,iFAAiF;IACjF,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI;IAQzB,4DAA4D;IAC5D,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,8EAA8E;IAC9E,OAAO,IAAI,OAAO;IAIlB;;;;;;OAMG;IACH,QAAQ,IAAI,MAAM,GAAG,IAAI;CAQ1B"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* analyzeCostObserver — what a frame costs ON THIS BOX, measured by this box.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* `MEASURED_LOCAL_ANALYZE_MS` is ONE constant, and the fleet it sizes does not
|
|
8
|
+
* vary by a little. As of 2026-08-19 the measured spread on the SHIPPED path is
|
|
9
|
+
* an Apple M4 Max at 0.45 s/frame against a GPU-less 2-vCPU x86 box at tens of
|
|
10
|
+
* seconds — and `periodicIntervalMsForFeedCount` turns that constant directly
|
|
11
|
+
* into the still-scene heartbeat, which IS the resolution of the hazard
|
|
12
|
+
* persistence timeline the SOP product's legal argument turns on. So a single
|
|
13
|
+
* global number is wrong in both directions at once: it asks the slow box for a
|
|
14
|
+
* cadence it cannot serve, and it denies the fast box resolution it has already
|
|
15
|
+
* paid for.
|
|
16
|
+
*
|
|
17
|
+
* The fix is not a better constant. There is no constant that is right for a
|
|
18
|
+
* fleet with that spread, and picking the fast desk is the exact error
|
|
19
|
+
* `.claude/lessons/2026-08-18-a-benchmark-with-no-machine-axis-is-a-spec-for-one-desk.md`
|
|
20
|
+
* was written about. The fix is that the box measures ITSELF — it is already
|
|
21
|
+
* running the very inference the number describes, once per frame, forever.
|
|
22
|
+
*
|
|
23
|
+
* ── Why the constant stays, and stays at 7.30 s ─────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* `MEASURED_LOCAL_ANALYZE_MS` becomes the BOOTSTRAP: what to assume before this
|
|
26
|
+
* machine has measured itself. It should therefore be conservative rather than
|
|
27
|
+
* accurate, and 7.30 s is exactly that — the docblock on it already says it is
|
|
28
|
+
* not a measurement of any machine. A connector that guessed FAST on a slow box
|
|
29
|
+
* would oversubscribe the single admission slot from its first minute; guessing
|
|
30
|
+
* slow costs only heartbeat resolution, and only until this observer settles.
|
|
31
|
+
*
|
|
32
|
+
* ── Three decisions worth not re-deriving ───────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* 1. MEDIAN, never mean. One degenerate generation runs to the token cap
|
|
35
|
+
* (measured 15-23 s against a sub-second median). A mean would let a single
|
|
36
|
+
* bad frame re-tune the cadence for every camera on the host; a median over
|
|
37
|
+
* a 16-frame window cannot be moved by fewer than 8 of them.
|
|
38
|
+
*
|
|
39
|
+
* 2. Only a SUCCESSFUL analysis is recorded. A throw is not a frame cost — the
|
|
40
|
+
* same reasoning that makes `localInferencesCompleted` count past every
|
|
41
|
+
* throw, and the same reasoning `keptForTiming` applies in the bench. A
|
|
42
|
+
* request that failed fast would otherwise look like a very quick frame.
|
|
43
|
+
*
|
|
44
|
+
* 3. `medianMs()` is NULL until MIN_SAMPLES, and that is not a nicety. The first
|
|
45
|
+
* frames on a cold box carry the model load and the slower settle behind it —
|
|
46
|
+
* the bench spends WARMUP_CALLS_BEFORE_TIMING (16) untimed calls excluding
|
|
47
|
+
* exactly that, having measured a 36% monotonic drift over the frames after a
|
|
48
|
+
* single warm-up. A connector cannot refuse to sense while it warms up, so it
|
|
49
|
+
* does the other half instead: it keeps the conservative bootstrap until it
|
|
50
|
+
* has enough samples, and because the window ROLLS, the cold frames age out
|
|
51
|
+
* on their own. Convergence is therefore downward, which is the safe
|
|
52
|
+
* direction — a box never claims to be faster than it has proved.
|
|
53
|
+
*/
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.AnalyzeCostObserver = exports.ANALYZE_COST_MIN_SAMPLES = exports.ANALYZE_COST_WINDOW = void 0;
|
|
56
|
+
/**
|
|
57
|
+
* Frames behind the rolling median. Matched to the bench's
|
|
58
|
+
* `WARMUP_CALLS_BEFORE_TIMING` on purpose: that is the measured order of
|
|
59
|
+
* magnitude on which this runtime settles, so a window shorter than it would
|
|
60
|
+
* still be reporting the settle.
|
|
61
|
+
*/
|
|
62
|
+
exports.ANALYZE_COST_WINDOW = 16;
|
|
63
|
+
/**
|
|
64
|
+
* Samples before this observer will answer at all. Half the window: enough that
|
|
65
|
+
* the median is a median, few enough that a connector converges off its
|
|
66
|
+
* bootstrap within minutes rather than hours at a 30 s analyze floor.
|
|
67
|
+
*/
|
|
68
|
+
exports.ANALYZE_COST_MIN_SAMPLES = 8;
|
|
69
|
+
/**
|
|
70
|
+
* Ignore a duration that cannot be a frame. Zero and negatives are clock
|
|
71
|
+
* artifacts, not fast frames, and a NaN would poison the median silently.
|
|
72
|
+
*/
|
|
73
|
+
function isUsable(ms) {
|
|
74
|
+
return typeof ms === 'number' && Number.isFinite(ms) && ms > 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A bounded ring of recent per-frame costs. Bounded because a connector runs for
|
|
78
|
+
* months: an unbounded array here is a slow leak on a box we are also telling
|
|
79
|
+
* customers needs only 8 GB.
|
|
80
|
+
*/
|
|
81
|
+
class AnalyzeCostObserver {
|
|
82
|
+
constructor(options = {}) {
|
|
83
|
+
this.ring = [];
|
|
84
|
+
this.next = 0;
|
|
85
|
+
this.seen = 0;
|
|
86
|
+
const w = isUsable(options.window) ? Math.floor(options.window) : exports.ANALYZE_COST_WINDOW;
|
|
87
|
+
this.window = Math.max(1, w);
|
|
88
|
+
const m = isUsable(options.minSamples) ? Math.floor(options.minSamples) : exports.ANALYZE_COST_MIN_SAMPLES;
|
|
89
|
+
// Never require more samples than the ring can hold, or the observer could
|
|
90
|
+
// never settle and the bootstrap would silently be permanent.
|
|
91
|
+
this.minSamples = Math.max(1, Math.min(this.window, m));
|
|
92
|
+
}
|
|
93
|
+
/** Record ONE completed analysis. Anything that is not a duration is dropped. */
|
|
94
|
+
record(ms) {
|
|
95
|
+
if (!isUsable(ms))
|
|
96
|
+
return;
|
|
97
|
+
if (this.ring.length < this.window)
|
|
98
|
+
this.ring.push(ms);
|
|
99
|
+
else
|
|
100
|
+
this.ring[this.next] = ms;
|
|
101
|
+
this.next = (this.next + 1) % this.window;
|
|
102
|
+
this.seen += 1;
|
|
103
|
+
}
|
|
104
|
+
/** Completed analyses ever recorded — not the ring size. */
|
|
105
|
+
get sampleCount() {
|
|
106
|
+
return this.seen;
|
|
107
|
+
}
|
|
108
|
+
/** Has this box measured itself enough to overrule the bootstrap constant? */
|
|
109
|
+
settled() {
|
|
110
|
+
return this.ring.length >= this.minSamples;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The median of the rolling window, or `null` while the bootstrap still
|
|
114
|
+
* governs. `null` is deliberately not `MEASURED_LOCAL_ANALYZE_MS`: the caller
|
|
115
|
+
* must be able to tell "this box has not measured itself yet" from "this box
|
|
116
|
+
* measured itself and got the default", because only the first should be
|
|
117
|
+
* re-derived later.
|
|
118
|
+
*/
|
|
119
|
+
medianMs() {
|
|
120
|
+
if (!this.settled())
|
|
121
|
+
return null;
|
|
122
|
+
const sorted = [...this.ring].sort((a, b) => a - b);
|
|
123
|
+
const mid = sorted.length >> 1;
|
|
124
|
+
return sorted.length % 2 === 1
|
|
125
|
+
? sorted[mid]
|
|
126
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.AnalyzeCostObserver = AnalyzeCostObserver;
|
|
130
|
+
//# sourceMappingURL=analyzeCostObserver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzeCostObserver.js","sourceRoot":"","sources":["../src/analyzeCostObserver.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;;;AAEH;;;;;GAKG;AACU,QAAA,mBAAmB,GAAG,EAAE,CAAC;AAEtC;;;;GAIG;AACU,QAAA,wBAAwB,GAAG,CAAC,CAAC;AAE1C;;;GAGG;AACH,SAAS,QAAQ,CAAC,EAAW;IAC3B,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAa,mBAAmB;IAW9B,YAAY,UAAoD,EAAE;QANjD,SAAI,GAAa,EAAE,CAAC;QAE7B,SAAI,GAAG,CAAC,CAAC;QAET,SAAI,GAAG,CAAC,CAAC;QAGf,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,2BAAmB,CAAC;QACtF,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,gCAAwB,CAAC;QACnG,2EAA2E;QAC3E,8DAA8D;QAC9D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,iFAAiF;IACjF,MAAM,CAAC,EAAW;QAChB,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,OAAO;QAC1B,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;;YAClD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;IACjB,CAAC;IAED,4DAA4D;IAC5D,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,8EAA8E;IAC9E,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC;IAC7C,CAAC;IAED;;;;;;OAMG;IACH,QAAQ;QACN,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YAAE,OAAO,IAAI,CAAC;QACjC,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACpD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAC/B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC;YAC5B,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;YACb,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAC1C,CAAC;CACF;AAtDD,kDAsDC"}
|
|
@@ -44,11 +44,13 @@
|
|
|
44
44
|
* of 493 analyses/hour for the WHOLE connector, however many
|
|
45
45
|
* cameras it runs.
|
|
46
46
|
* STALE — see the block under MEASURED_LOCAL_ANALYZE_MS. The same
|
|
47
|
-
* harness measures 1.90 s
|
|
48
|
-
* config.json change
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
47
|
+
* harness measures 1.90 s, then 1.22 s with the 2026-08-19
|
|
48
|
+
* config.json change, then a further paired 2.42x with the 2026-08-19
|
|
49
|
+
* fast-conversation shim, on the pinned 0.16.0 — so the real ceiling
|
|
50
|
+
* on this machine class is ~1,895/hour, ~2,951/hour and higher again
|
|
51
|
+
* respectively. The constant is kept as a conservative FLEET default
|
|
52
|
+
* because the fleet varies ~7x; do not quote 493 as this machine's
|
|
53
|
+
* ceiling.
|
|
52
54
|
* (The DOOH audience schema measured 12.48 s median and ranged to
|
|
53
55
|
* 23.8 s because it emits a free-text narrative. The SOP field set
|
|
54
56
|
* is both cheaper and far tighter — 6.0-7.5 s — which is a second,
|
|
@@ -141,25 +143,81 @@ export declare const MEASURED_LOCAL_ANALYZE_MS = 7300;
|
|
|
141
143
|
* the box can afford, which costs hazard-persistence resolution against the
|
|
142
144
|
* 111 s constructive-notice bracket — the one thing this file exists to protect.
|
|
143
145
|
*
|
|
144
|
-
* ──
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
146
|
+
* ── AND IT MOVED AGAIN, 2026-08-19 (the shim) ──────────────────────────────
|
|
147
|
+
*
|
|
148
|
+
* `litert-lm serve` built `ConstrainedDecodingConfig(enable=True, …)` for every
|
|
149
|
+
* request even when nothing asked for a `response_format`, so every frame paid
|
|
150
|
+
* a constraint build for a grammar nothing used. Removing it (see
|
|
151
|
+
* `deploy/litertlm/trillboards_litertlm_shim.py`) is worth a further **paired
|
|
152
|
+
* 2.42x** on the shipped path — two stock serves on two ports, interleaved,
|
|
153
|
+
* n=16, 16/16 schema-valid and 16/16 BYTE-IDENTICAL generations on both arms.
|
|
154
|
+
* `bench-local-vlm.mjs --arms gpu-sop`, back to back in the same window, moves
|
|
155
|
+
* **1.32 -> 0.72 s/frame, i.e. 2,727 -> 5,000 analyses/hour**, at load 22-25
|
|
156
|
+
* where the same harness measures the stock arm at 1.32 s against the 1.22 s
|
|
157
|
+
* this file already records for it — so that window is close to the regime the
|
|
158
|
+
* 1.22 s was taken in, and 0.72 s is the nearest thing to a quotable per-frame
|
|
159
|
+
* number this machine class has.
|
|
160
|
+
*
|
|
161
|
+
* TWO CAVEATS THAT TRAVEL WITH IT. The ratio is itself load-dependent — 3.734x
|
|
162
|
+
* at 0.0% CPU idle against 2.42x at load 25 — because the removed work is
|
|
163
|
+
* CPU-bound and the retained work is GPU-bound, so a figure taken under
|
|
164
|
+
* contention flatters the change. And 0.72 s/frame is still BELOW the capacity
|
|
165
|
+
* target: 4 cameras resolving a 2 s bracket needs 0.5 s/frame (7,200
|
|
166
|
+
* analyses/hour), and this serves 2-3.
|
|
167
|
+
*
|
|
168
|
+
* ── IT IS NOT LOWERED HERE. IT IS NOW A BOOTSTRAP. (2026-08-19) ───────────
|
|
169
|
+
*
|
|
170
|
+
* This is ONE constant for a fleet whose measured spread on the SHIPPED path is
|
|
171
|
+
* 0.50 s/frame (M4 Max, post-#12567/#12594) against tens of seconds on a
|
|
172
|
+
* GPU-less x86 box. No constant is right for both: the fast desk's number would
|
|
173
|
+
* demand ~70x what the slow box can deliver, and the slow box's number makes
|
|
174
|
+
* every heartbeat worse than today. Picking the fast desk is exactly the error
|
|
175
|
+
* `.claude/lessons/2026-08-18-a-benchmark-with-no-machine-axis-is-a-spec-for-
|
|
176
|
+
* one-desk.md` was written about.
|
|
177
|
+
*
|
|
178
|
+
* So the fix was never a better constant, and as of 2026-08-19 it is built: the
|
|
179
|
+
* box DERIVES this for itself. `AnalyzeCostObserver` (analyzeCostObserver.ts)
|
|
180
|
+
* times every successful `analyze()` in `LocalSensingClient`, reports a rolling
|
|
181
|
+
* median over 16 frames, and `periodicIntervalMsForFeedCount` /
|
|
182
|
+
* `resolveDeployedCadence` take it as an optional input. A connector that has
|
|
183
|
+
* measured itself uses its OWN number; one that has not uses this one.
|
|
184
|
+
*
|
|
185
|
+
* NOTE WHAT THAT MAKES THIS CONSTANT. It is the value to assume BEFORE a machine
|
|
186
|
+
* has measured itself, so it should be conservative rather than accurate — and
|
|
187
|
+
* 7.30 s is exactly that. Guessing FAST on a slow box oversubscribes the single
|
|
188
|
+
* admission slot from the first minute; guessing slow costs only heartbeat
|
|
189
|
+
* resolution, and only until the observer settles (8 frames). It is therefore
|
|
190
|
+
* deliberately NOT lowered, and quoting it as a measurement of any machine is
|
|
191
|
+
* still what this block exists to stop.
|
|
192
|
+
*
|
|
193
|
+
* A CONSEQUENCE WORTH KNOWING: because this value does not move, neither does
|
|
194
|
+
* `sensingCatchRate.DEFAULT_SAMPLER.analyzeS`, so none of this required a
|
|
195
|
+
* corpus replay or a `.github/contracts/sensing-catch-rate-baseline.json`
|
|
196
|
+
* regeneration. If you ever DO move it, all of that becomes mandatory in the
|
|
197
|
+
* same change — and the catch-rate ratchet is shrink-only, so a baseline may
|
|
198
|
+
* never be regenerated merely to clear red.
|
|
199
|
+
*
|
|
200
|
+
* ── The idle-box objection, and why it does not block the derivation ────────
|
|
201
|
+
*
|
|
202
|
+
* The 2026-08-19 predecessor of this block held the per-machine derivation back
|
|
203
|
+
* for want of a per-frame number taken on an idle box, on the grounds that a
|
|
204
|
+
* figure measured at 0.0% CPU idle would hardcode one session's contention into
|
|
205
|
+
* every connector. That objection is answered twice over.
|
|
206
|
+
*
|
|
207
|
+
* First, it no longer applies to the DERIVATION at all: nothing global is
|
|
208
|
+
* derived from a measured number any more. Each box measures itself, in situ,
|
|
209
|
+
* under whatever load it actually runs at — which is the right load by
|
|
210
|
+
* definition, and is the one thing a bench on someone else's desk can never
|
|
211
|
+
* reproduce.
|
|
212
|
+
*
|
|
213
|
+
* Second, the verdict turned out not to depend on it. Three measurements of the
|
|
214
|
+
* shipped arm on one M4 Max across a 3x load range: 0.45 s at load 7.77, 0.50 s
|
|
215
|
+
* at load 11.87, 0.72 s at load 22-25. All three clear the 13.88 s/frame budget
|
|
216
|
+
* for 4 cameras by 19-31x, so "this class clears 4 cameras" is invariant across
|
|
217
|
+
* everything that could be measured. Load moves the quotable figure, not the
|
|
218
|
+
* verdict — and the post-merge path is markedly LESS load-sensitive than the one
|
|
219
|
+
* that motivated the objection, because the work #12594 removed was CPU-bound
|
|
220
|
+
* and what remains is GPU-bound.
|
|
163
221
|
*/
|
|
164
222
|
/**
|
|
165
223
|
* Share of the single admission slot the STILL-scene heartbeat floor may take.
|
|
@@ -213,8 +271,13 @@ export declare const STILL_FLOOR_SLOT_SHARE = 0.5;
|
|
|
213
271
|
*/
|
|
214
272
|
export declare const MIN_ANALYZE_INTERVAL_MS = 30000;
|
|
215
273
|
/**
|
|
216
|
-
* Measured wall-clock cost of ONE LOOK, in ms — a
|
|
217
|
-
* decode-and-detect that follows it.
|
|
274
|
+
* Measured wall-clock cost of ONE LOOK on the PER-GRAB capture path, in ms — a
|
|
275
|
+
* real RTSP grab, not the decode-and-detect that follows it.
|
|
276
|
+
*
|
|
277
|
+
* STILL LIVE, and still exactly right for the path it describes: the per-grab
|
|
278
|
+
* provider is what a feed runs when it cannot hold a persistent session (see
|
|
279
|
+
* `MEASURED_STREAM_LOOK_MS`), so this is the number `lookIntervalMsForFeedCount`
|
|
280
|
+
* uses in `'per_grab'` mode and nothing about it has changed.
|
|
218
281
|
*
|
|
219
282
|
* The cascade budget above quotes 3.6 ms for a look. That is the cost AFTER the
|
|
220
283
|
* frame is in hand: pull it down to ~178x100 grey and run the detector. Getting
|
|
@@ -247,6 +310,75 @@ export declare const MIN_ANALYZE_INTERVAL_MS = 30000;
|
|
|
247
310
|
* the connector is not running.
|
|
248
311
|
*/
|
|
249
312
|
export declare const MEASURED_LOOK_MS = 2000;
|
|
313
|
+
/**
|
|
314
|
+
* Measured wall-clock cost of ONE LOOK on the PERSISTENT capture path, in ms.
|
|
315
|
+
*
|
|
316
|
+
* `RTSPStreamCameraProvider` holds one long-lived ffmpeg per feed decoding into
|
|
317
|
+
* a bounded ring, so `getFrame()` is a memory read of the newest frame rather
|
|
318
|
+
* than a process start plus a DESCRIBE/SETUP/PLAY plus a wait for an IDR. The
|
|
319
|
+
* handshake is paid ONCE, at open.
|
|
320
|
+
*
|
|
321
|
+
* MEASURED 2026-08-18 against the same rig, same clip, same degraded profile
|
|
322
|
+
* (640x360 / 399 kbps / GOP 2 s), driving looks at the shipped cadence so a
|
|
323
|
+
* fresh frame is already in hand — which is the condition the connector runs
|
|
324
|
+
* in and the only one where "cost of a look" means anything:
|
|
325
|
+
*
|
|
326
|
+
* per-grab p50 2,000 ms
|
|
327
|
+
* persistent p50 0 ms (sub-millisecond; the ring read is a pointer)
|
|
328
|
+
*
|
|
329
|
+
* Reproduce: `node scripts/measure-persistent-capture.mjs --feeds 1
|
|
330
|
+
* --seconds 60 --pace-ms 500`. The rig is committed — see that file's header
|
|
331
|
+
* for why, and for why a `file://` fixture cannot stand in for it.
|
|
332
|
+
*
|
|
333
|
+
* It is 1 rather than 0 so that `lookIntervalMsForFeedCount` can never divide
|
|
334
|
+
* by it, and so nobody reads a zero as "free" — a look still costs the
|
|
335
|
+
* detector's `MEASURED_LOOK_DETECT_MS`, which is what actually bounds the
|
|
336
|
+
* persistent path.
|
|
337
|
+
*/
|
|
338
|
+
export declare const MEASURED_STREAM_LOOK_MS = 1;
|
|
339
|
+
/**
|
|
340
|
+
* Cost of a look AFTER the frame is in hand, in ms: pull it down to ~178x100
|
|
341
|
+
* grey and run `SceneChangeDetector`. Measured 3.6 ms median / 7.1 ms p95.
|
|
342
|
+
*
|
|
343
|
+
* This is the number the cascade budget has always quoted, and until the
|
|
344
|
+
* capture path changed it was irrelevant — a 3.6 ms stage behind a 2,000 ms
|
|
345
|
+
* one bounds nothing. On the persistent path it is the ONLY per-look cost
|
|
346
|
+
* left, so it becomes the binding term and gets named here rather than living
|
|
347
|
+
* in a comment.
|
|
348
|
+
*/
|
|
349
|
+
export declare const MEASURED_LOOK_DETECT_MS = 3.6;
|
|
350
|
+
/**
|
|
351
|
+
* Share of ONE core the motion detector may take across every feed.
|
|
352
|
+
*
|
|
353
|
+
* The one judgement in the persistent derivation, stated rather than hidden —
|
|
354
|
+
* the same shape as `STILL_FLOOR_SLOT_SHARE`. Detection is the cheapest stage
|
|
355
|
+
* on the box and it must not become the noisy neighbour: the connector also
|
|
356
|
+
* runs one ffmpeg decode per feed (measured on the rig at ~2.4% of a core per
|
|
357
|
+
* feed at this profile) and one local VLM slot, and the VLM is the thing the
|
|
358
|
+
* product is actually spending money on.
|
|
359
|
+
*
|
|
360
|
+
* A quarter of a core does not bind at any realistic feed count — see
|
|
361
|
+
* `lookIntervalMsForFeedCount` — which is the point: it is a guard rail, not a
|
|
362
|
+
* throttle, and it exists so that a future 64-feed host derives a cadence it
|
|
363
|
+
* can serve instead of one it merely asked for.
|
|
364
|
+
*/
|
|
365
|
+
export declare const LOOK_DETECT_CPU_SHARE = 0.25;
|
|
366
|
+
/**
|
|
367
|
+
* The persistent session's frame interval, in ms — the freshest a look can
|
|
368
|
+
* possibly be, because a look reads the newest DECODED frame.
|
|
369
|
+
*
|
|
370
|
+
* `RTSPStreamCameraProvider` defaults `streamFps` to 4, i.e. 250 ms. Restated
|
|
371
|
+
* here rather than imported because `edge-sdk` must not depend on a platform
|
|
372
|
+
* package, and asserted against the real default by `connectorCadence.test.ts`
|
|
373
|
+
* so the two cannot drift — the same treatment `LOOK_CAPTURE_CONCURRENCY` gets.
|
|
374
|
+
*
|
|
375
|
+
* `LOOK_INTERVAL_MS` must never be finer than this: asking for a 100 ms look
|
|
376
|
+
* cadence against a 250 ms stream would hand the same frame to the motion
|
|
377
|
+
* detector twice, which the provider refuses (it throws rather than repeat a
|
|
378
|
+
* frame) and which would report as a stale-stream fault on a perfectly healthy
|
|
379
|
+
* camera.
|
|
380
|
+
*/
|
|
381
|
+
export declare const MEASURED_STREAM_FRAME_INTERVAL_MS = 250;
|
|
250
382
|
/**
|
|
251
383
|
* Concurrent RTSP grabs one physical recorder will take —
|
|
252
384
|
* `DEFAULT_MAX_CONCURRENT_CAPTURES_PER_SOURCE` in `RTSPCameraProvider`, which
|
|
@@ -296,32 +428,59 @@ export declare const LEGACY_LOOK_INTERVAL_MS = 30000;
|
|
|
296
428
|
* T Instant Brief Sustained State
|
|
297
429
|
* 30 s 8.4% 18.6% 100% 100%
|
|
298
430
|
* 5 s 50.2% 55.1% 100% 100%
|
|
299
|
-
* 2.44 s 89.1% 71.2% 100% 100%
|
|
300
431
|
* 2 s 95.5% 75.2% 100% 100%
|
|
301
432
|
* 1 s 100.0% 86.6% 100% 100%
|
|
302
433
|
* 0.75 s 100.0% 90.1% 100% 100%
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* `
|
|
321
|
-
*
|
|
322
|
-
*
|
|
434
|
+
* 0.5 s 100.0% 93.2% 100% 100%
|
|
435
|
+
*
|
|
436
|
+
* ── 2 000 -> 500 ms ON 2026-08-18, AND THE REASON IS THE CAPTURE PATH ──────
|
|
437
|
+
*
|
|
438
|
+
* This constant was 2 000 ms, and the comment here said why in as many words:
|
|
439
|
+
* "the Brief group does NOT reach 90% until 0.75 s, and 0.75 s is well below
|
|
440
|
+
* the measured cost of a single grab. So 90% on every shape is NOT reachable by
|
|
441
|
+
* look cadence on the current capture architecture… Closing that gap needs a
|
|
442
|
+
* persistent RTSP session — one long-lived ffmpeg per feed instead of one per
|
|
443
|
+
* grab — which is an architecture change, not a number."
|
|
444
|
+
*
|
|
445
|
+
* That is now built (`RTSPStreamCameraProvider`), the constraint it named is
|
|
446
|
+
* gone, and this is the number that moves as a result. A look is a memory read
|
|
447
|
+
* of the newest decoded frame — `MEASURED_STREAM_LOOK_MS`, sub-millisecond —
|
|
448
|
+
* so the recorder no longer bounds the look cadence at all. Brief clears the
|
|
449
|
+
* 90% target at 0.75 s; 500 ms is the next value that is BOTH under it and a
|
|
450
|
+
* whole multiple of the persistent session's 250 ms frame interval
|
|
451
|
+
* (`MEASURED_STREAM_FRAME_INTERVAL_MS`), so a look always finds an unseen frame
|
|
452
|
+
* in hand and never blocks.
|
|
453
|
+
*
|
|
454
|
+
* ── WHY STOP AT 500 ms ────────────────────────────────────────────────────
|
|
455
|
+
*
|
|
456
|
+
* The same stopping rule that picked 2 000 ms: go until the last unmet target
|
|
457
|
+
* is met, and no further. At 500 ms every reporting group clears 90% — the
|
|
458
|
+
* first cadence in the product's history where that is true. Below it the
|
|
459
|
+
* corpus has nothing left to buy (Instant and Brief are saturated, Sustained
|
|
460
|
+
* and State were never look-limited) while the cost is real and linear: each
|
|
461
|
+
* halving doubles both the detector's CPU (`MEASURED_LOOK_DETECT_MS` per look
|
|
462
|
+
* per feed) and the session's JPEG-encode rate.
|
|
463
|
+
*
|
|
464
|
+
* ── AND IT MUST NOT COST ANALYSES ─────────────────────────────────────────
|
|
465
|
+
*
|
|
466
|
+
* Looking 4x more often must not analyse 4x more often, or the change has
|
|
467
|
+
* failed: one 7.30 s slot is 493 analyses/hour for the whole connector and
|
|
468
|
+
* there is no cloud path left to absorb an overflow. Three things hold that,
|
|
469
|
+
* and all three are asserted rather than assumed:
|
|
470
|
+
* - `MIN_ANALYZE_INTERVAL_MS` (30 s) is a SEPARATE constant and is what
|
|
471
|
+
* `MotionGate.minAnalyzeIntervalMs` floors on.
|
|
472
|
+
* - `periodicIntervalMsForFeedCount` floors on the ANALYZE floor, never on
|
|
473
|
+
* the look cadence, so the still-scene heartbeat does not move.
|
|
474
|
+
* - a still scene contributes exactly ZERO motion-triggered analyses, which
|
|
475
|
+
* is what makes the extra looks free at the model.
|
|
476
|
+
* The `sensing-catch-rate` guard's volume ratchet is the mechanical version:
|
|
477
|
+
* `volume.idleAnalyses` may only go DOWN.
|
|
478
|
+
*
|
|
479
|
+
* Do not raise or lower this without re-running the replay — the guard re-reads
|
|
480
|
+
* this constant and fails if it drifts from the cadence the corpus baseline was
|
|
481
|
+
* measured at.
|
|
323
482
|
*/
|
|
324
|
-
export declare const LOOK_INTERVAL_MS =
|
|
483
|
+
export declare const LOOK_INTERVAL_MS = 500;
|
|
325
484
|
/** The DOOH audience default (`DEFAULT_PERIODIC_MS`). The slow end of the clamp. */
|
|
326
485
|
export declare const DOOH_PERIODIC_MS = 300000;
|
|
327
486
|
/**
|
|
@@ -334,29 +493,99 @@ export declare const DOOH_PERIODIC_MS = 300000;
|
|
|
334
493
|
* Returns `null` when the count is not a positive integer, so a caller can omit
|
|
335
494
|
* the key entirely rather than write a cadence derived from a bad count.
|
|
336
495
|
*/
|
|
337
|
-
export declare function periodicIntervalMsForFeedCount(feedCount: unknown
|
|
496
|
+
export declare function periodicIntervalMsForFeedCount(feedCount: unknown,
|
|
338
497
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
498
|
+
* What a frame ACTUALLY costs on this box, in ms, when this box has measured
|
|
499
|
+
* itself (`AnalyzeCostObserver.medianMs()`). Omitted or `null` falls back to
|
|
500
|
+
* `MEASURED_LOCAL_ANALYZE_MS`, which is the BOOTSTRAP — deliberately
|
|
501
|
+
* conservative, and not a measurement of any machine.
|
|
502
|
+
*
|
|
503
|
+
* This parameter is the whole reason a fleet spanning 0.45 s/frame to tens of
|
|
504
|
+
* seconds can share one code path: the equation was always per-machine, and
|
|
505
|
+
* until 2026-08-19 only its input was global.
|
|
506
|
+
*/
|
|
507
|
+
analyzeMs?: number | null): number | null;
|
|
508
|
+
/**
|
|
509
|
+
* How a feed's frames are acquired. The look cadence a connector can SUSTAIN
|
|
510
|
+
* depends on this and on nothing else about the box.
|
|
511
|
+
*
|
|
512
|
+
* 'persistent' one long-lived ffmpeg per feed into a bounded ring
|
|
513
|
+
* (`RTSPStreamCameraProvider`). A look is a memory read.
|
|
514
|
+
* 'per_grab' one ffmpeg per grab (`RTSPCameraProvider`). A look is an RTSP
|
|
515
|
+
* handshake, and the RECORDER is the bound.
|
|
516
|
+
*/
|
|
517
|
+
export type CaptureMode = 'persistent' | 'per_grab';
|
|
518
|
+
/**
|
|
519
|
+
* The fastest look cadence a connector running `feedCount` feeds can actually
|
|
520
|
+
* SUSTAIN, in ms.
|
|
341
521
|
*
|
|
342
522
|
* Same shape as `periodicIntervalMsForFeedCount` and for the same reason: a
|
|
343
523
|
* cadence is only real if the machine can deliver it. That function divides one
|
|
344
|
-
* inference slot among N feeds; this one divides
|
|
345
|
-
*
|
|
524
|
+
* inference slot among N feeds; this one divides whatever the capture path's
|
|
525
|
+
* scarce resource is — and the whole point of the persistent session is that it
|
|
526
|
+
* CHANGES WHICH RESOURCE THAT IS.
|
|
527
|
+
*
|
|
528
|
+
* ── 'per_grab' — the RECORDER is the bound (unchanged) ────────────────────
|
|
346
529
|
*
|
|
347
530
|
* N feeds x (1 look / T) <= LOOK_CAPTURE_CONCURRENCY / MEASURED_LOOK_MS
|
|
348
531
|
* T >= N x MEASURED_LOOK_MS / LOOK_CAPTURE_CONCURRENCY
|
|
349
532
|
*
|
|
350
533
|
* Measured against that formula at 16 feeds on one recorder, GOP 2 s: predicted
|
|
351
|
-
* 16.0 s/feed, observed 16.0 s/feed.
|
|
534
|
+
* 16.0 s/feed, observed 16.0 s/feed. This is still exactly right for a feed that
|
|
535
|
+
* could not hold a persistent session and degraded — see
|
|
536
|
+
* `RTSPStreamCameraProvider`'s degrade path — so the branch stays live rather
|
|
537
|
+
* than being deleted as legacy.
|
|
538
|
+
*
|
|
539
|
+
* ── 'persistent' — the recorder drops out and the DETECTOR is the bound ───
|
|
540
|
+
*
|
|
541
|
+
* A look no longer touches the recorder, so `LOOK_CAPTURE_CONCURRENCY` and
|
|
542
|
+
* `MEASURED_LOOK_MS` have nothing to say about it. What is left is the cost
|
|
543
|
+
* AFTER the frame is in hand:
|
|
544
|
+
*
|
|
545
|
+
* N feeds x (1 look / T) x MEASURED_LOOK_DETECT_MS <= LOOK_DETECT_CPU_SHARE
|
|
546
|
+
* T >= N x 3.6 / 0.25 ms
|
|
547
|
+
*
|
|
548
|
+
* That is 230 ms at 16 feeds and 461 ms at 32 — both BELOW `LOOK_INTERVAL_MS`,
|
|
549
|
+
* so the clamp wins and a realistic connector simply gets the eval target. It
|
|
550
|
+
* begins to bind past ~34 feeds, which is the honest place for it to bind: a
|
|
551
|
+
* host with 34 cameras has other problems (its still-scene heartbeat pinned at
|
|
552
|
+
* `DOOH_PERIODIC_MS` since 21 feeds) and `getSensingHealth()` reports the
|
|
553
|
+
* effective interval either way.
|
|
352
554
|
*
|
|
353
555
|
* Never faster than `LOOK_INTERVAL_MS` (the eval target — looking finer than
|
|
354
556
|
* that buys nothing the corpus can see) and never slower than
|
|
355
557
|
* `LEGACY_LOOK_INTERVAL_MS`, so this can only make a connector look MORE often
|
|
356
|
-
* than it did, never less. Returns `null` on an uncountable feed count so a
|
|
357
|
-
* omit the key rather than derive a cadence from nothing.
|
|
558
|
+
* than it did, never less. Returns `null` on an uncountable feed count so a
|
|
559
|
+
* caller can omit the key rather than derive a cadence from nothing.
|
|
560
|
+
*/
|
|
561
|
+
export declare function lookIntervalMsForFeedCount(feedCount: unknown, captureMode?: CaptureMode): number | null;
|
|
562
|
+
/**
|
|
563
|
+
* Decoded frames a persistent session must produce per LOOK.
|
|
564
|
+
*
|
|
565
|
+
* TWO, and the second one is not waste. `RTSPStreamCameraProvider.getFrame()`
|
|
566
|
+
* refuses to hand back a frame it has already served — that refusal is the
|
|
567
|
+
* whole reason a frozen camera cannot report as healthy — so a look that
|
|
568
|
+
* arrives before the next frame does BLOCKS, and if it blocks past the
|
|
569
|
+
* staleness deadline it throws on a camera that is working perfectly.
|
|
570
|
+
*
|
|
571
|
+
* Running the stream at exactly the look rate makes that a coin flip on every
|
|
572
|
+
* tick, because neither clock is locked to the other: the session's frame
|
|
573
|
+
* cadence follows the CAMERA's timestamps and the look cadence follows a Node
|
|
574
|
+
* timer, and they drift. One frame of headroom removes the race for the cost of
|
|
575
|
+
* one extra scale+encode per look — the cheapest stage in the pipeline, and one
|
|
576
|
+
* that never touches the recorder or the model.
|
|
577
|
+
*/
|
|
578
|
+
export declare const STREAM_LOOK_HEADROOM = 2;
|
|
579
|
+
/**
|
|
580
|
+
* Frames per second a persistent session should decode to serve a given look
|
|
581
|
+
* cadence. Derived, so the two can never be set independently and drift into
|
|
582
|
+
* the blocking case above.
|
|
583
|
+
*
|
|
584
|
+
* At the shipped 500 ms look cadence this is 4 fps — which is also the
|
|
585
|
+
* provider's own default (`MEASURED_STREAM_FRAME_INTERVAL_MS`), and
|
|
586
|
+
* `connectorCadence.test.ts` asserts the agreement rather than trusting it.
|
|
358
587
|
*/
|
|
359
|
-
export declare function
|
|
588
|
+
export declare function streamFpsForLookIntervalMs(lookIntervalMs: unknown): number;
|
|
360
589
|
/**
|
|
361
590
|
* The still-scene analyses/hour a whole connector demands at a heartbeat, and
|
|
362
591
|
* what fraction of the single slot that is. Exposed so the derivation is
|