@camstack/addon-pipeline 1.2.201 → 1.2.202
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.
|
@@ -1093,7 +1093,7 @@ var SessionDecodeSession = class {
|
|
|
1093
1093
|
* rejects — native crops are strictly best-effort with a free fallback.
|
|
1094
1094
|
*/
|
|
1095
1095
|
requestNativeCrop(frameId, bbox, maxWidth) {
|
|
1096
|
-
if (this.exited) return Promise.resolve(
|
|
1096
|
+
if (this.exited) return Promise.resolve({ kind: "miss" });
|
|
1097
1097
|
const requestId = this.nextNativeCropId++;
|
|
1098
1098
|
return new Promise((resolve) => {
|
|
1099
1099
|
this.pendingNativeCrops.set(requestId, { resolve });
|
|
@@ -1107,7 +1107,7 @@ var SessionDecodeSession = class {
|
|
|
1107
1107
|
});
|
|
1108
1108
|
} catch {
|
|
1109
1109
|
this.pendingNativeCrops.delete(requestId);
|
|
1110
|
-
resolve(
|
|
1110
|
+
resolve({ kind: "miss" });
|
|
1111
1111
|
}
|
|
1112
1112
|
});
|
|
1113
1113
|
}
|
|
@@ -1187,10 +1187,13 @@ var SessionDecodeSession = class {
|
|
|
1187
1187
|
return;
|
|
1188
1188
|
case "nativeCropResult":
|
|
1189
1189
|
this.settleNativeCrop(raw.requestId, {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1190
|
+
kind: "crop",
|
|
1191
|
+
crop: {
|
|
1192
|
+
bytes: raw.bytes,
|
|
1193
|
+
width: raw.width,
|
|
1194
|
+
height: raw.height,
|
|
1195
|
+
...raw.source ? { source: raw.source } : {}
|
|
1196
|
+
}
|
|
1194
1197
|
});
|
|
1195
1198
|
return;
|
|
1196
1199
|
case "nativeCropMiss":
|
|
@@ -1198,7 +1201,10 @@ var SessionDecodeSession = class {
|
|
|
1198
1201
|
requestId: raw.requestId,
|
|
1199
1202
|
reason: raw.reason
|
|
1200
1203
|
} });
|
|
1201
|
-
this.settleNativeCrop(raw.requestId,
|
|
1204
|
+
this.settleNativeCrop(raw.requestId, {
|
|
1205
|
+
kind: "miss",
|
|
1206
|
+
...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
|
|
1207
|
+
});
|
|
1202
1208
|
return;
|
|
1203
1209
|
case "error":
|
|
1204
1210
|
this.settleError(raw.message, raw.frameId);
|
|
@@ -1235,15 +1241,15 @@ var SessionDecodeSession = class {
|
|
|
1235
1241
|
}
|
|
1236
1242
|
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
|
|
1237
1243
|
this.pendingToBuffers.clear();
|
|
1238
|
-
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(
|
|
1244
|
+
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve({ kind: "miss" });
|
|
1239
1245
|
this.pendingNativeCrops.clear();
|
|
1240
1246
|
}
|
|
1241
1247
|
/** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
|
|
1242
|
-
settleNativeCrop(requestId,
|
|
1248
|
+
settleNativeCrop(requestId, attempt) {
|
|
1243
1249
|
const waiter = this.pendingNativeCrops.get(requestId);
|
|
1244
1250
|
if (!waiter) return;
|
|
1245
1251
|
this.pendingNativeCrops.delete(requestId);
|
|
1246
|
-
waiter.resolve(
|
|
1252
|
+
waiter.resolve(attempt);
|
|
1247
1253
|
}
|
|
1248
1254
|
settlePendingPull(outcome) {
|
|
1249
1255
|
const waiter = this.pendingPull;
|
|
@@ -2907,7 +2913,8 @@ async function runDetailSubtree(deps, input) {
|
|
|
2907
2913
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2908
2914
|
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2909
2915
|
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2910
|
-
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
2916
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null,
|
|
2917
|
+
nativeWorkerReason: nativeMiss?.workerReason ?? null
|
|
2911
2918
|
}
|
|
2912
2919
|
});
|
|
2913
2920
|
return null;
|
|
@@ -7243,6 +7250,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7243
7250
|
handleNodeId: input.handle.nodeId,
|
|
7244
7251
|
nativeMissReason: resolved.missReason,
|
|
7245
7252
|
nativeHandleAgeMs: resolved.handleAgeMs,
|
|
7253
|
+
...resolved.workerReason !== void 0 ? { nativeWorkerReason: resolved.workerReason } : {},
|
|
7246
7254
|
maxWidth: input.maxWidth ?? null,
|
|
7247
7255
|
hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
|
|
7248
7256
|
}
|
|
@@ -7526,18 +7534,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7526
7534
|
let nativeResult = null;
|
|
7527
7535
|
let missReason = null;
|
|
7528
7536
|
let handleAgeMs = null;
|
|
7537
|
+
let workerReason;
|
|
7529
7538
|
if (!isUnboundedFullFrame) {
|
|
7530
7539
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
7531
7540
|
handleAgeMs = found?.ageMs ?? null;
|
|
7532
7541
|
if (found) {
|
|
7533
|
-
const
|
|
7534
|
-
if (
|
|
7535
|
-
bytes:
|
|
7536
|
-
width:
|
|
7537
|
-
height:
|
|
7538
|
-
...
|
|
7542
|
+
const attempt = await found.request(input.bbox, input.maxWidth);
|
|
7543
|
+
if (attempt.kind === "crop") nativeResult = {
|
|
7544
|
+
bytes: attempt.crop.bytes,
|
|
7545
|
+
width: attempt.crop.width,
|
|
7546
|
+
height: attempt.crop.height,
|
|
7547
|
+
...attempt.crop.source ? { source: attempt.crop.source } : {}
|
|
7539
7548
|
};
|
|
7540
|
-
else
|
|
7549
|
+
else {
|
|
7550
|
+
missReason = "worker-lease-gone";
|
|
7551
|
+
workerReason = attempt.workerReason;
|
|
7552
|
+
}
|
|
7541
7553
|
} else missReason = "no-registry-entry";
|
|
7542
7554
|
if (missReason !== null) {
|
|
7543
7555
|
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
@@ -7550,7 +7562,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7550
7562
|
handleNodeId: input.handle.nodeId,
|
|
7551
7563
|
handleAgeMs,
|
|
7552
7564
|
registrySize: this.nativeCropRegistry.size,
|
|
7553
|
-
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
|
|
7565
|
+
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP,
|
|
7566
|
+
...workerReason !== void 0 ? { workerReason } : {}
|
|
7554
7567
|
}
|
|
7555
7568
|
};
|
|
7556
7569
|
if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
|
|
@@ -7561,7 +7574,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7561
7574
|
pixels: nativeResult,
|
|
7562
7575
|
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
7563
7576
|
missReason,
|
|
7564
|
-
handleAgeMs
|
|
7577
|
+
handleAgeMs,
|
|
7578
|
+
workerReason
|
|
7565
7579
|
};
|
|
7566
7580
|
}
|
|
7567
7581
|
/**
|
|
@@ -7871,7 +7885,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
|
|
|
7871
7885
|
});
|
|
7872
7886
|
if (resolved.pixels === null || resolved.tier !== "native") return {
|
|
7873
7887
|
miss: resolved.missReason ?? "worker-lease-gone",
|
|
7874
|
-
handleAgeMs: resolved.handleAgeMs
|
|
7888
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
7889
|
+
...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
|
|
7875
7890
|
};
|
|
7876
7891
|
const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
|
|
7877
7892
|
if (!encoded?.jpeg) return {
|
|
@@ -1087,7 +1087,7 @@ var SessionDecodeSession = class {
|
|
|
1087
1087
|
* rejects — native crops are strictly best-effort with a free fallback.
|
|
1088
1088
|
*/
|
|
1089
1089
|
requestNativeCrop(frameId, bbox, maxWidth) {
|
|
1090
|
-
if (this.exited) return Promise.resolve(
|
|
1090
|
+
if (this.exited) return Promise.resolve({ kind: "miss" });
|
|
1091
1091
|
const requestId = this.nextNativeCropId++;
|
|
1092
1092
|
return new Promise((resolve) => {
|
|
1093
1093
|
this.pendingNativeCrops.set(requestId, { resolve });
|
|
@@ -1101,7 +1101,7 @@ var SessionDecodeSession = class {
|
|
|
1101
1101
|
});
|
|
1102
1102
|
} catch {
|
|
1103
1103
|
this.pendingNativeCrops.delete(requestId);
|
|
1104
|
-
resolve(
|
|
1104
|
+
resolve({ kind: "miss" });
|
|
1105
1105
|
}
|
|
1106
1106
|
});
|
|
1107
1107
|
}
|
|
@@ -1181,10 +1181,13 @@ var SessionDecodeSession = class {
|
|
|
1181
1181
|
return;
|
|
1182
1182
|
case "nativeCropResult":
|
|
1183
1183
|
this.settleNativeCrop(raw.requestId, {
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1184
|
+
kind: "crop",
|
|
1185
|
+
crop: {
|
|
1186
|
+
bytes: raw.bytes,
|
|
1187
|
+
width: raw.width,
|
|
1188
|
+
height: raw.height,
|
|
1189
|
+
...raw.source ? { source: raw.source } : {}
|
|
1190
|
+
}
|
|
1188
1191
|
});
|
|
1189
1192
|
return;
|
|
1190
1193
|
case "nativeCropMiss":
|
|
@@ -1192,7 +1195,10 @@ var SessionDecodeSession = class {
|
|
|
1192
1195
|
requestId: raw.requestId,
|
|
1193
1196
|
reason: raw.reason
|
|
1194
1197
|
} });
|
|
1195
|
-
this.settleNativeCrop(raw.requestId,
|
|
1198
|
+
this.settleNativeCrop(raw.requestId, {
|
|
1199
|
+
kind: "miss",
|
|
1200
|
+
...raw.reason !== void 0 ? { workerReason: raw.reason } : {}
|
|
1201
|
+
});
|
|
1196
1202
|
return;
|
|
1197
1203
|
case "error":
|
|
1198
1204
|
this.settleError(raw.message, raw.frameId);
|
|
@@ -1229,15 +1235,15 @@ var SessionDecodeSession = class {
|
|
|
1229
1235
|
}
|
|
1230
1236
|
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
|
|
1231
1237
|
this.pendingToBuffers.clear();
|
|
1232
|
-
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(
|
|
1238
|
+
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve({ kind: "miss" });
|
|
1233
1239
|
this.pendingNativeCrops.clear();
|
|
1234
1240
|
}
|
|
1235
1241
|
/** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
|
|
1236
|
-
settleNativeCrop(requestId,
|
|
1242
|
+
settleNativeCrop(requestId, attempt) {
|
|
1237
1243
|
const waiter = this.pendingNativeCrops.get(requestId);
|
|
1238
1244
|
if (!waiter) return;
|
|
1239
1245
|
this.pendingNativeCrops.delete(requestId);
|
|
1240
|
-
waiter.resolve(
|
|
1246
|
+
waiter.resolve(attempt);
|
|
1241
1247
|
}
|
|
1242
1248
|
settlePendingPull(outcome) {
|
|
1243
1249
|
const waiter = this.pendingPull;
|
|
@@ -2900,7 +2906,8 @@ async function runDetailSubtree(deps, input) {
|
|
|
2900
2906
|
hadFrameJpeg: input.frameJpeg !== void 0,
|
|
2901
2907
|
hadCropJpeg: input.cropJpeg !== void 0,
|
|
2902
2908
|
nativeMissReason: nativeMiss?.miss ?? null,
|
|
2903
|
-
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null
|
|
2909
|
+
nativeHandleAgeMs: nativeMiss?.handleAgeMs ?? null,
|
|
2910
|
+
nativeWorkerReason: nativeMiss?.workerReason ?? null
|
|
2904
2911
|
}
|
|
2905
2912
|
});
|
|
2906
2913
|
return null;
|
|
@@ -7236,6 +7243,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7236
7243
|
handleNodeId: input.handle.nodeId,
|
|
7237
7244
|
nativeMissReason: resolved.missReason,
|
|
7238
7245
|
nativeHandleAgeMs: resolved.handleAgeMs,
|
|
7246
|
+
...resolved.workerReason !== void 0 ? { nativeWorkerReason: resolved.workerReason } : {},
|
|
7239
7247
|
maxWidth: input.maxWidth ?? null,
|
|
7240
7248
|
hint: "both the native lease and the retained-frame window have closed for this frame — the detail dispatch that asked for it produces NO crop"
|
|
7241
7249
|
}
|
|
@@ -7519,18 +7527,22 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7519
7527
|
let nativeResult = null;
|
|
7520
7528
|
let missReason = null;
|
|
7521
7529
|
let handleAgeMs = null;
|
|
7530
|
+
let workerReason;
|
|
7522
7531
|
if (!isUnboundedFullFrame) {
|
|
7523
7532
|
const found = this.nativeCropRegistry.get(input.handle);
|
|
7524
7533
|
handleAgeMs = found?.ageMs ?? null;
|
|
7525
7534
|
if (found) {
|
|
7526
|
-
const
|
|
7527
|
-
if (
|
|
7528
|
-
bytes:
|
|
7529
|
-
width:
|
|
7530
|
-
height:
|
|
7531
|
-
...
|
|
7535
|
+
const attempt = await found.request(input.bbox, input.maxWidth);
|
|
7536
|
+
if (attempt.kind === "crop") nativeResult = {
|
|
7537
|
+
bytes: attempt.crop.bytes,
|
|
7538
|
+
width: attempt.crop.width,
|
|
7539
|
+
height: attempt.crop.height,
|
|
7540
|
+
...attempt.crop.source ? { source: attempt.crop.source } : {}
|
|
7532
7541
|
};
|
|
7533
|
-
else
|
|
7542
|
+
else {
|
|
7543
|
+
missReason = "worker-lease-gone";
|
|
7544
|
+
workerReason = attempt.workerReason;
|
|
7545
|
+
}
|
|
7534
7546
|
} else missReason = "no-registry-entry";
|
|
7535
7547
|
if (missReason !== null) {
|
|
7536
7548
|
const tierOnMiss = resolveNativeCropSource(isFullFrame, false);
|
|
@@ -7543,7 +7555,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7543
7555
|
handleNodeId: input.handle.nodeId,
|
|
7544
7556
|
handleAgeMs,
|
|
7545
7557
|
registrySize: this.nativeCropRegistry.size,
|
|
7546
|
-
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP
|
|
7558
|
+
registryCap: DEFAULT_NATIVE_CROP_REGISTRY_CAP,
|
|
7559
|
+
...workerReason !== void 0 ? { workerReason } : {}
|
|
7547
7560
|
}
|
|
7548
7561
|
};
|
|
7549
7562
|
if (tierOnMiss === "miss") this.ctx.logger.warn("native crop miss", fields);
|
|
@@ -7554,7 +7567,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7554
7567
|
pixels: nativeResult,
|
|
7555
7568
|
tier: resolveNativeCropSource(isFullFrame, nativeResult !== null),
|
|
7556
7569
|
missReason,
|
|
7557
|
-
handleAgeMs
|
|
7570
|
+
handleAgeMs,
|
|
7571
|
+
workerReason
|
|
7558
7572
|
};
|
|
7559
7573
|
}
|
|
7560
7574
|
/**
|
|
@@ -7864,7 +7878,8 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
|
|
|
7864
7878
|
});
|
|
7865
7879
|
if (resolved.pixels === null || resolved.tier !== "native") return {
|
|
7866
7880
|
miss: resolved.missReason ?? "worker-lease-gone",
|
|
7867
|
-
handleAgeMs: resolved.handleAgeMs
|
|
7881
|
+
handleAgeMs: resolved.handleAgeMs,
|
|
7882
|
+
...resolved.workerReason !== void 0 ? { workerReason: resolved.workerReason } : {}
|
|
7868
7883
|
};
|
|
7869
7884
|
const encoded = await this.encodeNativeCropResult(resolved.pixels, "native", true);
|
|
7870
7885
|
if (!encoded?.jpeg) return {
|
|
@@ -656,20 +656,6 @@ var NativeLeaseStore = class {
|
|
|
656
656
|
}
|
|
657
657
|
};
|
|
658
658
|
//#endregion
|
|
659
|
-
//#region src/session-decode/to-buffer-miss.ts
|
|
660
|
-
/** Which rung lost the frame, from what the worker knows at the miss. */
|
|
661
|
-
function toBufferMissRung(facts) {
|
|
662
|
-
if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
|
|
663
|
-
if (!facts.marked) return "never-leased";
|
|
664
|
-
if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
|
|
665
|
-
return "released";
|
|
666
|
-
}
|
|
667
|
-
/** The error line the runner sees: the rung first, the numbers after it. */
|
|
668
|
-
function describeToBufferMiss(facts) {
|
|
669
|
-
const rung = toBufferMissRung(facts);
|
|
670
|
-
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
|
|
671
|
-
}
|
|
672
|
-
//#endregion
|
|
673
659
|
//#region src/session-decode/native-tile-geometry.ts
|
|
674
660
|
/**
|
|
675
661
|
* Pure geometry for the **native subject tile** — the compressed, subject-sized
|
|
@@ -921,6 +907,127 @@ var NativeTileStore = class {
|
|
|
921
907
|
this.bytes -= entry.bytes;
|
|
922
908
|
}
|
|
923
909
|
};
|
|
910
|
+
/**
|
|
911
|
+
* The set of tile cuts currently between "I have this frame's pixels" and
|
|
912
|
+
* "the stores can answer for it", plus the latency of the ones that finished.
|
|
913
|
+
*/
|
|
914
|
+
var PendingCuts = class {
|
|
915
|
+
waitMs;
|
|
916
|
+
now;
|
|
917
|
+
latencySampleCap;
|
|
918
|
+
cuts = /* @__PURE__ */ new Map();
|
|
919
|
+
/** Insertion-ordered ring of completed cut latencies (ms). */
|
|
920
|
+
latencies = [];
|
|
921
|
+
constructor(options) {
|
|
922
|
+
this.waitMs = Math.max(0, options.waitMs);
|
|
923
|
+
this.now = options.now ?? Date.now;
|
|
924
|
+
this.latencySampleCap = Math.max(1, options.latencySamples ?? 256);
|
|
925
|
+
}
|
|
926
|
+
/** Cuts currently in flight (tests / metrics). */
|
|
927
|
+
get inFlight() {
|
|
928
|
+
return this.cuts.size;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* A cut for `frameId` has started. Call BEFORE the first `await` in the cut,
|
|
932
|
+
* so no request can observe the gap this module exists to cover.
|
|
933
|
+
*
|
|
934
|
+
* A duplicate id should never occur (frame ids are monotonic); settling the
|
|
935
|
+
* prior entry rather than replacing it keeps any waiter on it from hanging.
|
|
936
|
+
*/
|
|
937
|
+
begin(frameId) {
|
|
938
|
+
this.settle(frameId);
|
|
939
|
+
let finish = () => {};
|
|
940
|
+
const done = new Promise((resolve) => {
|
|
941
|
+
finish = resolve;
|
|
942
|
+
});
|
|
943
|
+
this.cuts.set(frameId, {
|
|
944
|
+
startedAt: this.now(),
|
|
945
|
+
done,
|
|
946
|
+
finish
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* The cut for `frameId` is over — the stores now hold whatever it produced.
|
|
951
|
+
* Call from the cut's `finally`, so a throwing encode releases waiters too.
|
|
952
|
+
* Idempotent.
|
|
953
|
+
*/
|
|
954
|
+
settle(frameId) {
|
|
955
|
+
const cut = this.cuts.get(frameId);
|
|
956
|
+
if (!cut) return;
|
|
957
|
+
this.cuts.delete(frameId);
|
|
958
|
+
this.noteLatency(this.now() - cut.startedAt);
|
|
959
|
+
cut.finish();
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Wait for `frameId`'s cut to land, bounded.
|
|
963
|
+
*
|
|
964
|
+
* `none` means there was nothing in flight, and the caller must NOT read that
|
|
965
|
+
* as "wait exhausted" — it is the ordinary answer for a frame whose cut has
|
|
966
|
+
* already finished or never ran.
|
|
967
|
+
*/
|
|
968
|
+
async waitFor(frameId) {
|
|
969
|
+
const cut = this.cuts.get(frameId);
|
|
970
|
+
if (!cut) return "none";
|
|
971
|
+
if (this.waitMs === 0) return "timeout";
|
|
972
|
+
let timer;
|
|
973
|
+
const expired = new Promise((resolve) => {
|
|
974
|
+
timer = setTimeout(() => resolve("timeout"), this.waitMs);
|
|
975
|
+
timer.unref?.();
|
|
976
|
+
});
|
|
977
|
+
try {
|
|
978
|
+
return await Promise.race([cut.done.then(() => "settled"), expired]);
|
|
979
|
+
} finally {
|
|
980
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Release every waiter and forget every in-flight cut — re-dial and teardown.
|
|
985
|
+
* A waiter left behind here would outlive the worker that could settle it.
|
|
986
|
+
*/
|
|
987
|
+
clear() {
|
|
988
|
+
for (const cut of this.cuts.values()) cut.finish();
|
|
989
|
+
this.cuts.clear();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The `percentile`-th cut latency in ms over the retained samples, or `null`
|
|
993
|
+
* when nothing has completed yet. `null` rather than 0: a worker that has cut
|
|
994
|
+
* nothing and a worker that cuts instantly must not read the same.
|
|
995
|
+
*/
|
|
996
|
+
latencyMs(percentile) {
|
|
997
|
+
if (this.latencies.length === 0) return null;
|
|
998
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
999
|
+
const clamped = Math.min(100, Math.max(0, percentile));
|
|
1000
|
+
const index = Math.min(sorted.length - 1, Math.ceil(clamped / 100 * sorted.length) - 1);
|
|
1001
|
+
return sorted[Math.max(0, index)] ?? null;
|
|
1002
|
+
}
|
|
1003
|
+
noteLatency(ms) {
|
|
1004
|
+
this.latencies.push(Math.max(0, ms));
|
|
1005
|
+
while (this.latencies.length > this.latencySampleCap) this.latencies.shift();
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
/**
|
|
1009
|
+
* Serve a frame's retained tiles, waiting for an in-flight cut ONLY when
|
|
1010
|
+
* waiting could change the answer.
|
|
1011
|
+
*
|
|
1012
|
+
* The order is the whole point. Anything already retained is served at once —
|
|
1013
|
+
* a cut in flight for a LATER subject of the same frame must not delay a
|
|
1014
|
+
* request the stores can already answer. Only an empty store consults
|
|
1015
|
+
* {@link PendingCuts}, and only then is a success attributable to the wait.
|
|
1016
|
+
*/
|
|
1017
|
+
async function attemptTileServe(pending, frameId, source) {
|
|
1018
|
+
if (source.hasRetained()) return {
|
|
1019
|
+
served: await source.serve(),
|
|
1020
|
+
wait: "none",
|
|
1021
|
+
resolvedByWait: false
|
|
1022
|
+
};
|
|
1023
|
+
const wait = await pending.waitFor(frameId);
|
|
1024
|
+
const served = await source.serve();
|
|
1025
|
+
return {
|
|
1026
|
+
served,
|
|
1027
|
+
wait,
|
|
1028
|
+
resolvedByWait: served !== null && wait === "settled"
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
924
1031
|
//#endregion
|
|
925
1032
|
//#region src/session-decode/planar-crop-source.ts
|
|
926
1033
|
/** Layout a detection `toBuffer` can read, or `null` when it must name the
|
|
@@ -950,6 +1057,19 @@ function nativeCropSourceTag(format) {
|
|
|
950
1057
|
case "rgb24": return "rgb:";
|
|
951
1058
|
}
|
|
952
1059
|
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Bytes a crop of `rows` rows actually reads, measured from the crop's ORIGIN.
|
|
1062
|
+
*
|
|
1063
|
+
* The LAST row needs only its own `rowBytes`, never a whole stride — the stride
|
|
1064
|
+
* is the distance to the NEXT row, and for the last row there is no next row.
|
|
1065
|
+
* Demanding `stride * rows` instead overshoots the plane's end by exactly the
|
|
1066
|
+
* horizontal offset, which is invisible everywhere except where the crop's
|
|
1067
|
+
* bottom IS the frame's bottom. See {@link resolvePlanarCropSource}.
|
|
1068
|
+
*/
|
|
1069
|
+
function planarSpan(rows, stride, rowBytes) {
|
|
1070
|
+
if (rows <= 0) return 0;
|
|
1071
|
+
return (rows - 1) * stride + rowBytes;
|
|
1072
|
+
}
|
|
953
1073
|
/** A plane view starting at `offset`, or `null` when the plane is too short. */
|
|
954
1074
|
function viewAt(plane, offset, requiredSpan) {
|
|
955
1075
|
if (!plane) return null;
|
|
@@ -968,6 +1088,28 @@ function viewAt(plane, offset, requiredSpan) {
|
|
|
968
1088
|
* An odd origin is floored onto the chroma grid rather than rejected: the
|
|
969
1089
|
* geometry helper already aligns to even, and flooring reads the chroma sample
|
|
970
1090
|
* that CONTAINS the origin instead of inventing a half sample.
|
|
1091
|
+
*
|
|
1092
|
+
* ## The bottom edge (2026-09-07)
|
|
1093
|
+
*
|
|
1094
|
+
* The span demanded from each plane used to be `stride * region.height` from
|
|
1095
|
+
* the crop ORIGIN. A stride is the distance to the NEXT row, and the last row
|
|
1096
|
+
* of a crop has no next row — it needs only `left + width` bytes. So the demand
|
|
1097
|
+
* overshot the plane's end by exactly `left`, which is invisible everywhere
|
|
1098
|
+
* except when the crop's bottom IS the frame's last row. `resolveNativeTileRegion`
|
|
1099
|
+
* clamps a padded box to precisely that, and `clampEven` floors both `top` and
|
|
1100
|
+
* `height`, so a bottom-clamped tile lands flush every time.
|
|
1101
|
+
*
|
|
1102
|
+
* The result was that every subject standing near the camera — the biggest, the
|
|
1103
|
+
* closest, the best shot — lost its native tile, and the hold path threw for
|
|
1104
|
+
* the same rectangles. Measured over 6 h on the live cluster: **100 of 100**
|
|
1105
|
+
* logged `missing plane data` geometries had `top + height === frameHeight`
|
|
1106
|
+
* with `left > 0`, 101 of 102 on dev:590 (salone, 3840x2160).
|
|
1107
|
+
*
|
|
1108
|
+
* The check is now the region's real extent, so a crop that genuinely runs past
|
|
1109
|
+
* a plane — one row too tall, or a last row reaching past the stride — is still
|
|
1110
|
+
* refused. The views handed back are unchanged: `subarray(offset)` always ran
|
|
1111
|
+
* to the plane's true end, so nothing here ever sized a buffer, and tightening
|
|
1112
|
+
* the predicate cannot shorten one.
|
|
971
1113
|
*/
|
|
972
1114
|
function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
973
1115
|
if (!planes || planes.length < PLANE_COUNT[kind]) return null;
|
|
@@ -978,10 +1120,11 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
|
978
1120
|
const left = Math.max(0, region.left);
|
|
979
1121
|
const chromaRow = Math.floor(top / 2);
|
|
980
1122
|
const chromaHeight = Math.ceil(region.height / 2);
|
|
981
|
-
const
|
|
1123
|
+
const chromaWidth = Math.ceil(Math.max(0, region.width) / 2);
|
|
1124
|
+
const y = viewAt(planes[0], top * yStride + left, planarSpan(region.height, yStride, region.width));
|
|
982
1125
|
if (kind === "nv12") {
|
|
983
1126
|
const uvOffset = chromaRow * cStride + Math.floor(left / 2) * 2;
|
|
984
|
-
const uv = viewAt(planes[1], uvOffset, cStride *
|
|
1127
|
+
const uv = viewAt(planes[1], uvOffset, planarSpan(chromaHeight, cStride, chromaWidth * 2));
|
|
985
1128
|
if (!y || !uv) return null;
|
|
986
1129
|
return {
|
|
987
1130
|
planes: [y, uv],
|
|
@@ -990,8 +1133,8 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
|
990
1133
|
}
|
|
991
1134
|
const vStride = strides?.[2] ?? cStride;
|
|
992
1135
|
const chromaOffset = chromaRow * cStride + Math.floor(left / 2);
|
|
993
|
-
const u = viewAt(planes[1], chromaOffset, cStride
|
|
994
|
-
const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), vStride
|
|
1136
|
+
const u = viewAt(planes[1], chromaOffset, planarSpan(chromaHeight, cStride, chromaWidth));
|
|
1137
|
+
const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), planarSpan(chromaHeight, vStride, chromaWidth));
|
|
995
1138
|
if (!y || !u || !v) return null;
|
|
996
1139
|
return {
|
|
997
1140
|
planes: [
|
|
@@ -1331,6 +1474,20 @@ async function cutSubjectTiles(frame, bboxes, ports) {
|
|
|
1331
1474
|
return tiles;
|
|
1332
1475
|
}
|
|
1333
1476
|
//#endregion
|
|
1477
|
+
//#region src/session-decode/to-buffer-miss.ts
|
|
1478
|
+
/** Which rung lost the frame, from what the worker knows at the miss. */
|
|
1479
|
+
function toBufferMissRung(facts) {
|
|
1480
|
+
if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
|
|
1481
|
+
if (!facts.marked) return "never-leased";
|
|
1482
|
+
if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
|
|
1483
|
+
return "released";
|
|
1484
|
+
}
|
|
1485
|
+
/** The error line the runner sees: the rung first, the numbers after it. */
|
|
1486
|
+
function describeToBufferMiss(facts) {
|
|
1487
|
+
const rung = toBufferMissRung(facts);
|
|
1488
|
+
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
|
|
1489
|
+
}
|
|
1490
|
+
//#endregion
|
|
1334
1491
|
//#region src/session-decode/decode-worker-child.ts
|
|
1335
1492
|
/** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
|
|
1336
1493
|
function toLeasedNavFrame(frame) {
|
|
@@ -1458,6 +1615,28 @@ var NATIVE_TILE_MAX_PER_FRAME = (() => {
|
|
|
1458
1615
|
/** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
|
|
1459
1616
|
var NATIVE_TILE_JPEG_QUALITY = 90;
|
|
1460
1617
|
/**
|
|
1618
|
+
* How long a native-crop request waits for a tile cut that is still ENCODING.
|
|
1619
|
+
*
|
|
1620
|
+
* The cut reads its pixels synchronously at `FrameResult` time but only writes
|
|
1621
|
+
* the stores after N JPEG encodes; a request landing in between reads two empty
|
|
1622
|
+
* stores and is told `no retained tile for this frame` — the same sentence a
|
|
1623
|
+
* frame that was never cut gets. Measured 2026-09-07: misses onset at ~100 ms
|
|
1624
|
+
* (only 0.1% below it) and peak at 500-1000 ms, on cameras that cut a tile for
|
|
1625
|
+
* 99.7% of their inferred frames, so the window — not any lifetime — is what
|
|
1626
|
+
* they are landing in.
|
|
1627
|
+
*
|
|
1628
|
+
* 500 ms deliberately, not more: the confirmation gate bounds each crop attempt
|
|
1629
|
+
* at 300 ms and the capture scheduler drops an enrichment request older than
|
|
1630
|
+
* 3 s, so a miss held too long stops being a fast retryable `null` and becomes
|
|
1631
|
+
* an upstream timeout. Widen it via `CAMSTACK_SESSION_TILE_CUT_WAIT_MS` only
|
|
1632
|
+
* once `cutLatencyMs` on the metrics line says what the encode window really
|
|
1633
|
+
* is; `0` disables the wait and restores the pre-2026-09-07 miss profile.
|
|
1634
|
+
*/
|
|
1635
|
+
var TILE_CUT_WAIT_MS = (() => {
|
|
1636
|
+
const raw = Number(process.env["CAMSTACK_SESSION_TILE_CUT_WAIT_MS"]);
|
|
1637
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 500;
|
|
1638
|
+
})();
|
|
1639
|
+
/**
|
|
1461
1640
|
* Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
|
|
1462
1641
|
* per-frame native downloads run only within this window of the last
|
|
1463
1642
|
* native-crop request (or dial start). `0` disables the GATE (legacy always-on
|
|
@@ -1598,6 +1777,14 @@ function errMessage(err) {
|
|
|
1598
1777
|
return String(err);
|
|
1599
1778
|
}
|
|
1600
1779
|
/**
|
|
1780
|
+
* Render a cut-latency percentile for the metrics line. `-` when nothing has
|
|
1781
|
+
* been cut yet: a worker that cut nothing and one that cuts in 0 ms must not
|
|
1782
|
+
* read the same, or the number that sizes {@link TILE_CUT_WAIT_MS} is a guess.
|
|
1783
|
+
*/
|
|
1784
|
+
function formatLatency(ms) {
|
|
1785
|
+
return ms === null ? "-" : String(Math.round(ms));
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1601
1788
|
* RTSP low-latency demuxer options — mirrors
|
|
1602
1789
|
* `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
|
|
1603
1790
|
* than cross-imported: this file is a standalone forked entry point, not an
|
|
@@ -1802,6 +1989,19 @@ var DecodeWorkerChild = class {
|
|
|
1802
1989
|
tileHits = 0;
|
|
1803
1990
|
tileMisses = 0;
|
|
1804
1991
|
tileCutFailures = 0;
|
|
1992
|
+
/**
|
|
1993
|
+
* Serves that produced pixels ONLY because the request waited for a cut that
|
|
1994
|
+
* was still encoding — i.e. misses before {@link TILE_CUT_WAIT_MS} existed.
|
|
1995
|
+
*
|
|
1996
|
+
* This is the counter the write-gap hypothesis stands or falls on. If it
|
|
1997
|
+
* stays near zero while `tileMisses` does not move, the misses were never a
|
|
1998
|
+
* timing race and the question becomes WHICH frames get cut at all.
|
|
1999
|
+
*/
|
|
2000
|
+
tileMissesResolvedByWait = 0;
|
|
2001
|
+
/** Requests that gave up on an in-flight cut at the bound — dropped work. */
|
|
2002
|
+
tileWaitTimeouts = 0;
|
|
2003
|
+
/** Cuts between "I have the pixels" and "the stores can answer". */
|
|
2004
|
+
pendingCuts = new PendingCuts({ waitMs: TILE_CUT_WAIT_MS });
|
|
1805
2005
|
/** Scene tiles cut / served, on the throughput line. */
|
|
1806
2006
|
scenesCut = 0;
|
|
1807
2007
|
sceneHits = 0;
|
|
@@ -2058,7 +2258,7 @@ var DecodeWorkerChild = class {
|
|
|
2058
2258
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
2059
2259
|
const rssMb = Math.round(process.memoryUsage().rss / 1048576);
|
|
2060
2260
|
const ageS = Math.round((now - this.startedAt) / 1e3);
|
|
2061
|
-
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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2261
|
+
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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMissesResolvedByWait:${this.tileMissesResolvedByWait}, tileWaitTimeouts:${this.tileWaitTimeouts}, cutLatencyMsP50:${formatLatency(this.pendingCuts.latencyMs(50))}, cutLatencyMsP95:${formatLatency(this.pendingCuts.latencyMs(95))}, cutsInFlight:${this.pendingCuts.inFlight}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2062
2262
|
this.lastMetricsAt = now;
|
|
2063
2263
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
2064
2264
|
if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
|
|
@@ -2210,10 +2410,16 @@ var DecodeWorkerChild = class {
|
|
|
2210
2410
|
*/
|
|
2211
2411
|
async serveTileFallback(requestId, frameId, bbox, maxWidth) {
|
|
2212
2412
|
try {
|
|
2213
|
-
const
|
|
2413
|
+
const attempt = await attemptTileServe(this.pendingCuts, frameId, {
|
|
2414
|
+
hasRetained: () => this.tileStore.get(frameId).length > 0 || this.sceneStore.get(frameId).length > 0,
|
|
2415
|
+
serve: () => this.serveFromTile(frameId, bbox, maxWidth)
|
|
2416
|
+
});
|
|
2417
|
+
const served = attempt.served;
|
|
2418
|
+
if (attempt.wait === "timeout") this.tileWaitTimeouts += 1;
|
|
2214
2419
|
if (served) {
|
|
2215
2420
|
this.tileHits += 1;
|
|
2216
2421
|
this.nativeCropHits++;
|
|
2422
|
+
if (attempt.resolvedByWait) this.tileMissesResolvedByWait += 1;
|
|
2217
2423
|
this.send({
|
|
2218
2424
|
kind: "nativeCropResult",
|
|
2219
2425
|
requestId,
|
|
@@ -2229,7 +2435,7 @@ var DecodeWorkerChild = class {
|
|
|
2229
2435
|
this.send({
|
|
2230
2436
|
kind: "nativeCropMiss",
|
|
2231
2437
|
requestId,
|
|
2232
|
-
reason: this.tileMissReason(frameId)
|
|
2438
|
+
reason: this.tileMissReason(frameId, attempt.wait)
|
|
2233
2439
|
});
|
|
2234
2440
|
} catch (err) {
|
|
2235
2441
|
this.tileMisses += 1;
|
|
@@ -2249,7 +2455,8 @@ var DecodeWorkerChild = class {
|
|
|
2249
2455
|
* "the rectangle is outside every container" are three different sentences,
|
|
2250
2456
|
* and one "not retained" hid all of them.
|
|
2251
2457
|
*/
|
|
2252
|
-
tileMissReason(frameId) {
|
|
2458
|
+
tileMissReason(frameId, wait) {
|
|
2459
|
+
if (wait === "timeout") return `frame ${frameId} released and its tile cut did not finish within ${TILE_CUT_WAIT_MS} ms — the request outran the encode, not the retention`;
|
|
2253
2460
|
if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
|
|
2254
2461
|
if (!this.sceneStore.enabled) return `frame ${frameId} released and no retained subject tile contains the requested rectangle; scene tiles are DISABLED (sceneBudgetMb 0), so a full-frame request cannot be served at all`;
|
|
2255
2462
|
if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
|
|
@@ -2281,6 +2488,7 @@ var DecodeWorkerChild = class {
|
|
|
2281
2488
|
* explanation, which is the failure this whole redesign exists to remove.
|
|
2282
2489
|
*/
|
|
2283
2490
|
async handleCutTiles(frameId, bboxes) {
|
|
2491
|
+
this.pendingCuts.begin(frameId);
|
|
2284
2492
|
try {
|
|
2285
2493
|
if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
|
|
2286
2494
|
const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
|
|
@@ -2362,6 +2570,7 @@ var DecodeWorkerChild = class {
|
|
|
2362
2570
|
this.sceneStore.put(frameId, [scene]);
|
|
2363
2571
|
}
|
|
2364
2572
|
} finally {
|
|
2573
|
+
this.pendingCuts.settle(frameId);
|
|
2365
2574
|
this.noteLeaseRelease(frameId, this.leaseStore.release(frameId), "cut");
|
|
2366
2575
|
}
|
|
2367
2576
|
}
|
|
@@ -3144,6 +3353,7 @@ var DecodeWorkerChild = class {
|
|
|
3144
3353
|
this.leaseStore.clear();
|
|
3145
3354
|
this.tileStore.clear();
|
|
3146
3355
|
this.sceneStore.clear();
|
|
3356
|
+
this.pendingCuts.clear();
|
|
3147
3357
|
this.tileFrameWidth = 0;
|
|
3148
3358
|
this.tileFrameHeight = 0;
|
|
3149
3359
|
this.nativeLeaseDownloadFilter?.close();
|
|
@@ -655,20 +655,6 @@ var NativeLeaseStore = class {
|
|
|
655
655
|
}
|
|
656
656
|
};
|
|
657
657
|
//#endregion
|
|
658
|
-
//#region src/session-decode/to-buffer-miss.ts
|
|
659
|
-
/** Which rung lost the frame, from what the worker knows at the miss. */
|
|
660
|
-
function toBufferMissRung(facts) {
|
|
661
|
-
if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
|
|
662
|
-
if (!facts.marked) return "never-leased";
|
|
663
|
-
if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
|
|
664
|
-
return "released";
|
|
665
|
-
}
|
|
666
|
-
/** The error line the runner sees: the rung first, the numbers after it. */
|
|
667
|
-
function describeToBufferMiss(facts) {
|
|
668
|
-
const rung = toBufferMissRung(facts);
|
|
669
|
-
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
|
|
670
|
-
}
|
|
671
|
-
//#endregion
|
|
672
658
|
//#region src/session-decode/native-tile-geometry.ts
|
|
673
659
|
/**
|
|
674
660
|
* Pure geometry for the **native subject tile** — the compressed, subject-sized
|
|
@@ -920,6 +906,127 @@ var NativeTileStore = class {
|
|
|
920
906
|
this.bytes -= entry.bytes;
|
|
921
907
|
}
|
|
922
908
|
};
|
|
909
|
+
/**
|
|
910
|
+
* The set of tile cuts currently between "I have this frame's pixels" and
|
|
911
|
+
* "the stores can answer for it", plus the latency of the ones that finished.
|
|
912
|
+
*/
|
|
913
|
+
var PendingCuts = class {
|
|
914
|
+
waitMs;
|
|
915
|
+
now;
|
|
916
|
+
latencySampleCap;
|
|
917
|
+
cuts = /* @__PURE__ */ new Map();
|
|
918
|
+
/** Insertion-ordered ring of completed cut latencies (ms). */
|
|
919
|
+
latencies = [];
|
|
920
|
+
constructor(options) {
|
|
921
|
+
this.waitMs = Math.max(0, options.waitMs);
|
|
922
|
+
this.now = options.now ?? Date.now;
|
|
923
|
+
this.latencySampleCap = Math.max(1, options.latencySamples ?? 256);
|
|
924
|
+
}
|
|
925
|
+
/** Cuts currently in flight (tests / metrics). */
|
|
926
|
+
get inFlight() {
|
|
927
|
+
return this.cuts.size;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* A cut for `frameId` has started. Call BEFORE the first `await` in the cut,
|
|
931
|
+
* so no request can observe the gap this module exists to cover.
|
|
932
|
+
*
|
|
933
|
+
* A duplicate id should never occur (frame ids are monotonic); settling the
|
|
934
|
+
* prior entry rather than replacing it keeps any waiter on it from hanging.
|
|
935
|
+
*/
|
|
936
|
+
begin(frameId) {
|
|
937
|
+
this.settle(frameId);
|
|
938
|
+
let finish = () => {};
|
|
939
|
+
const done = new Promise((resolve) => {
|
|
940
|
+
finish = resolve;
|
|
941
|
+
});
|
|
942
|
+
this.cuts.set(frameId, {
|
|
943
|
+
startedAt: this.now(),
|
|
944
|
+
done,
|
|
945
|
+
finish
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The cut for `frameId` is over — the stores now hold whatever it produced.
|
|
950
|
+
* Call from the cut's `finally`, so a throwing encode releases waiters too.
|
|
951
|
+
* Idempotent.
|
|
952
|
+
*/
|
|
953
|
+
settle(frameId) {
|
|
954
|
+
const cut = this.cuts.get(frameId);
|
|
955
|
+
if (!cut) return;
|
|
956
|
+
this.cuts.delete(frameId);
|
|
957
|
+
this.noteLatency(this.now() - cut.startedAt);
|
|
958
|
+
cut.finish();
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Wait for `frameId`'s cut to land, bounded.
|
|
962
|
+
*
|
|
963
|
+
* `none` means there was nothing in flight, and the caller must NOT read that
|
|
964
|
+
* as "wait exhausted" — it is the ordinary answer for a frame whose cut has
|
|
965
|
+
* already finished or never ran.
|
|
966
|
+
*/
|
|
967
|
+
async waitFor(frameId) {
|
|
968
|
+
const cut = this.cuts.get(frameId);
|
|
969
|
+
if (!cut) return "none";
|
|
970
|
+
if (this.waitMs === 0) return "timeout";
|
|
971
|
+
let timer;
|
|
972
|
+
const expired = new Promise((resolve) => {
|
|
973
|
+
timer = setTimeout(() => resolve("timeout"), this.waitMs);
|
|
974
|
+
timer.unref?.();
|
|
975
|
+
});
|
|
976
|
+
try {
|
|
977
|
+
return await Promise.race([cut.done.then(() => "settled"), expired]);
|
|
978
|
+
} finally {
|
|
979
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Release every waiter and forget every in-flight cut — re-dial and teardown.
|
|
984
|
+
* A waiter left behind here would outlive the worker that could settle it.
|
|
985
|
+
*/
|
|
986
|
+
clear() {
|
|
987
|
+
for (const cut of this.cuts.values()) cut.finish();
|
|
988
|
+
this.cuts.clear();
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* The `percentile`-th cut latency in ms over the retained samples, or `null`
|
|
992
|
+
* when nothing has completed yet. `null` rather than 0: a worker that has cut
|
|
993
|
+
* nothing and a worker that cuts instantly must not read the same.
|
|
994
|
+
*/
|
|
995
|
+
latencyMs(percentile) {
|
|
996
|
+
if (this.latencies.length === 0) return null;
|
|
997
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
998
|
+
const clamped = Math.min(100, Math.max(0, percentile));
|
|
999
|
+
const index = Math.min(sorted.length - 1, Math.ceil(clamped / 100 * sorted.length) - 1);
|
|
1000
|
+
return sorted[Math.max(0, index)] ?? null;
|
|
1001
|
+
}
|
|
1002
|
+
noteLatency(ms) {
|
|
1003
|
+
this.latencies.push(Math.max(0, ms));
|
|
1004
|
+
while (this.latencies.length > this.latencySampleCap) this.latencies.shift();
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
/**
|
|
1008
|
+
* Serve a frame's retained tiles, waiting for an in-flight cut ONLY when
|
|
1009
|
+
* waiting could change the answer.
|
|
1010
|
+
*
|
|
1011
|
+
* The order is the whole point. Anything already retained is served at once —
|
|
1012
|
+
* a cut in flight for a LATER subject of the same frame must not delay a
|
|
1013
|
+
* request the stores can already answer. Only an empty store consults
|
|
1014
|
+
* {@link PendingCuts}, and only then is a success attributable to the wait.
|
|
1015
|
+
*/
|
|
1016
|
+
async function attemptTileServe(pending, frameId, source) {
|
|
1017
|
+
if (source.hasRetained()) return {
|
|
1018
|
+
served: await source.serve(),
|
|
1019
|
+
wait: "none",
|
|
1020
|
+
resolvedByWait: false
|
|
1021
|
+
};
|
|
1022
|
+
const wait = await pending.waitFor(frameId);
|
|
1023
|
+
const served = await source.serve();
|
|
1024
|
+
return {
|
|
1025
|
+
served,
|
|
1026
|
+
wait,
|
|
1027
|
+
resolvedByWait: served !== null && wait === "settled"
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
923
1030
|
//#endregion
|
|
924
1031
|
//#region src/session-decode/planar-crop-source.ts
|
|
925
1032
|
/** Layout a detection `toBuffer` can read, or `null` when it must name the
|
|
@@ -949,6 +1056,19 @@ function nativeCropSourceTag(format) {
|
|
|
949
1056
|
case "rgb24": return "rgb:";
|
|
950
1057
|
}
|
|
951
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Bytes a crop of `rows` rows actually reads, measured from the crop's ORIGIN.
|
|
1061
|
+
*
|
|
1062
|
+
* The LAST row needs only its own `rowBytes`, never a whole stride — the stride
|
|
1063
|
+
* is the distance to the NEXT row, and for the last row there is no next row.
|
|
1064
|
+
* Demanding `stride * rows` instead overshoots the plane's end by exactly the
|
|
1065
|
+
* horizontal offset, which is invisible everywhere except where the crop's
|
|
1066
|
+
* bottom IS the frame's bottom. See {@link resolvePlanarCropSource}.
|
|
1067
|
+
*/
|
|
1068
|
+
function planarSpan(rows, stride, rowBytes) {
|
|
1069
|
+
if (rows <= 0) return 0;
|
|
1070
|
+
return (rows - 1) * stride + rowBytes;
|
|
1071
|
+
}
|
|
952
1072
|
/** A plane view starting at `offset`, or `null` when the plane is too short. */
|
|
953
1073
|
function viewAt(plane, offset, requiredSpan) {
|
|
954
1074
|
if (!plane) return null;
|
|
@@ -967,6 +1087,28 @@ function viewAt(plane, offset, requiredSpan) {
|
|
|
967
1087
|
* An odd origin is floored onto the chroma grid rather than rejected: the
|
|
968
1088
|
* geometry helper already aligns to even, and flooring reads the chroma sample
|
|
969
1089
|
* that CONTAINS the origin instead of inventing a half sample.
|
|
1090
|
+
*
|
|
1091
|
+
* ## The bottom edge (2026-09-07)
|
|
1092
|
+
*
|
|
1093
|
+
* The span demanded from each plane used to be `stride * region.height` from
|
|
1094
|
+
* the crop ORIGIN. A stride is the distance to the NEXT row, and the last row
|
|
1095
|
+
* of a crop has no next row — it needs only `left + width` bytes. So the demand
|
|
1096
|
+
* overshot the plane's end by exactly `left`, which is invisible everywhere
|
|
1097
|
+
* except when the crop's bottom IS the frame's last row. `resolveNativeTileRegion`
|
|
1098
|
+
* clamps a padded box to precisely that, and `clampEven` floors both `top` and
|
|
1099
|
+
* `height`, so a bottom-clamped tile lands flush every time.
|
|
1100
|
+
*
|
|
1101
|
+
* The result was that every subject standing near the camera — the biggest, the
|
|
1102
|
+
* closest, the best shot — lost its native tile, and the hold path threw for
|
|
1103
|
+
* the same rectangles. Measured over 6 h on the live cluster: **100 of 100**
|
|
1104
|
+
* logged `missing plane data` geometries had `top + height === frameHeight`
|
|
1105
|
+
* with `left > 0`, 101 of 102 on dev:590 (salone, 3840x2160).
|
|
1106
|
+
*
|
|
1107
|
+
* The check is now the region's real extent, so a crop that genuinely runs past
|
|
1108
|
+
* a plane — one row too tall, or a last row reaching past the stride — is still
|
|
1109
|
+
* refused. The views handed back are unchanged: `subarray(offset)` always ran
|
|
1110
|
+
* to the plane's true end, so nothing here ever sized a buffer, and tightening
|
|
1111
|
+
* the predicate cannot shorten one.
|
|
970
1112
|
*/
|
|
971
1113
|
function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
972
1114
|
if (!planes || planes.length < PLANE_COUNT[kind]) return null;
|
|
@@ -977,10 +1119,11 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
|
977
1119
|
const left = Math.max(0, region.left);
|
|
978
1120
|
const chromaRow = Math.floor(top / 2);
|
|
979
1121
|
const chromaHeight = Math.ceil(region.height / 2);
|
|
980
|
-
const
|
|
1122
|
+
const chromaWidth = Math.ceil(Math.max(0, region.width) / 2);
|
|
1123
|
+
const y = viewAt(planes[0], top * yStride + left, planarSpan(region.height, yStride, region.width));
|
|
981
1124
|
if (kind === "nv12") {
|
|
982
1125
|
const uvOffset = chromaRow * cStride + Math.floor(left / 2) * 2;
|
|
983
|
-
const uv = viewAt(planes[1], uvOffset, cStride *
|
|
1126
|
+
const uv = viewAt(planes[1], uvOffset, planarSpan(chromaHeight, cStride, chromaWidth * 2));
|
|
984
1127
|
if (!y || !uv) return null;
|
|
985
1128
|
return {
|
|
986
1129
|
planes: [y, uv],
|
|
@@ -989,8 +1132,8 @@ function resolvePlanarCropSource(kind, planes, strides, region) {
|
|
|
989
1132
|
}
|
|
990
1133
|
const vStride = strides?.[2] ?? cStride;
|
|
991
1134
|
const chromaOffset = chromaRow * cStride + Math.floor(left / 2);
|
|
992
|
-
const u = viewAt(planes[1], chromaOffset, cStride
|
|
993
|
-
const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), vStride
|
|
1135
|
+
const u = viewAt(planes[1], chromaOffset, planarSpan(chromaHeight, cStride, chromaWidth));
|
|
1136
|
+
const v = viewAt(planes[2], chromaRow * vStride + Math.floor(left / 2), planarSpan(chromaHeight, vStride, chromaWidth));
|
|
994
1137
|
if (!y || !u || !v) return null;
|
|
995
1138
|
return {
|
|
996
1139
|
planes: [
|
|
@@ -1330,6 +1473,20 @@ async function cutSubjectTiles(frame, bboxes, ports) {
|
|
|
1330
1473
|
return tiles;
|
|
1331
1474
|
}
|
|
1332
1475
|
//#endregion
|
|
1476
|
+
//#region src/session-decode/to-buffer-miss.ts
|
|
1477
|
+
/** Which rung lost the frame, from what the worker knows at the miss. */
|
|
1478
|
+
function toBufferMissRung(facts) {
|
|
1479
|
+
if (facts.reservedFrameId !== null && facts.frameId > facts.reservedFrameId) return "not-yet-delivered";
|
|
1480
|
+
if (!facts.marked) return "never-leased";
|
|
1481
|
+
if (facts.leaseFrames >= facts.holdFrames) return "hold-full";
|
|
1482
|
+
return "released";
|
|
1483
|
+
}
|
|
1484
|
+
/** The error line the runner sees: the rung first, the numbers after it. */
|
|
1485
|
+
function describeToBufferMiss(facts) {
|
|
1486
|
+
const rung = toBufferMissRung(facts);
|
|
1487
|
+
return `decode-worker-child: toBuffer miss for frameId ${facts.frameId} — ${rung} {reserved=${facts.reservedFrameId ?? "none"} marked=${facts.marked} leaseFrames=${facts.leaseFrames}/${facts.holdFrames} holdOverflow=${facts.holdOverflow}${facts.unmarkedBy !== void 0 ? ` unmarkedBy=${facts.unmarkedBy}` : ""}}`;
|
|
1488
|
+
}
|
|
1489
|
+
//#endregion
|
|
1333
1490
|
//#region src/session-decode/decode-worker-child.ts
|
|
1334
1491
|
/** Wrap a software YUV420P frame as a lease entry (byte size ≈ w×h×1.5). */
|
|
1335
1492
|
function toLeasedNavFrame(frame) {
|
|
@@ -1457,6 +1614,28 @@ var NATIVE_TILE_MAX_PER_FRAME = (() => {
|
|
|
1457
1614
|
/** JPEG quality for a retained tile. High: this is a MODEL INPUT, not a preview. */
|
|
1458
1615
|
var NATIVE_TILE_JPEG_QUALITY = 90;
|
|
1459
1616
|
/**
|
|
1617
|
+
* How long a native-crop request waits for a tile cut that is still ENCODING.
|
|
1618
|
+
*
|
|
1619
|
+
* The cut reads its pixels synchronously at `FrameResult` time but only writes
|
|
1620
|
+
* the stores after N JPEG encodes; a request landing in between reads two empty
|
|
1621
|
+
* stores and is told `no retained tile for this frame` — the same sentence a
|
|
1622
|
+
* frame that was never cut gets. Measured 2026-09-07: misses onset at ~100 ms
|
|
1623
|
+
* (only 0.1% below it) and peak at 500-1000 ms, on cameras that cut a tile for
|
|
1624
|
+
* 99.7% of their inferred frames, so the window — not any lifetime — is what
|
|
1625
|
+
* they are landing in.
|
|
1626
|
+
*
|
|
1627
|
+
* 500 ms deliberately, not more: the confirmation gate bounds each crop attempt
|
|
1628
|
+
* at 300 ms and the capture scheduler drops an enrichment request older than
|
|
1629
|
+
* 3 s, so a miss held too long stops being a fast retryable `null` and becomes
|
|
1630
|
+
* an upstream timeout. Widen it via `CAMSTACK_SESSION_TILE_CUT_WAIT_MS` only
|
|
1631
|
+
* once `cutLatencyMs` on the metrics line says what the encode window really
|
|
1632
|
+
* is; `0` disables the wait and restores the pre-2026-09-07 miss profile.
|
|
1633
|
+
*/
|
|
1634
|
+
var TILE_CUT_WAIT_MS = (() => {
|
|
1635
|
+
const raw = Number(process.env["CAMSTACK_SESSION_TILE_CUT_WAIT_MS"]);
|
|
1636
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 500;
|
|
1637
|
+
})();
|
|
1638
|
+
/**
|
|
1460
1639
|
* Demand window (ms) for the lease-capture {@link LeaseActivityGate}: eager
|
|
1461
1640
|
* per-frame native downloads run only within this window of the last
|
|
1462
1641
|
* native-crop request (or dial start). `0` disables the GATE (legacy always-on
|
|
@@ -1597,6 +1776,14 @@ function errMessage(err) {
|
|
|
1597
1776
|
return String(err);
|
|
1598
1777
|
}
|
|
1599
1778
|
/**
|
|
1779
|
+
* Render a cut-latency percentile for the metrics line. `-` when nothing has
|
|
1780
|
+
* been cut yet: a worker that cut nothing and one that cuts in 0 ms must not
|
|
1781
|
+
* read the same, or the number that sizes {@link TILE_CUT_WAIT_MS} is a guess.
|
|
1782
|
+
*/
|
|
1783
|
+
function formatLatency(ms) {
|
|
1784
|
+
return ms === null ? "-" : String(Math.round(ms));
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1600
1787
|
* RTSP low-latency demuxer options — mirrors
|
|
1601
1788
|
* `addon-decoder-nodeav/src/pull-demuxer-options.ts` (duplicated here rather
|
|
1602
1789
|
* than cross-imported: this file is a standalone forked entry point, not an
|
|
@@ -1801,6 +1988,19 @@ var DecodeWorkerChild = class {
|
|
|
1801
1988
|
tileHits = 0;
|
|
1802
1989
|
tileMisses = 0;
|
|
1803
1990
|
tileCutFailures = 0;
|
|
1991
|
+
/**
|
|
1992
|
+
* Serves that produced pixels ONLY because the request waited for a cut that
|
|
1993
|
+
* was still encoding — i.e. misses before {@link TILE_CUT_WAIT_MS} existed.
|
|
1994
|
+
*
|
|
1995
|
+
* This is the counter the write-gap hypothesis stands or falls on. If it
|
|
1996
|
+
* stays near zero while `tileMisses` does not move, the misses were never a
|
|
1997
|
+
* timing race and the question becomes WHICH frames get cut at all.
|
|
1998
|
+
*/
|
|
1999
|
+
tileMissesResolvedByWait = 0;
|
|
2000
|
+
/** Requests that gave up on an in-flight cut at the bound — dropped work. */
|
|
2001
|
+
tileWaitTimeouts = 0;
|
|
2002
|
+
/** Cuts between "I have the pixels" and "the stores can answer". */
|
|
2003
|
+
pendingCuts = new PendingCuts({ waitMs: TILE_CUT_WAIT_MS });
|
|
1804
2004
|
/** Scene tiles cut / served, on the throughput line. */
|
|
1805
2005
|
scenesCut = 0;
|
|
1806
2006
|
sceneHits = 0;
|
|
@@ -2057,7 +2257,7 @@ var DecodeWorkerChild = class {
|
|
|
2057
2257
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
2058
2258
|
const rssMb = Math.round(process.memoryUsage().rss / 1048576);
|
|
2059
2259
|
const ageS = Math.round((now - this.startedAt) / 1e3);
|
|
2060
|
-
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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2260
|
+
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}, leaseReleased:${this.leaseReleased}, leaseReleaseMisses:${this.leaseReleaseMisses}, leaseReleasedEarly:${this.leaseReleasedEarly}, tilesCut:${this.tilesCut}, tileHits:${this.tileHits}, tileMisses:${this.tileMisses}, tileCutFailures:${this.tileCutFailures}, tileMissesResolvedByWait:${this.tileMissesResolvedByWait}, tileWaitTimeouts:${this.tileWaitTimeouts}, cutLatencyMsP50:${formatLatency(this.pendingCuts.latencyMs(50))}, cutLatencyMsP95:${formatLatency(this.pendingCuts.latencyMs(95))}, cutsInFlight:${this.pendingCuts.inFlight}, tileMb:${Math.round(this.tileStore.totalBytes / 1048576)}, tileFrames:${this.tileStore.frameCount}, scenesCut:${this.scenesCut}, sceneHits:${this.sceneHits}, sceneRefused:${this.sceneRefused}, sceneUnclassified:${this.sceneUnclassified}, sceneEvicted:${this.sceneStore.evictedCount}, sceneMb:${Math.round(this.sceneStore.totalBytes / 1048576)}, sceneFrames:${this.sceneStore.frameCount}, rssMb:${rssMb}, ageS:${ageS}}${final ? " (final)" : ""}\n`);
|
|
2061
2261
|
this.lastMetricsAt = now;
|
|
2062
2262
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
2063
2263
|
if (!this.warnedLarge && rssMb >= WORKER_RSS_WARN_MB) {
|
|
@@ -2209,10 +2409,16 @@ var DecodeWorkerChild = class {
|
|
|
2209
2409
|
*/
|
|
2210
2410
|
async serveTileFallback(requestId, frameId, bbox, maxWidth) {
|
|
2211
2411
|
try {
|
|
2212
|
-
const
|
|
2412
|
+
const attempt = await attemptTileServe(this.pendingCuts, frameId, {
|
|
2413
|
+
hasRetained: () => this.tileStore.get(frameId).length > 0 || this.sceneStore.get(frameId).length > 0,
|
|
2414
|
+
serve: () => this.serveFromTile(frameId, bbox, maxWidth)
|
|
2415
|
+
});
|
|
2416
|
+
const served = attempt.served;
|
|
2417
|
+
if (attempt.wait === "timeout") this.tileWaitTimeouts += 1;
|
|
2213
2418
|
if (served) {
|
|
2214
2419
|
this.tileHits += 1;
|
|
2215
2420
|
this.nativeCropHits++;
|
|
2421
|
+
if (attempt.resolvedByWait) this.tileMissesResolvedByWait += 1;
|
|
2216
2422
|
this.send({
|
|
2217
2423
|
kind: "nativeCropResult",
|
|
2218
2424
|
requestId,
|
|
@@ -2228,7 +2434,7 @@ var DecodeWorkerChild = class {
|
|
|
2228
2434
|
this.send({
|
|
2229
2435
|
kind: "nativeCropMiss",
|
|
2230
2436
|
requestId,
|
|
2231
|
-
reason: this.tileMissReason(frameId)
|
|
2437
|
+
reason: this.tileMissReason(frameId, attempt.wait)
|
|
2232
2438
|
});
|
|
2233
2439
|
} catch (err) {
|
|
2234
2440
|
this.tileMisses += 1;
|
|
@@ -2248,7 +2454,8 @@ var DecodeWorkerChild = class {
|
|
|
2248
2454
|
* "the rectangle is outside every container" are three different sentences,
|
|
2249
2455
|
* and one "not retained" hid all of them.
|
|
2250
2456
|
*/
|
|
2251
|
-
tileMissReason(frameId) {
|
|
2457
|
+
tileMissReason(frameId, wait) {
|
|
2458
|
+
if (wait === "timeout") return `frame ${frameId} released and its tile cut did not finish within ${TILE_CUT_WAIT_MS} ms — the request outran the encode, not the retention`;
|
|
2252
2459
|
if (!this.tileStore.enabled && !this.sceneStore.enabled) return `frame ${frameId} released and retention is DISABLED (tileBudgetMb 0, sceneBudgetMb 0)`;
|
|
2253
2460
|
if (!this.sceneStore.enabled) return `frame ${frameId} released and no retained subject tile contains the requested rectangle; scene tiles are DISABLED (sceneBudgetMb 0), so a full-frame request cannot be served at all`;
|
|
2254
2461
|
if (!this.tileStore.enabled) return `frame ${frameId} released and subject tiles are DISABLED (tileBudgetMb 0)`;
|
|
@@ -2280,6 +2487,7 @@ var DecodeWorkerChild = class {
|
|
|
2280
2487
|
* explanation, which is the failure this whole redesign exists to remove.
|
|
2281
2488
|
*/
|
|
2282
2489
|
async handleCutTiles(frameId, bboxes) {
|
|
2490
|
+
this.pendingCuts.begin(frameId);
|
|
2283
2491
|
try {
|
|
2284
2492
|
if (!this.tileStore.enabled && !this.sceneStore.enabled || bboxes.length === 0) return;
|
|
2285
2493
|
const wanted = this.tileStore.enabled ? bboxes.slice(0, NATIVE_TILE_MAX_PER_FRAME) : [];
|
|
@@ -2361,6 +2569,7 @@ var DecodeWorkerChild = class {
|
|
|
2361
2569
|
this.sceneStore.put(frameId, [scene]);
|
|
2362
2570
|
}
|
|
2363
2571
|
} finally {
|
|
2572
|
+
this.pendingCuts.settle(frameId);
|
|
2364
2573
|
this.noteLeaseRelease(frameId, this.leaseStore.release(frameId), "cut");
|
|
2365
2574
|
}
|
|
2366
2575
|
}
|
|
@@ -3143,6 +3352,7 @@ var DecodeWorkerChild = class {
|
|
|
3143
3352
|
this.leaseStore.clear();
|
|
3144
3353
|
this.tileStore.clear();
|
|
3145
3354
|
this.sceneStore.clear();
|
|
3355
|
+
this.pendingCuts.clear();
|
|
3146
3356
|
this.tileFrameWidth = 0;
|
|
3147
3357
|
this.tileFrameHeight = 0;
|
|
3148
3358
|
this.nativeLeaseDownloadFilter?.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.202",
|
|
4
4
|
"description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|