@camstack/addon-benchmark 1.2.150 → 1.2.152

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.
Files changed (45) hide show
  1. package/dist/MaskShapeCanvas-IMozTYd-.mjs +13806 -0
  2. package/dist/MotionZonesSettings-BvoWvBNY.mjs +310 -0
  3. package/dist/{PrivacyMaskSettings-Bw2433zS.mjs → PrivacyMaskSettings-DK35n--f.mjs} +9 -9
  4. package/dist/{SceneMonitorEditor-CEq7UYYx.mjs → SceneMonitorEditor-Dc16r-bx.mjs} +269 -269
  5. package/dist/_stub.js +13287 -25722
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-BMIcawh_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_benchmark_page-CWaZc0Ze.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C5KPMARW.mjs +26 -0
  8. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-wTD-CVC7.mjs +25 -0
  9. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-BxuRwc3x.mjs +25 -0
  10. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-CQ0VjvSn.mjs +26 -0
  11. package/dist/{_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react__loadShare__.js-GeuxDuzQ.mjs → _virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react__loadShare__.js-DQXGYpip.mjs} +1 -1
  12. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Dh0XkFx5.mjs +26 -0
  13. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom__loadShare__.js-NtWzzhEn.mjs +25 -0
  14. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-CFJOcZEH.mjs +25 -0
  15. package/dist/addon-benchmark.css +1 -1
  16. package/dist/{hostInit-BG79Vuom.mjs → hostInit-CiOgOTsz.mjs} +2 -2
  17. package/dist/index.js +1474 -1860
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +1477 -1842
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/player-overlays-D3oW9H_t.mjs +44 -0
  22. package/dist/remoteEntry.js +1 -1
  23. package/dist/{responsive-DU_Gek6C.mjs → responsive-DToJFlYA.mjs} +10 -14
  24. package/dist/{scene-monitor-copy-DwF-5VWK.mjs → scene-monitor-copy-CF1FcnV6.mjs} +26 -29
  25. package/dist/{square-e4QuB7SC.mjs → square-C28R3FF9.mjs} +1 -1
  26. package/dist/{trash-2-CYiIi9m7.mjs → trash-2-c3gg7o5f.mjs} +1 -1
  27. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-C4HwP7ml.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_benchmark_page__remoteEntry_js-Dr5M2GBW.mjs} +390 -399
  28. package/package.json +4 -5
  29. package/assets/fixtures/camera-sim-1080p.h264 +0 -0
  30. package/assets/fixtures/camera-sim-1440p-h265.hevc +0 -0
  31. package/assets/fixtures/camera-sim-360p.h264 +0 -0
  32. package/dist/MaskShapeCanvas-BDvrLf-K.mjs +0 -686
  33. package/dist/MotionZonesSettings-Ca7gw3LT.mjs +0 -292
  34. package/dist/ReactKonva-DXzDAU1q.mjs +0 -5416
  35. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DDbAl4wY.mjs +0 -26
  36. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-lv97RBhq.mjs +0 -89
  37. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_client__loadShare__.js-DtWmo0vJ.mjs +0 -58
  38. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare___mf_0_trpc_mf_1_react_mf_2_query__loadShare__.js-Bh7FS7ML.mjs +0 -62
  39. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-SOU_VnTj.mjs +0 -34
  40. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom__loadShare__.js-DQaf3kYb.mjs +0 -45
  41. package/dist/_virtual_mf___mfe_internal__addon_benchmark_page__loadShare__react_mf_2_dom_mf_1_client__loadShare__.js-DUzuN6aS.mjs +0 -34
  42. package/dist/grid-2x2-BJ3lFTm4.mjs +0 -21
  43. package/dist/hls-R1zGmDT3.mjs +0 -18476
  44. package/dist/lib-CVGFTwe0.mjs +0 -7770
  45. package/dist/player-overlays-BrlDlS6o.mjs +0 -69
package/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
@@ -35,14 +25,7 @@ __export(src_exports, {
35
25
  });
36
26
  module.exports = __toCommonJS(src_exports);
37
27
 
38
- // ../../node_modules/tsup/assets/cjs_shims.js
39
- var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
40
- var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
41
-
42
28
  // src/benchmark-addon.ts
43
- var fs2 = __toESM(require("fs"));
44
- var import_node_url = require("url");
45
- var path2 = __toESM(require("path"));
46
29
  var import_types4 = require("@camstack/types");
47
30
 
48
31
  // ../../node_modules/zod/v4/classic/external.js
@@ -811,10 +794,10 @@ function mergeDefs(...defs) {
811
794
  function cloneDef(schema) {
812
795
  return mergeDefs(schema._zod.def);
813
796
  }
814
- function getElementAtPath(obj, path3) {
815
- if (!path3)
797
+ function getElementAtPath(obj, path) {
798
+ if (!path)
816
799
  return obj;
817
- return path3.reduce((acc, key) => acc?.[key], obj);
800
+ return path.reduce((acc, key) => acc?.[key], obj);
818
801
  }
819
802
  function promiseAllObject(promisesObj) {
820
803
  const keys = Object.keys(promisesObj);
@@ -1223,11 +1206,11 @@ function explicitlyAborted(x, startIndex = 0) {
1223
1206
  }
1224
1207
  return false;
1225
1208
  }
1226
- function prefixIssues(path3, issues) {
1209
+ function prefixIssues(path, issues) {
1227
1210
  return issues.map((iss) => {
1228
1211
  var _a3;
1229
1212
  (_a3 = iss).path ?? (_a3.path = []);
1230
- iss.path.unshift(path3);
1213
+ iss.path.unshift(path);
1231
1214
  return iss;
1232
1215
  });
1233
1216
  }
@@ -1374,16 +1357,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
1374
1357
  }
1375
1358
  function formatError(error51, mapper = (issue2) => issue2.message) {
1376
1359
  const fieldErrors = { _errors: [] };
1377
- const processError = (error52, path3 = []) => {
1360
+ const processError = (error52, path = []) => {
1378
1361
  for (const issue2 of error52.issues) {
1379
1362
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1380
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
1363
+ issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1381
1364
  } else if (issue2.code === "invalid_key") {
1382
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1365
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1383
1366
  } else if (issue2.code === "invalid_element") {
1384
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1367
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1385
1368
  } else {
1386
- const fullpath = [...path3, ...issue2.path];
1369
+ const fullpath = [...path, ...issue2.path];
1387
1370
  if (fullpath.length === 0) {
1388
1371
  fieldErrors._errors.push(mapper(issue2));
1389
1372
  } else {
@@ -1410,17 +1393,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
1410
1393
  }
1411
1394
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
1412
1395
  const result = { errors: [] };
1413
- const processError = (error52, path3 = []) => {
1396
+ const processError = (error52, path = []) => {
1414
1397
  var _a3, _b;
1415
1398
  for (const issue2 of error52.issues) {
1416
1399
  if (issue2.code === "invalid_union" && issue2.errors.length) {
1417
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
1400
+ issue2.errors.map((issues) => processError({ issues }, [...path, ...issue2.path]));
1418
1401
  } else if (issue2.code === "invalid_key") {
1419
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1402
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1420
1403
  } else if (issue2.code === "invalid_element") {
1421
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
1404
+ processError({ issues: issue2.issues }, [...path, ...issue2.path]);
1422
1405
  } else {
1423
- const fullpath = [...path3, ...issue2.path];
1406
+ const fullpath = [...path, ...issue2.path];
1424
1407
  if (fullpath.length === 0) {
1425
1408
  result.errors.push(mapper(issue2));
1426
1409
  continue;
@@ -1452,8 +1435,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
1452
1435
  }
1453
1436
  function toDotPath(_path) {
1454
1437
  const segs = [];
1455
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1456
- for (const seg of path3) {
1438
+ const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1439
+ for (const seg of path) {
1457
1440
  if (typeof seg === "number")
1458
1441
  segs.push(`[${seg}]`);
1459
1442
  else if (typeof seg === "symbol")
@@ -11535,42 +11518,42 @@ function extractDefs(ctx, schema) {
11535
11518
  if (!root)
11536
11519
  throw new Error("Unprocessed schema. This is a bug in Zod.");
11537
11520
  const idToSchema = /* @__PURE__ */ new Map();
11538
- for (const entry of ctx.seen.entries()) {
11539
- const id = ctx.metadataRegistry.get(entry[0])?.id;
11521
+ for (const entry2 of ctx.seen.entries()) {
11522
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
11540
11523
  if (id) {
11541
11524
  const existing = idToSchema.get(id);
11542
- if (existing && existing !== entry[0]) {
11525
+ if (existing && existing !== entry2[0]) {
11543
11526
  throw new Error(`Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);
11544
11527
  }
11545
- idToSchema.set(id, entry[0]);
11528
+ idToSchema.set(id, entry2[0]);
11546
11529
  }
11547
11530
  }
11548
- const makeURI = (entry) => {
11531
+ const makeURI = (entry2) => {
11549
11532
  const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
11550
11533
  if (ctx.external) {
11551
- const externalId = ctx.external.registry.get(entry[0])?.id;
11534
+ const externalId = ctx.external.registry.get(entry2[0])?.id;
11552
11535
  const uriGenerator = ctx.external.uri ?? ((id2) => id2);
11553
11536
  if (externalId) {
11554
11537
  return { ref: uriGenerator(externalId) };
11555
11538
  }
11556
- const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
11557
- entry[1].defId = id;
11539
+ const id = entry2[1].defId ?? entry2[1].schema.id ?? `schema${ctx.counter++}`;
11540
+ entry2[1].defId = id;
11558
11541
  return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
11559
11542
  }
11560
- if (entry[1] === root) {
11543
+ if (entry2[1] === root) {
11561
11544
  return { ref: "#" };
11562
11545
  }
11563
11546
  const uriPrefix = `#`;
11564
11547
  const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
11565
- const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
11548
+ const defId = entry2[1].schema.id ?? `__schema${ctx.counter++}`;
11566
11549
  return { defId, ref: defUriPrefix + defId };
11567
11550
  };
11568
- const extractToDef = (entry) => {
11569
- if (entry[1].schema.$ref) {
11551
+ const extractToDef = (entry2) => {
11552
+ if (entry2[1].schema.$ref) {
11570
11553
  return;
11571
11554
  }
11572
- const seen = entry[1];
11573
- const { ref, defId } = makeURI(entry);
11555
+ const seen = entry2[1];
11556
+ const { ref, defId } = makeURI(entry2);
11574
11557
  seen.def = { ...seen.schema };
11575
11558
  if (defId)
11576
11559
  seen.defId = defId;
@@ -11581,8 +11564,8 @@ function extractDefs(ctx, schema) {
11581
11564
  schema2.$ref = ref;
11582
11565
  };
11583
11566
  if (ctx.cycles === "throw") {
11584
- for (const entry of ctx.seen.entries()) {
11585
- const seen = entry[1];
11567
+ for (const entry2 of ctx.seen.entries()) {
11568
+ const seen = entry2[1];
11586
11569
  if (seen.cycle) {
11587
11570
  throw new Error(`Cycle detected: #/${seen.cycle?.join("/")}/<root>
11588
11571
 
@@ -11590,31 +11573,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
11590
11573
  }
11591
11574
  }
11592
11575
  }
11593
- for (const entry of ctx.seen.entries()) {
11594
- const seen = entry[1];
11595
- if (schema === entry[0]) {
11596
- extractToDef(entry);
11576
+ for (const entry2 of ctx.seen.entries()) {
11577
+ const seen = entry2[1];
11578
+ if (schema === entry2[0]) {
11579
+ extractToDef(entry2);
11597
11580
  continue;
11598
11581
  }
11599
11582
  if (ctx.external) {
11600
- const ext = ctx.external.registry.get(entry[0])?.id;
11601
- if (schema !== entry[0] && ext) {
11602
- extractToDef(entry);
11583
+ const ext = ctx.external.registry.get(entry2[0])?.id;
11584
+ if (schema !== entry2[0] && ext) {
11585
+ extractToDef(entry2);
11603
11586
  continue;
11604
11587
  }
11605
11588
  }
11606
- const id = ctx.metadataRegistry.get(entry[0])?.id;
11589
+ const id = ctx.metadataRegistry.get(entry2[0])?.id;
11607
11590
  if (id) {
11608
- extractToDef(entry);
11591
+ extractToDef(entry2);
11609
11592
  continue;
11610
11593
  }
11611
11594
  if (seen.cycle) {
11612
- extractToDef(entry);
11595
+ extractToDef(entry2);
11613
11596
  continue;
11614
11597
  }
11615
11598
  if (seen.count > 1) {
11616
11599
  if (ctx.reused === "ref") {
11617
- extractToDef(entry);
11600
+ extractToDef(entry2);
11618
11601
  continue;
11619
11602
  }
11620
11603
  }
@@ -11686,8 +11669,8 @@ function finalize(ctx, schema) {
11686
11669
  path: seen.path ?? []
11687
11670
  });
11688
11671
  };
11689
- for (const entry of [...ctx.seen.entries()].reverse()) {
11690
- flattenRef(entry[0]);
11672
+ for (const entry2 of [...ctx.seen.entries()].reverse()) {
11673
+ flattenRef(entry2[0]);
11691
11674
  }
11692
11675
  const result = {};
11693
11676
  if (ctx.target === "draft-2020-12") {
@@ -11710,8 +11693,8 @@ function finalize(ctx, schema) {
11710
11693
  if (rootMetaId !== void 0 && result.id === rootMetaId)
11711
11694
  delete result.id;
11712
11695
  const defs = ctx.external?.defs ?? {};
11713
- for (const entry of ctx.seen.entries()) {
11714
- const seen = entry[1];
11696
+ for (const entry2 of ctx.seen.entries()) {
11697
+ const seen = entry2[1];
11715
11698
  if (seen.def && seen.defId) {
11716
11699
  if (seen.def.id === seen.defId)
11717
11700
  delete seen.def.id;
@@ -12327,8 +12310,8 @@ function toJSONSchema(input, params) {
12327
12310
  const registry2 = input;
12328
12311
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
12329
12312
  const defs = {};
12330
- for (const entry of registry2._idmap.entries()) {
12331
- const [_, schema] = entry;
12313
+ for (const entry2 of registry2._idmap.entries()) {
12314
+ const [_, schema] = entry2;
12332
12315
  process2(schema, ctx2);
12333
12316
  }
12334
12317
  const schemas = {};
@@ -12338,8 +12321,8 @@ function toJSONSchema(input, params) {
12338
12321
  defs
12339
12322
  };
12340
12323
  ctx2.external = external;
12341
- for (const entry of registry2._idmap.entries()) {
12342
- const [key, schema] = entry;
12324
+ for (const entry2 of registry2._idmap.entries()) {
12325
+ const [key, schema] = entry2;
12343
12326
  extractDefs(ctx2, schema);
12344
12327
  schemas[key] = finalize(ctx2, schema);
12345
12328
  }
@@ -14145,13 +14128,13 @@ function resolveRef(ref, ctx) {
14145
14128
  if (!ref.startsWith("#")) {
14146
14129
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14147
14130
  }
14148
- const path3 = ref.slice(1).split("/").filter(Boolean);
14149
- if (path3.length === 0) {
14131
+ const path = ref.slice(1).split("/").filter(Boolean);
14132
+ if (path.length === 0) {
14150
14133
  return ctx.rootSchema;
14151
14134
  }
14152
14135
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14153
- if (path3[0] === defsKey) {
14154
- const key = path3[1];
14136
+ if (path[0] === defsKey) {
14137
+ const key = path[1];
14155
14138
  if (!key || !ctx.defs[key]) {
14156
14139
  throw new Error(`Reference not found: ${ref}`);
14157
14140
  }
@@ -14560,17 +14543,162 @@ function date4(params) {
14560
14543
  config(en_default());
14561
14544
 
14562
14545
  // src/benchmark-actions.ts
14563
- var import_types2 = require("@camstack/types");
14546
+ var import_types3 = require("@camstack/types");
14564
14547
 
14565
14548
  // src/benchmark-schemas.ts
14549
+ var import_types2 = require("@camstack/types");
14550
+
14551
+ // src/session-replay/schemas.ts
14566
14552
  var import_types = require("@camstack/types");
14567
- var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14568
- kind: external_exports.literal("pipeline").default("pipeline"),
14569
- id: external_exports.string(),
14570
- date: external_exports.string(),
14553
+ var NormBoxSchema = external_exports.object({
14554
+ x: external_exports.number(),
14555
+ y: external_exports.number(),
14556
+ w: external_exports.number(),
14557
+ h: external_exports.number()
14558
+ });
14559
+ var ReplayBoxSchema = external_exports.object({
14560
+ className: external_exports.string(),
14561
+ score: external_exports.number(),
14562
+ box: NormBoxSchema
14563
+ });
14564
+ var StoredPositionSchema = external_exports.object({
14565
+ trackId: external_exports.string(),
14566
+ className: external_exports.string(),
14567
+ t: external_exports.number(),
14568
+ box: NormBoxSchema,
14569
+ score: external_exports.number().nullable()
14570
+ });
14571
+ var ReferenceBasisSchema = external_exports.enum(["exact", "interpolated", "uncertain"]);
14572
+ var ReferenceEntrySchema = StoredPositionSchema.extend({ basis: ReferenceBasisSchema });
14573
+ var TimelineReplayBoxSchema = ReplayBoxSchema.extend({
14574
+ /** `unreferenced`: no stored track was alive at this instant — nothing to contradict. */
14575
+ outcome: external_exports.enum(["matched", "class-changed", "added", "unreferenced"])
14576
+ });
14577
+ var TimelineStoredBoxSchema = ReferenceEntrySchema.extend({
14578
+ outcome: external_exports.enum(["matched", "class-changed", "missed"])
14579
+ });
14580
+ var TimelineFrameSchema = external_exports.object({
14581
+ t: external_exports.number(),
14582
+ /** Some stored track was alive at `t` — the instant HAS a reference. */
14583
+ referenced: external_exports.boolean(),
14584
+ replay: external_exports.array(TimelineReplayBoxSchema).readonly(),
14585
+ stored: external_exports.array(TimelineStoredBoxSchema).readonly(),
14586
+ inferenceMs: external_exports.number().nullable()
14587
+ });
14588
+ var ClassCountsSchema = external_exports.object({
14589
+ replay: external_exports.number(),
14590
+ stored: external_exports.number(),
14591
+ matched: external_exports.number(),
14592
+ added: external_exports.number(),
14593
+ missed: external_exports.number(),
14594
+ unreferenced: external_exports.number(),
14595
+ /** Comparisons (matched / class-changed / missed) made on a non-exact stored box. */
14596
+ interpolated: external_exports.number()
14597
+ });
14598
+ var ReplaySummarySchema = external_exports.object({
14599
+ totals: external_exports.object({
14600
+ matched: external_exports.number(),
14601
+ classChanged: external_exports.number(),
14602
+ added: external_exports.number(),
14603
+ missed: external_exports.number(),
14604
+ unreferenced: external_exports.number(),
14605
+ interpolated: external_exports.number()
14606
+ }),
14607
+ perClass: external_exports.record(external_exports.string(), ClassCountsSchema),
14608
+ scoreDelta: external_exports.object({ mean: external_exports.number(), p10: external_exports.number(), p50: external_exports.number(), p90: external_exports.number() }).nullable(),
14609
+ inferenceMs: external_exports.object({ p50: external_exports.number(), p95: external_exports.number(), count: external_exports.number() }).nullable()
14610
+ });
14611
+ var CadenceSourceSchema = external_exports.enum(["operator", "stored-positions", "default"]);
14612
+ var ReplayCadenceSchema = external_exports.object({ fps: external_exports.number(), source: CadenceSourceSchema });
14613
+ var SessionReplayReportSchema = external_exports.object({
14614
+ deviceId: external_exports.number(),
14615
+ motionEventId: external_exports.number(),
14616
+ window: external_exports.object({
14617
+ startMs: external_exports.number(),
14618
+ endMs: external_exports.number(),
14619
+ motionStartMs: external_exports.number(),
14620
+ motionEndMs: external_exports.number(),
14621
+ /** Where the pre-roll / tail came from: the camera's own settings, or the fleet default. */
14622
+ prerollSource: external_exports.enum(["camera", "default"]),
14623
+ tailSource: external_exports.enum(["camera", "default"])
14624
+ }),
14625
+ pin: external_exports.object({
14626
+ nodeId: external_exports.string(),
14627
+ deviceKey: external_exports.string().nullable(),
14628
+ modelId: external_exports.string(),
14629
+ cameraModelId: external_exports.string().nullable()
14630
+ }),
14631
+ rootStep: import_types.PipelineStepInputSchema,
14632
+ cadence: ReplayCadenceSchema,
14633
+ frameSize: external_exports.object({ width: external_exports.number(), source: external_exports.enum(["stored-tracks", "default"]) }),
14634
+ frames: external_exports.object({
14635
+ planned: external_exports.number(),
14636
+ decoded: external_exports.number(),
14637
+ ran: external_exports.number(),
14638
+ failed: external_exports.number(),
14639
+ withoutFootage: external_exports.number(),
14640
+ notDecoded: external_exports.number(),
14641
+ truncated: external_exports.boolean(),
14642
+ truncatedBySafetyCap: external_exports.boolean(),
14643
+ aborted: external_exports.boolean()
14644
+ }),
14645
+ reference: external_exports.object({
14646
+ tracks: external_exports.number(),
14647
+ positionsCompared: external_exports.number(),
14648
+ recoveryExcluded: external_exports.number(),
14649
+ withoutGeometry: external_exports.number(),
14650
+ models: external_exports.record(external_exports.string(), external_exports.number()),
14651
+ /** The stored-track read: how many came back, and whether its limit cut it short. */
14652
+ read: external_exports.object({ tracks: external_exports.number(), truncated: external_exports.boolean() })
14653
+ }),
14654
+ confirmation: external_exports.object({
14655
+ liveVerdicts: external_exports.record(external_exports.string(), external_exports.number()),
14656
+ /** Added or unreferenced detections near a live SUPPRESSED birth of their class. */
14657
+ nearSuppressedBirth: external_exports.number()
14658
+ }),
14659
+ summary: ReplaySummarySchema,
14660
+ timeline: external_exports.array(TimelineFrameSchema).readonly(),
14661
+ failures: external_exports.array(external_exports.object({ t: external_exports.number(), error: external_exports.string() })).readonly()
14662
+ });
14663
+ var SessionReplayRefusalSchema = external_exports.enum([
14664
+ "session-not-found",
14665
+ "session-open",
14666
+ "window-too-long",
14667
+ "no-root-step",
14668
+ "model-unpinned",
14669
+ "model-not-servable",
14670
+ "device-not-servable",
14671
+ "no-recording",
14672
+ "no-footage",
14673
+ "session-length-unknown",
14674
+ "pool-failing"
14675
+ ]);
14676
+ var SessionReplayOutcomeSchema = external_exports.discriminatedUnion("kind", [
14677
+ external_exports.object({
14678
+ kind: external_exports.literal("refused"),
14679
+ reason: SessionReplayRefusalSchema,
14680
+ detail: external_exports.string()
14681
+ }),
14682
+ external_exports.object({ kind: external_exports.literal("ran"), report: SessionReplayReportSchema })
14683
+ ]);
14684
+ var SessionReplayProgressSchema = external_exports.object({
14685
+ phase: external_exports.enum(["preparing", "replaying", "done"]),
14686
+ framesPlanned: external_exports.number(),
14687
+ framesDone: external_exports.number()
14688
+ });
14689
+
14690
+ // src/benchmark-schemas.ts
14691
+ var BenchModeSchema = external_exports.enum(["inference", "full-tree", "live-frame"]);
14692
+ var BenchPoolTuningSchema = external_exports.object({
14693
+ runtime: external_exports.string().nullable(),
14694
+ numWorkers: external_exports.number().nullable(),
14695
+ concurrency: external_exports.number().nullable(),
14696
+ batchMode: external_exports.string().nullable(),
14697
+ maxBatchSize: external_exports.number().nullable()
14698
+ });
14699
+ var HistoryMeasurementFields = {
14571
14700
  stepId: external_exports.string(),
14572
14701
  modelId: external_exports.string(),
14573
- runtime: external_exports.string(),
14574
14702
  iterations: external_exports.number(),
14575
14703
  warmup: external_exports.number(),
14576
14704
  fps: external_exports.number(),
@@ -14579,723 +14707,213 @@ var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14579
14707
  p95Ms: external_exports.number(),
14580
14708
  p99Ms: external_exports.number(),
14581
14709
  /**
14582
- * OPTIONAL, and absent on every entry the page writes.
14583
- * `pipeline-runner.runSyntheticBench` reports mean / p50 / p95 / p99 and
14584
- * measures no per-call min or max. They were required here while nothing
14585
- * in the page ever called `saveBenchmarkResult`, so nothing noticed; the
14586
- * moment it does, a required field can only be satisfied by inventing a
14587
- * number, and a fabricated latency in the history table is worse than a
14588
- * blank one.
14710
+ * OPTIONAL, and absent on every entry the page writes: the runner reports
14711
+ * mean / p50 / p95 / p99 and measures no per-call min or max. A fabricated
14712
+ * latency in the history table is worse than a blank one.
14589
14713
  */
14590
14714
  minMs: external_exports.number().optional(),
14591
14715
  maxMs: external_exports.number().optional(),
14592
14716
  avgDetections: external_exports.number(),
14593
14717
  referenceImage: external_exports.string().optional(),
14594
- /** Number of concurrent runs aggregated into this entry; 1 = single run. */
14718
+ /** Concurrent in-flight requests (simulated cameras) — NOT pool workers. */
14595
14719
  parallelRuns: external_exports.number().min(1).default(1)
14596
- });
14597
- var PipelineBenchmarkHistoryEntryInputSchema = PipelineBenchmarkHistoryEntrySchema.omit({
14598
- kind: true,
14599
- id: true,
14600
- date: true
14601
- });
14602
- var DecoderFixtureCodecSchema = external_exports.enum(["h264", "h265"]);
14603
- var DecoderFixtureSchema = external_exports.object({
14604
- filename: external_exports.string(),
14605
- codec: DecoderFixtureCodecSchema,
14606
- resolutionLabel: external_exports.string(),
14607
- // '360p', '1080p', '1440p'
14608
- sizeBytes: external_exports.number(),
14609
- description: external_exports.string().optional()
14610
- });
14611
- var CamProfileSchema = external_exports.enum(["high", "mid", "low"]);
14612
- var DecoderPerfSourceSchema = external_exports.discriminatedUnion("kind", [
14613
- external_exports.object({
14614
- kind: external_exports.literal("sample"),
14615
- fixtureFilename: external_exports.string()
14616
- }),
14617
- external_exports.object({
14618
- kind: external_exports.literal("stream"),
14619
- deviceId: external_exports.number().int().min(1),
14620
- profile: CamProfileSchema
14621
- })
14622
- ]);
14623
- var DecoderPerfModeSchema = external_exports.discriminatedUnion("kind", [
14624
- external_exports.object({ kind: external_exports.literal("duration"), durationSec: external_exports.number().min(5).max(1800) }),
14625
- external_exports.object({ kind: external_exports.literal("iterations"), iterations: external_exports.number().min(10).max(1e6) })
14626
- ]);
14627
- var DecoderPerfFormatSchema = external_exports.enum(["jpeg", "gray"]);
14628
- var HWACCEL_VALUES = import_types.HWACCEL_OPTIONS.map((o) => o.value);
14629
- var DecoderPerfHwaccelSchema = external_exports.enum(HWACCEL_VALUES);
14630
- var DecoderPerfThresholdsSchema = external_exports.object({
14631
- warnMBMin: external_exports.number().min(0).default(5),
14632
- failMBMin: external_exports.number().min(0).default(20)
14633
- });
14634
- var DecoderPerfInputSchema = external_exports.object({
14635
- sessionId: external_exports.string(),
14636
- cameras: external_exports.number().int().min(1).max(16),
14637
- mode: DecoderPerfModeSchema,
14638
- source: DecoderPerfSourceSchema,
14639
- format: DecoderPerfFormatSchema,
14640
- fps: external_exports.number().min(1).max(120),
14641
- toggleMs: external_exports.number().min(0).default(0),
14642
- warmupSec: external_exports.number().min(0).default(15),
14643
- sampleIntervalMs: external_exports.number().min(500).default(2e3),
14644
- hwaccel: DecoderPerfHwaccelSchema.default("none"),
14645
- thresholds: DecoderPerfThresholdsSchema.default({ warnMBMin: 5, failMBMin: 20 }),
14646
- // Pin the decoder sessions to the selected node. createSession is the only
14647
- // decoder call without a node selector (the rest route via the nodeId that
14648
- // createSession returns), so without this the whole test runs on the
14649
- // singleton's elected node (the hub) regardless of the UI selection.
14650
- nodeId: external_exports.string().optional()
14651
- });
14652
- var DecoderPerfSampleSchema = external_exports.object({
14653
- tSec: external_exports.number(),
14654
- rssMB: external_exports.number(),
14655
- heapMB: external_exports.number(),
14656
- externalMB: external_exports.number(),
14657
- arrayBuffersMB: external_exports.number(),
14658
- cpuPct: external_exports.number(),
14659
- framesTotal: external_exports.number()
14660
- });
14661
- var DecoderPerfVerdictSchema = external_exports.enum(["pass", "warn", "fail", "aborted", "error"]);
14662
- var DecoderPerfResultSchema = external_exports.object({
14663
- sessionId: external_exports.string(),
14664
- verdict: DecoderPerfVerdictSchema,
14665
- aborted: external_exports.boolean().default(false),
14666
- errorMessage: external_exports.string().optional(),
14667
- samples: external_exports.array(DecoderPerfSampleSchema).readonly(),
14668
- slopes: external_exports.object({
14669
- rssMBMin: external_exports.number(),
14670
- heapMBMin: external_exports.number(),
14671
- externalMBMin: external_exports.number()
14672
- }),
14673
- totals: external_exports.object({
14674
- wallSec: external_exports.number(),
14675
- framesEmitted: external_exports.number(),
14676
- fpsPerCamera: external_exports.number(),
14677
- cpuAvgPct: external_exports.number(),
14678
- droppedFrames: external_exports.number()
14679
- }),
14680
- config: DecoderPerfInputSchema
14681
- });
14682
- var DecoderPerfProgressSchema = external_exports.object({
14683
- kind: external_exports.literal("decoder-perf"),
14684
- sessionId: external_exports.string(),
14685
- phase: external_exports.enum(["init", "warmup", "running", "complete", "aborted", "error"]),
14686
- tSec: external_exports.number().optional(),
14687
- sample: DecoderPerfSampleSchema.optional(),
14688
- message: external_exports.string().optional()
14689
- });
14690
- var DecoderPerfHistoryEntrySchema = external_exports.object({
14691
- kind: external_exports.literal("decoder-perf"),
14720
+ };
14721
+ var PipelineBenchmarkHistoryEntrySchema = external_exports.object({
14722
+ kind: external_exports.literal("pipeline").default("pipeline"),
14692
14723
  id: external_exports.string(),
14693
14724
  date: external_exports.string(),
14694
- sessionId: external_exports.string(),
14695
- verdict: DecoderPerfVerdictSchema,
14696
- cameras: external_exports.number(),
14697
- format: DecoderPerfFormatSchema,
14698
- sourceLabel: external_exports.string(),
14699
- // 'sample:camera-sim-360p' | 'stream:dev-2/low'
14700
- modeLabel: external_exports.string(),
14701
- // 'duration:60s' | 'iterations:5000'
14702
- rssSlopeMBMin: external_exports.number(),
14703
- heapSlopeMBMin: external_exports.number(),
14704
- externalSlopeMBMin: external_exports.number(),
14705
- framesEmitted: external_exports.number(),
14706
- fpsPerCamera: external_exports.number(),
14707
- cpuAvgPct: external_exports.number(),
14708
- droppedFrames: external_exports.number(),
14709
- wallSec: external_exports.number(),
14710
- hwaccel: DecoderPerfHwaccelSchema
14725
+ ...HistoryMeasurementFields,
14726
+ nodeId: external_exports.string().optional(),
14727
+ deviceKey: external_exports.string().nullable().optional(),
14728
+ plane: external_exports.enum(["full", "frame"]).nullable().optional(),
14729
+ mode: BenchModeSchema.optional(),
14730
+ pool: BenchPoolTuningSchema.optional(),
14731
+ runtime: external_exports.string().optional()
14711
14732
  });
14712
- var DecoderPerfHistoryEntryInputSchema = DecoderPerfHistoryEntrySchema.omit({
14713
- kind: true,
14714
- id: true,
14715
- date: true
14733
+ var PipelineBenchmarkHistoryEntryInputSchema = external_exports.object({
14734
+ ...HistoryMeasurementFields,
14735
+ nodeId: external_exports.string(),
14736
+ deviceKey: external_exports.string().nullable(),
14737
+ plane: external_exports.enum(["full", "frame"]).nullable(),
14738
+ mode: BenchModeSchema,
14739
+ pool: BenchPoolTuningSchema
14716
14740
  });
14717
- var HistoryEntrySchema = external_exports.discriminatedUnion("kind", [
14718
- PipelineBenchmarkHistoryEntrySchema,
14719
- DecoderPerfHistoryEntrySchema
14720
- ]);
14741
+ var HistoryEntrySchema = PipelineBenchmarkHistoryEntrySchema;
14721
14742
  var PipelineRunInputSchema = external_exports.object({
14722
- steps: external_exports.array(import_types.PipelineStepInputSchema).readonly(),
14743
+ steps: external_exports.array(import_types2.PipelineStepInputSchema).readonly(),
14723
14744
  imageBase64: external_exports.string().optional(),
14724
14745
  referenceImage: external_exports.string().optional(),
14725
- frameId: external_exports.string().optional(),
14726
- /**
14727
- * Target node for the pipeline test. Forwarded to
14728
- * `pipelineExecutor.runPipeline`. Omit to run on the hub.
14729
- */
14746
+ /** Target node. Forwarded to `pipelineExecutor.runPipeline`. Omit to run on the hub. */
14730
14747
  nodeId: external_exports.string().optional(),
14731
- /**
14732
- * UI-provided correlation id. When set, every `pipeline.progress`
14733
- * event emitted by the run is tagged with this value so the caller
14734
- * can filter out concurrent camera-runtime dispatches that share the
14735
- * same bus category.
14736
- */
14748
+ /** UI correlation id stamped on every `pipeline.progress` line of the run. */
14737
14749
  sessionId: external_exports.string().optional(),
14738
- engine: external_exports.object({
14739
- runtime: external_exports.enum(["python", "node"]),
14740
- backend: external_exports.string(),
14741
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14742
- device: external_exports.string().optional()
14743
- }).optional(),
14744
14750
  deviceKey: external_exports.string().optional(),
14745
14751
  plane: external_exports.enum(["full", "frame"]).optional()
14746
14752
  });
14747
- var PipelineRunBatchInputSchema = external_exports.object({
14748
- steps: external_exports.array(import_types.PipelineStepInputSchema).readonly(),
14749
- imageBase64: external_exports.string(),
14750
- batchSize: external_exports.number().int().min(1).max(255),
14751
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14752
- nodeId: external_exports.string().optional(),
14753
- sessionId: external_exports.string().optional(),
14754
- engine: external_exports.object({
14755
- runtime: external_exports.enum(["python", "node"]),
14756
- backend: external_exports.string(),
14757
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14758
- device: external_exports.string().optional()
14759
- }).optional()
14753
+ var SyntheticBenchPoolSchema = external_exports.object({
14754
+ nodeId: external_exports.string(),
14755
+ deviceKey: external_exports.string().nullable(),
14756
+ runtime: external_exports.string().nullable(),
14757
+ numWorkers: external_exports.number().nullable(),
14758
+ concurrency: external_exports.number().nullable(),
14759
+ batchMode: external_exports.string().nullable(),
14760
+ maxBatchSize: external_exports.number().nullable()
14760
14761
  });
14761
- var PipelineRunBatchResultSchema = external_exports.object({
14762
- results: external_exports.array(PipelineRunResultBridgeRef()).readonly(),
14763
- callMs: external_exports.number(),
14764
- inferMs: external_exports.number()
14762
+ var SyntheticBenchStatsSchema = external_exports.object({
14763
+ runs: external_exports.number(),
14764
+ wallSec: external_exports.number(),
14765
+ fps: external_exports.number(),
14766
+ detectionsPerSec: external_exports.number(),
14767
+ avgDetections: external_exports.number(),
14768
+ callMs: external_exports.object({ mean: external_exports.number(), p50: external_exports.number(), p95: external_exports.number(), p99: external_exports.number() }),
14769
+ inferMs: external_exports.number(),
14770
+ preprocessMs: external_exports.number(),
14771
+ predictMs: external_exports.number(),
14772
+ batchSizeMean: external_exports.number(),
14773
+ batchSizeMax: external_exports.number(),
14774
+ pool: SyntheticBenchPoolSchema,
14775
+ path: external_exports.string()
14765
14776
  });
14766
- function PipelineRunResultBridgeRef() {
14767
- return external_exports.unknown();
14768
- }
14769
- var MultiCameraStressInputSchema = external_exports.object({
14770
- steps: external_exports.array(import_types.PipelineStepInputSchema).readonly(),
14771
- imageBase64: external_exports.string(),
14772
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14773
- simulatedCameras: external_exports.number().int().min(1).max(64).optional(),
14774
- batchesPerCamera: external_exports.number().int().min(1).max(2e3).optional(),
14775
- batchSize: external_exports.number().int().min(1).max(64).optional(),
14776
- extraSinglesPerRound: external_exports.number().int().min(0).max(16).optional(),
14777
- nodeId: external_exports.string().optional(),
14778
- sessionId: external_exports.string().optional()
14777
+ var SyntheticBenchStatusSchema = external_exports.object({
14778
+ sessionId: external_exports.string(),
14779
+ state: external_exports.enum(["running", "done", "failed", "aborted"]),
14780
+ startedAtMs: external_exports.number(),
14781
+ finishedAtMs: external_exports.number().nullable(),
14782
+ progress: external_exports.object({ runs: external_exports.number(), totalRuns: external_exports.number() }).nullable(),
14783
+ result: SyntheticBenchStatsSchema.nullable(),
14784
+ error: external_exports.string().nullable()
14785
+ }).nullable();
14786
+ var MotionSessionSchema = external_exports.object({
14787
+ motionEventId: external_exports.number(),
14788
+ deviceId: external_exports.number(),
14789
+ startMs: external_exports.number(),
14790
+ /** `null` while the session is still open. */
14791
+ durationMs: external_exports.number().nullable(),
14792
+ open: external_exports.boolean(),
14793
+ /** Why a replay of this session would be refused (`session-open`, `session-length-unknown`, …); `null` = replayable. */
14794
+ refusal: external_exports.string().nullable(),
14795
+ /** Stored tracks born inside the session window (slim read). */
14796
+ storedTracks: external_exports.number()
14779
14797
  });
14780
- var MultiCameraStressResultSchema = external_exports.object({
14781
- totalMs: external_exports.number(),
14782
- countedDetections: external_exports.number(),
14783
- detsPerSec: external_exports.number(),
14784
- totalCalls: external_exports.number(),
14785
- totalFrames: external_exports.number(),
14786
- callMsP50: external_exports.number(),
14787
- callMsP95: external_exports.number(),
14788
- callMsP99: external_exports.number(),
14789
- inferMsMean: external_exports.number(),
14790
- errors: external_exports.number()
14798
+ var ListMotionSessionsInputSchema = external_exports.object({
14799
+ deviceId: external_exports.number(),
14800
+ /** Default: the last 24 h. */
14801
+ sinceMs: external_exports.number().optional(),
14802
+ untilMs: external_exports.number().optional(),
14803
+ limit: external_exports.number().int().min(1).max(200).default(50)
14791
14804
  });
14792
- var SustainedEngineSchema = external_exports.object({
14793
- runtime: external_exports.enum(["python", "node"]),
14794
- backend: external_exports.string(),
14795
- format: external_exports.enum(["onnx", "coreml", "openvino", "tflite"]),
14796
- device: external_exports.string().optional()
14797
- }).optional();
14798
- var SustainedNodeTargetSchema = external_exports.object({
14799
- nodeId: external_exports.string().optional(),
14800
- engine: SustainedEngineSchema,
14805
+ var SESSION_REPLAY_MAX_FRAMES = 1200;
14806
+ var StartSessionReplayInputSchema = external_exports.object({
14807
+ deviceId: external_exports.number(),
14808
+ motionEventId: external_exports.number(),
14809
+ /** The session's start (`getMotionEvents.timestamp`) — the lookup is windowed on it. */
14810
+ motionStartMs: external_exports.number(),
14811
+ nodeId: external_exports.string(),
14812
+ /** Absent ⇒ the node-default pool. */
14801
14813
  deviceKey: external_exports.string().optional(),
14802
- steps: external_exports.array(import_types.PipelineStepInputSchema).readonly(),
14803
- label: external_exports.string().optional()
14804
- });
14805
- var SustainedThroughputInputSchema = external_exports.object({
14806
- frameId: external_exports.string().optional(),
14807
- imageBase64: external_exports.string().optional(),
14808
- durationSec: external_exports.number().min(1).max(3600),
14809
- batchSize: external_exports.number().int().min(1).max(255),
14810
- concurrency: external_exports.number().int().min(1).max(64),
14811
- targetSize: external_exports.number().int().min(64).max(2048).optional(),
14812
- sessionId: external_exports.string(),
14813
- nodes: external_exports.array(SustainedNodeTargetSchema).min(1).readonly()
14814
+ /** Absent ⇒ the model the camera's resolved tree names. */
14815
+ modelId: external_exports.string().optional(),
14816
+ /** Absent ⇒ measured from the stored positions (the live cadence). */
14817
+ fps: external_exports.number().min(1).max(30).optional(),
14818
+ maxFrames: external_exports.number().int().min(1).max(SESSION_REPLAY_MAX_FRAMES).default(600)
14814
14819
  });
14815
- var SustainedLatencyStatsSchema = external_exports.object({
14816
- p50: external_exports.number(),
14817
- p95: external_exports.number(),
14818
- p99: external_exports.number(),
14819
- mean: external_exports.number(),
14820
- min: external_exports.number(),
14821
- max: external_exports.number()
14820
+ var StartSessionReplayAcceptedSchema = external_exports.object({
14821
+ accepted: external_exports.literal(true),
14822
+ runId: external_exports.string()
14822
14823
  });
14823
- var SustainedNodeResultSchema = external_exports.object({
14824
- nodeId: external_exports.string().nullable(),
14825
- label: external_exports.string().optional(),
14826
- /** Total frames processed by this node's loop. */
14827
- totalFrames: external_exports.number(),
14828
- /** Total completed batch calls. */
14829
- totalBatches: external_exports.number(),
14830
- /** Measured wall-clock window in ms. */
14831
- wallMs: external_exports.number(),
14832
- /** Sustained throughput. */
14833
- fps: external_exports.number(),
14834
- detsPerSec: external_exports.number(),
14835
- /** Mean detections per frame — stable since the frame is fixed. */
14836
- avgDetections: external_exports.number(),
14837
- latency: SustainedLatencyStatsSchema,
14838
- /** Batch calls that errored (transport / inference). */
14839
- errors: external_exports.number(),
14840
- /** True when this node ended via the stop button rather than the deadline. */
14841
- aborted: external_exports.boolean()
14824
+ var SessionReplayKeySchema = external_exports.object({ runId: external_exports.string() });
14825
+ var SessionReplayStatusSchema = external_exports.object({
14826
+ runId: external_exports.string(),
14827
+ state: external_exports.enum(["running", "done", "failed", "aborted"]),
14828
+ startedAtMs: external_exports.number(),
14829
+ finishedAtMs: external_exports.number().nullable(),
14830
+ progress: SessionReplayProgressSchema.nullable(),
14831
+ outcome: SessionReplayOutcomeSchema.nullable(),
14832
+ error: external_exports.string().nullable()
14833
+ }).nullable();
14834
+ var StoredSessionReplaySummarySchema = external_exports.object({
14835
+ id: external_exports.string(),
14836
+ deviceId: external_exports.number(),
14837
+ nodeId: external_exports.string(),
14838
+ createdAtMs: external_exports.number(),
14839
+ report: SessionReplayReportSchema.omit({ timeline: true })
14842
14840
  });
14843
- var SustainedThroughputResultSchema = external_exports.object({
14844
- perNode: external_exports.array(SustainedNodeResultSchema).readonly(),
14845
- durationSec: external_exports.number(),
14846
- batchSize: external_exports.number(),
14847
- concurrency: external_exports.number()
14841
+ var ListSessionReplaysInputSchema = external_exports.object({ deviceId: external_exports.number() });
14842
+ var ListSessionReplaysResultSchema = external_exports.object({
14843
+ runs: external_exports.array(StoredSessionReplaySummarySchema).readonly(),
14844
+ /** Rows that did not read as a session replay (the removed clip replays). */
14845
+ skipped: external_exports.number()
14848
14846
  });
14849
- var ReplaySourceSchema = external_exports.custom();
14850
- var ReplayOverlaySchema = external_exports.custom();
14851
- var OrchestrateReplayRunResultSchema = external_exports.custom();
14852
- var StoredReplayRunSchema = external_exports.custom();
14853
- var ReplayDiffSchema = external_exports.custom();
14854
- var RunReplayClipInputSchema = external_exports.object({
14847
+ var GetSessionReplayInputSchema = external_exports.object({ id: external_exports.string() });
14848
+ var StoredSessionReplaySchema = external_exports.object({
14849
+ id: external_exports.string(),
14855
14850
  deviceId: external_exports.number(),
14856
14851
  nodeId: external_exports.string(),
14857
- source: ReplaySourceSchema,
14858
- overlay: ReplayOverlaySchema,
14859
- fps: external_exports.number().positive(),
14860
- maxFrames: external_exports.number().int().positive(),
14861
- maxWidth: external_exports.number().int().positive().default(1920),
14862
- deviceKey: external_exports.string().optional()
14863
- });
14864
- var UploadReplayClipInputSchema = external_exports.object({
14865
- dataBase64: external_exports.string(),
14866
- durationMs: external_exports.number().positive()
14852
+ createdAtMs: external_exports.number(),
14853
+ report: SessionReplayReportSchema
14867
14854
  });
14868
- var UploadReplayClipResultSchema = external_exports.object({ clipId: external_exports.string() });
14869
- var ListReplayRunsInputSchema = external_exports.object({ deviceId: external_exports.number() });
14870
- var DiffReplayRunsInputSchema = external_exports.object({ runIdA: external_exports.string(), runIdB: external_exports.string() });
14871
14855
 
14872
14856
  // src/benchmark-actions.ts
14873
- var benchmarkActions = (0, import_types2.defineCustomActions)({
14874
- // Pipeline tester
14875
- runPipelineTest: (0, import_types2.customAction)(PipelineRunInputSchema, import_types2.PipelineRunResultBridge, {
14876
- kind: "mutation"
14877
- }),
14878
- /**
14879
- * Decode a JPEG once on the server and cache the resulting raw RGB
14880
- * buffer keyed by a returned `frameId`. Subsequent `runPipelineTest`
14881
- * calls pass `frameId` instead of `imageBase64` — the addon ships the
14882
- * cached raw frame to `pipelineExecutor.runPipeline` via the `frame`
14883
- * field, mirroring the production camera path (stream-broker delivers
14884
- * decoded RGB to pipeline-runner). Eliminates per-call JPEG decode +
14885
- * 265KB base64 transfer that was dominating bench transport time.
14886
- */
14887
- cacheBenchFrame: (0, import_types2.customAction)(
14888
- external_exports.object({ imageBase64: external_exports.string(), ttlSeconds: external_exports.number().int().positive().optional() }),
14889
- external_exports.object({ frameId: external_exports.string(), width: external_exports.number(), height: external_exports.number(), expiresAt: external_exports.number() }),
14890
- { kind: "mutation" }
14891
- ),
14892
- releaseBenchFrame: (0, import_types2.customAction)(
14893
- external_exports.object({ frameId: external_exports.string() }),
14894
- external_exports.object({ released: external_exports.boolean() }),
14895
- { kind: "mutation" }
14896
- ),
14897
- /**
14898
- * Batched pipeline tester. Decodes the JPEG once into RGB on the
14899
- * addon, then forwards N FrameInputs to the target node in a single
14900
- * `pipelineExecutor.runPipelineBatch` cap call. Returns N
14901
- * `FrameResult`s plus aggregate timing.
14902
- *
14903
- * NO UI CALLER — and it is NOT dead. `scripts/bench-pool-ceiling.mts` and
14904
- * `scripts/bench-pipeline-investigation.mts` drive it directly. Do not
14905
- * delete it on the strength of an in-package grep; those scripts break
14906
- * silently and nothing in CI runs them.
14907
- */
14908
- runPipelineBatchTest: (0, import_types2.customAction)(PipelineRunBatchInputSchema, PipelineRunBatchResultSchema, {
14857
+ var benchmarkActions = (0, import_types3.defineCustomActions)({
14858
+ // Detection Pipeline — one run.
14859
+ runPipelineTest: (0, import_types3.customAction)(PipelineRunInputSchema, import_types3.PipelineRunResultBridge, {
14909
14860
  kind: "mutation"
14910
14861
  }),
14911
14862
  /**
14912
- * Multi-camera stress tester — runs the full multi-camera workload
14913
- * loop in-process (decode JPEG once, then N parallel "cameras" each
14914
- * issuing a configurable mix of batched + single cap calls). Returns
14915
- * only aggregate detection-rate stats. Used to measure the pool's
14916
- * true throughput ceiling without per-call tRPC overhead.
14917
- *
14918
- * NO UI CALLER — and it is NOT dead. `scripts/bench-pool-inprocess.mts`
14919
- * drives it directly. Same warning as `runPipelineBatchTest` above.
14920
- */
14921
- runMultiCameraStressTest: (0, import_types2.customAction)(
14922
- MultiCameraStressInputSchema,
14923
- MultiCameraStressResultSchema,
14924
- { kind: "mutation" }
14925
- ),
14926
- /**
14927
- * Sustained-throughput ROOT-detection e2e run. Pins ONE frame (decoded
14928
- * once), then feeds the identical frame to root object-detection for
14929
- * `durationSec` seconds — driven by BATCHES (`runPipelineBatch`,
14930
- * `batchSize` frames/call) with `concurrency` calls in flight, fanned out
14931
- * across `nodes`, each on its resolved runtime/model + root-only steps.
14932
- * Returns per-node sustained fps / det/s + latency percentiles on the
14933
- * identical pinned frame. Bounded by N + `abortSustainedThroughput`.
14934
- */
14935
- runSustainedThroughput: (0, import_types2.customAction)(
14936
- SustainedThroughputInputSchema,
14937
- SustainedThroughputResultSchema,
14938
- { kind: "mutation" }
14939
- ),
14940
- abortSustainedThroughput: (0, import_types2.customAction)(
14941
- external_exports.object({ sessionId: external_exports.string() }),
14942
- external_exports.object({ aborted: external_exports.boolean() }),
14943
- { kind: "mutation" }
14944
- ),
14945
- /**
14946
- * History (shared across pipeline + decoder-perf entries).
14863
+ * History.
14947
14864
  *
14948
14865
  * `z.object({})`, never `z.void()`: the `addons.custom` envelope insists
14949
- * on an `input` object, and a `z.void()` schema REJECTS `{}` — so the
14950
- * action resolved and could never actually be called, not from a settings
14951
- * button and not from `hub.mjs`. The tabs only got away with it by
14952
- * sending `input: undefined`.
14866
+ * on an `input` object, and a `z.void()` schema REJECTS `{}`.
14953
14867
  */
14954
- listBenchmarkHistory: (0, import_types2.customAction)(external_exports.object({}), external_exports.array(HistoryEntrySchema).readonly()),
14955
- saveBenchmarkResult: (0, import_types2.customAction)(
14868
+ listBenchmarkHistory: (0, import_types3.customAction)(external_exports.object({}), external_exports.array(HistoryEntrySchema).readonly()),
14869
+ saveBenchmarkResult: (0, import_types3.customAction)(
14956
14870
  PipelineBenchmarkHistoryEntryInputSchema,
14957
14871
  PipelineBenchmarkHistoryEntrySchema,
14958
14872
  { kind: "mutation" }
14959
14873
  ),
14960
- saveDecoderPerformanceResult: (0, import_types2.customAction)(
14961
- DecoderPerfHistoryEntryInputSchema,
14962
- DecoderPerfHistoryEntrySchema,
14963
- { kind: "mutation" }
14964
- ),
14965
- deleteBenchmarkResult: (0, import_types2.customAction)(
14874
+ deleteBenchmarkResult: (0, import_types3.customAction)(
14966
14875
  external_exports.object({ id: external_exports.string() }),
14967
14876
  external_exports.object({ success: external_exports.literal(true) }),
14968
14877
  { kind: "mutation" }
14969
14878
  ),
14970
- clearBenchmarkHistory: (0, import_types2.customAction)(external_exports.object({}), external_exports.object({ success: external_exports.literal(true) }), {
14879
+ clearBenchmarkHistory: (0, import_types3.customAction)(external_exports.object({}), external_exports.object({ success: external_exports.literal(true) }), {
14971
14880
  kind: "mutation"
14972
14881
  }),
14973
- // Replay — rigiocare una pipeline vera su una clip fissa. Regia qui,
14974
- // FrameProcessor pass in `addon-post-analysis` via
14975
- // `pipelineAnalytics.runReplayFrameProcessor` (see `replay/replay-wiring.ts`).
14976
- uploadReplayClip: (0, import_types2.customAction)(UploadReplayClipInputSchema, UploadReplayClipResultSchema, {
14977
- kind: "mutation"
14978
- }),
14979
- runReplayClip: (0, import_types2.customAction)(RunReplayClipInputSchema, OrchestrateReplayRunResultSchema, {
14980
- kind: "mutation"
14981
- }),
14982
- listReplayRuns: (0, import_types2.customAction)(
14983
- ListReplayRunsInputSchema,
14984
- external_exports.array(StoredReplayRunSchema).readonly()
14882
+ // Session replay (D646) — the stored sessions to pick from, then start / poll / abort.
14883
+ listMotionSessions: (0, import_types3.customAction)(
14884
+ ListMotionSessionsInputSchema,
14885
+ external_exports.array(MotionSessionSchema).readonly()
14985
14886
  ),
14986
- diffReplayRuns: (0, import_types2.customAction)(DiffReplayRunsInputSchema, ReplayDiffSchema),
14987
- // Decoder perf — fixture discovery + run control.
14988
- // `z.object({})` for the same reason as `listBenchmarkHistory` above.
14989
- listDecoderFixtures: (0, import_types2.customAction)(external_exports.object({}), external_exports.array(DecoderFixtureSchema).readonly()),
14990
- runDecoderPerformanceTest: (0, import_types2.customAction)(DecoderPerfInputSchema, DecoderPerfResultSchema, {
14991
- kind: "mutation"
14887
+ startSessionReplay: (0, import_types3.customAction)(
14888
+ StartSessionReplayInputSchema,
14889
+ StartSessionReplayAcceptedSchema,
14890
+ { kind: "mutation", auth: "admin" }
14891
+ ),
14892
+ getSessionReplayStatus: (0, import_types3.customAction)(SessionReplayKeySchema, SessionReplayStatusSchema),
14893
+ abortSessionReplay: (0, import_types3.customAction)(SessionReplayKeySchema, external_exports.object({ aborted: external_exports.boolean() }), {
14894
+ kind: "mutation",
14895
+ auth: "admin"
14992
14896
  }),
14993
- abortDecoderPerformanceTest: (0, import_types2.customAction)(
14994
- external_exports.object({ sessionId: external_exports.string() }),
14995
- external_exports.object({ aborted: external_exports.boolean() }),
14996
- { kind: "mutation" }
14997
- )
14897
+ listSessionReplays: (0, import_types3.customAction)(ListSessionReplaysInputSchema, ListSessionReplaysResultSchema),
14898
+ getSessionReplay: (0, import_types3.customAction)(GetSessionReplayInputSchema, StoredSessionReplaySchema.nullable())
14998
14899
  });
14999
14900
 
15000
- // src/benchmark-addon.ts
15001
- var import_node_crypto = require("crypto");
15002
-
15003
- // src/decoder-perf-runner.ts
15004
- var fs = __toESM(require("fs"));
15005
- var path = __toESM(require("path"));
15006
- var import_node_perf_hooks = require("perf_hooks");
15007
- var import_node_process = require("process");
15008
- var import_types3 = require("@camstack/types");
15009
- var CHUNK_SIZE = 4096;
15010
- function emitProgress(eventBus, payload) {
15011
- if (!eventBus) return;
15012
- eventBus.emit(
15013
- (0, import_types3.createEvent)(
15014
- "benchmark.progress",
15015
- { type: "benchmark", id: "decoder-perf" },
15016
- {
15017
- kind: payload.kind,
15018
- sessionId: payload.sessionId,
15019
- phase: payload.phase,
15020
- tSec: payload.tSec,
15021
- sample: payload.sample ? { ...payload.sample } : void 0,
15022
- message: payload.message
15023
- }
15024
- )
15025
- );
15026
- }
15027
- function slope(xs, ys) {
15028
- if (xs.length < 2) return 0;
15029
- const n = xs.length;
15030
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
15031
- for (let i = 0; i < n; i++) {
15032
- sx += xs[i];
15033
- sy += ys[i];
15034
- sxx += xs[i] * xs[i];
15035
- sxy += xs[i] * ys[i];
15036
- }
15037
- const denom = n * sxx - sx * sx;
15038
- return denom === 0 ? 0 : (n * sxy - sx * sy) / denom;
15039
- }
15040
- function verdictFor(rssSlopeMBMin, warnMBMin, failMBMin) {
15041
- if (rssSlopeMBMin >= failMBMin) return "fail";
15042
- if (rssSlopeMBMin >= warnMBMin) return "warn";
15043
- return "pass";
15044
- }
15045
- function resolveFixtureCodec(filename) {
15046
- if (filename.endsWith(".hevc") || filename.endsWith(".h265")) return "h265";
15047
- return "h264";
15048
- }
15049
- function readFixtureBytes(fixturesDir, filename) {
15050
- const full = path.join(fixturesDir, filename);
15051
- if (!fs.existsSync(full)) {
15052
- throw new Error(`decoder fixture not found: ${filename}`);
15053
- }
15054
- return fs.readFileSync(full);
15055
- }
15056
- async function createSessions(api, input, codec2) {
15057
- const handles = [];
15058
- const pin = input.nodeId ? (0, import_types3.nodePin)(input.nodeId) : void 0;
15059
- for (let i = 0; i < input.cameras; i++) {
15060
- const created = await api.decoder.createSession.query(
15061
- {
15062
- codec: codec2,
15063
- maxFps: input.fps,
15064
- outputFormat: input.format === "gray" ? "gray" : "jpeg",
15065
- scale: 1
15066
- },
15067
- pin
15068
- );
15069
- handles.push({
15070
- sessionId: created.sessionId,
15071
- nodeId: created.nodeId,
15072
- codec: codec2,
15073
- offsetBytes: 0,
15074
- framesPushed: 0
15075
- });
15076
- }
15077
- return handles;
15078
- }
15079
- async function destroySessions(api, handles, logger) {
15080
- for (const h of handles) {
15081
- try {
15082
- await api.decoder.destroySession.query({ sessionId: h.sessionId, nodeId: h.nodeId });
15083
- } catch (err) {
15084
- logger.warn("destroySession failed", {
15085
- meta: { sessionId: h.sessionId, error: err instanceof Error ? err.message : String(err) }
15086
- });
15087
- }
15088
- }
15089
- }
15090
- async function pushChunk(api, handle, fixture) {
15091
- if (handle.offsetBytes >= fixture.length) handle.offsetBytes = 0;
15092
- const end = Math.min(handle.offsetBytes + CHUNK_SIZE, fixture.length);
15093
- const chunk = fixture.subarray(handle.offsetBytes, end);
15094
- handle.offsetBytes = end;
15095
- const now = Date.now();
15096
- await api.decoder.pushPacket.query({
15097
- sessionId: handle.sessionId,
15098
- nodeId: handle.nodeId,
15099
- packet: {
15100
- type: "video",
15101
- data: new Uint8Array(chunk),
15102
- pts: now,
15103
- dts: now,
15104
- keyframe: handle.offsetBytes <= CHUNK_SIZE,
15105
- codec: handle.codec
15106
- }
15107
- });
15108
- handle.framesPushed += 1;
15109
- }
15110
- async function sumDroppedFrames(api, handles) {
15111
- let dropped = 0;
15112
- for (const h of handles) {
15113
- try {
15114
- const stats = await api.decoder.getStats.query({ sessionId: h.sessionId, nodeId: h.nodeId });
15115
- dropped += stats.droppedFrames;
15116
- } catch {
15117
- }
15118
- }
15119
- return dropped;
15120
- }
15121
- async function runDecoderPerfTest(input, deps) {
15122
- const { api, eventBus, logger, fixturesDir, abortFlag } = deps;
15123
- const sessionId = input.sessionId;
15124
- const samples = [];
15125
- const startWall = import_node_perf_hooks.performance.now();
15126
- emitProgress(eventBus, {
15127
- kind: "decoder-perf",
15128
- sessionId,
15129
- phase: "init",
15130
- message: "initialising"
15131
- });
15132
- if (input.source.kind !== "sample") {
15133
- const msg = "decoder perf: stream source not yet implemented \u2014 use a sample fixture";
15134
- emitProgress(eventBus, { kind: "decoder-perf", sessionId, phase: "error", message: msg });
15135
- return {
15136
- sessionId,
15137
- verdict: "error",
15138
- aborted: false,
15139
- errorMessage: msg,
15140
- samples: [],
15141
- slopes: { rssMBMin: 0, heapMBMin: 0, externalMBMin: 0 },
15142
- totals: { wallSec: 0, framesEmitted: 0, fpsPerCamera: 0, cpuAvgPct: 0, droppedFrames: 0 },
15143
- config: input
15144
- };
15145
- }
15146
- const fixtureName = input.source.fixtureFilename;
15147
- const codec2 = resolveFixtureCodec(fixtureName);
15148
- const fixture = readFixtureBytes(fixturesDir, fixtureName);
15149
- try {
15150
- const active = await api.decoder.listActiveSessions.query();
15151
- if (active.length > 0) {
15152
- logger.warn("concurrent decoder sessions detected \u2014 measurement may be contaminated", {
15153
- meta: { concurrent: active.length }
15154
- });
15155
- }
15156
- } catch {
15157
- }
15158
- try {
15159
- const instances = await api.metricsProvider.listAddonInstances.query();
15160
- const workerInstances = instances.filter((i) => i.role === "worker");
15161
- if (workerInstances.length === 0) {
15162
- logger.info(
15163
- "decoder perf: no isolated worker detected \u2014 hub-local measurement (process.memoryUsage) mixes decoder + hub workload"
15164
- );
15165
- } else {
15166
- logger.info("decoder perf: worker instances visible", {
15167
- meta: { workers: workerInstances.map((w) => w.addonId) }
15168
- });
15169
- }
15170
- } catch {
15171
- }
15172
- let handles;
15173
- try {
15174
- handles = await createSessions(api, input, codec2);
15175
- } catch (err) {
15176
- const msg = err instanceof Error ? err.message : String(err);
15177
- emitProgress(eventBus, {
15178
- kind: "decoder-perf",
15179
- sessionId,
15180
- phase: "error",
15181
- message: `createSession failed: ${msg}`
15182
- });
15183
- return {
15184
- sessionId,
15185
- verdict: "error",
15186
- aborted: false,
15187
- errorMessage: msg,
15188
- samples: [],
15189
- slopes: { rssMBMin: 0, heapMBMin: 0, externalMBMin: 0 },
15190
- totals: { wallSec: 0, framesEmitted: 0, fpsPerCamera: 0, cpuAvgPct: 0, droppedFrames: 0 },
15191
- config: input
15192
- };
14901
+ // src/replay/mjpeg-split.ts
14902
+ var SOI = Buffer.from([255, 216]);
14903
+ var EOI = Buffer.from([255, 217]);
14904
+ function splitMjpegStream(bytes) {
14905
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
14906
+ const out = [];
14907
+ let pos = 0;
14908
+ for (; ; ) {
14909
+ const soi = buf.indexOf(SOI, pos);
14910
+ if (soi === -1) break;
14911
+ const eoi = buf.indexOf(EOI, soi + SOI.length);
14912
+ if (eoi === -1) break;
14913
+ out.push(buf.subarray(soi, eoi + EOI.length));
14914
+ pos = eoi + EOI.length;
15193
14915
  }
15194
- const frameIntervalMs = 1e3 / input.fps;
15195
- const warmupMs = input.warmupSec * 1e3;
15196
- const durationMs = input.mode.kind === "duration" ? input.mode.durationSec * 1e3 : Number.MAX_SAFE_INTEGER;
15197
- const targetIterations = input.mode.kind === "iterations" ? input.mode.iterations : Number.MAX_SAFE_INTEGER;
15198
- let cpuSnapshot = (0, import_node_process.cpuUsage)();
15199
- let cpuWallMs = import_node_perf_hooks.performance.now();
15200
- const feedTickerStart = import_node_perf_hooks.performance.now();
15201
- let nextFeedAt = feedTickerStart;
15202
- let nextSampleAt = feedTickerStart + warmupMs;
15203
- let aborted2 = false;
15204
- emitProgress(eventBus, {
15205
- kind: "decoder-perf",
15206
- sessionId,
15207
- phase: "warmup",
15208
- tSec: 0,
15209
- message: `warming up ${input.warmupSec}s`
15210
- });
15211
- const loopUntil = feedTickerStart + warmupMs + durationMs;
15212
- while (import_node_perf_hooks.performance.now() < loopUntil) {
15213
- if (abortFlag.aborted) {
15214
- aborted2 = true;
15215
- break;
15216
- }
15217
- const totalPushed = handles.reduce((s, h) => s + h.framesPushed, 0);
15218
- if (totalPushed >= targetIterations * handles.length) break;
15219
- const now = import_node_perf_hooks.performance.now();
15220
- if (now >= nextFeedAt) {
15221
- for (const h of handles) {
15222
- try {
15223
- await pushChunk(api, h, fixture);
15224
- } catch (err) {
15225
- logger.warn("pushPacket failed", {
15226
- meta: {
15227
- sessionId: h.sessionId,
15228
- error: err instanceof Error ? err.message : String(err)
15229
- }
15230
- });
15231
- }
15232
- }
15233
- nextFeedAt += frameIntervalMs;
15234
- }
15235
- if (now >= nextSampleAt) {
15236
- const tSec = (now - feedTickerStart) / 1e3;
15237
- const mem = (0, import_node_process.memoryUsage)();
15238
- const cpu = (0, import_node_process.cpuUsage)(cpuSnapshot);
15239
- const windowMs = now - cpuWallMs;
15240
- const cpuPct = windowMs > 0 ? (cpu.user + cpu.system) / 1e3 / windowMs * 100 : 0;
15241
- cpuSnapshot = (0, import_node_process.cpuUsage)();
15242
- cpuWallMs = now;
15243
- const sample = {
15244
- tSec,
15245
- rssMB: mem.rss / 1048576,
15246
- heapMB: mem.heapUsed / 1048576,
15247
- externalMB: mem.external / 1048576,
15248
- arrayBuffersMB: mem.arrayBuffers / 1048576,
15249
- cpuPct,
15250
- framesTotal: handles.reduce((s, h) => s + h.framesPushed, 0)
15251
- };
15252
- samples.push(sample);
15253
- emitProgress(eventBus, {
15254
- kind: "decoder-perf",
15255
- sessionId,
15256
- phase: tSec < 0 ? "warmup" : "running",
15257
- tSec,
15258
- sample
15259
- });
15260
- nextSampleAt += input.sampleIntervalMs;
15261
- }
15262
- const sleepMs = Math.max(0, Math.min(nextFeedAt, nextSampleAt) - import_node_perf_hooks.performance.now());
15263
- if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
15264
- }
15265
- const droppedFrames = await sumDroppedFrames(api, handles);
15266
- await destroySessions(api, handles, logger);
15267
- const postWarmup = samples.filter((s) => s.tSec * 1e3 >= warmupMs);
15268
- const xs = postWarmup.map((s) => s.tSec);
15269
- const rssYs = postWarmup.map((s) => s.rssMB);
15270
- const heapYs = postWarmup.map((s) => s.heapMB);
15271
- const extYs = postWarmup.map((s) => s.externalMB);
15272
- const rssSlopeMBMin = slope(xs, rssYs) * 60;
15273
- const heapSlopeMBMin = slope(xs, heapYs) * 60;
15274
- const externalSlopeMBMin = slope(xs, extYs) * 60;
15275
- const wallSec = (import_node_perf_hooks.performance.now() - startWall) / 1e3;
15276
- const framesEmitted = handles.reduce((s, h) => s + h.framesPushed, 0);
15277
- const fpsPerCamera = wallSec > 0 ? framesEmitted / Math.max(1, handles.length) / wallSec : 0;
15278
- const cpuAvgPct = samples.length > 0 ? samples.reduce((s, x) => s + x.cpuPct, 0) / samples.length : 0;
15279
- const verdict = aborted2 ? "aborted" : verdictFor(rssSlopeMBMin, input.thresholds.warnMBMin, input.thresholds.failMBMin);
15280
- emitProgress(eventBus, {
15281
- kind: "decoder-perf",
15282
- sessionId,
15283
- phase: aborted2 ? "aborted" : "complete",
15284
- message: `done \u2014 rss slope ${rssSlopeMBMin.toFixed(2)} MB/min`
15285
- });
15286
- return {
15287
- sessionId,
15288
- verdict,
15289
- aborted: aborted2,
15290
- samples,
15291
- slopes: {
15292
- rssMBMin: rssSlopeMBMin,
15293
- heapMBMin: heapSlopeMBMin,
15294
- externalMBMin: externalSlopeMBMin
15295
- },
15296
- totals: { wallSec, framesEmitted, fpsPerCamera, cpuAvgPct, droppedFrames },
15297
- config: input
15298
- };
14916
+ return out;
15299
14917
  }
15300
14918
 
15301
14919
  // src/replay/replay-decode.ts
@@ -15324,13 +14942,13 @@ var REPLAY_DECODE_MAX_CONCURRENT = 2;
15324
14942
  var REPLAY_DECODE_TIMEOUT_MS = 15e3;
15325
14943
  var KILL_GRACE_MS = 500;
15326
14944
  var ReplayDecoder = class {
15327
- ffmpegPath;
14945
+ resolveFfmpegPath;
15328
14946
  spawnFn;
15329
14947
  maxConcurrent;
15330
14948
  timeoutMs;
15331
14949
  inFlight = 0;
15332
14950
  constructor(deps) {
15333
- this.ffmpegPath = deps.ffmpegPath;
14951
+ this.resolveFfmpegPath = deps.resolveFfmpegPath;
15334
14952
  this.spawnFn = deps.spawnFn ?? ((bin, args) => (0, import_node_child_process.spawn)(bin, [...args]));
15335
14953
  this.maxConcurrent = deps.maxConcurrent ?? REPLAY_DECODE_MAX_CONCURRENT;
15336
14954
  this.timeoutMs = deps.timeoutMs ?? REPLAY_DECODE_TIMEOUT_MS;
@@ -15347,7 +14965,7 @@ var ReplayDecoder = class {
15347
14965
  * uploaded clip) are fetched through a capability call, not a local path.
15348
14966
  * Resolves with the whole MJPEG stdout stream.
15349
14967
  */
15350
- async decodeSequence(args, input) {
14968
+ async decodeSequence(args, input, opts = {}) {
15351
14969
  if (this.inFlight >= this.maxConcurrent) {
15352
14970
  throw new Error(
15353
14971
  `replay decode busy: node already running ${this.inFlight}/${this.maxConcurrent} decodes`
@@ -15355,19 +14973,20 @@ var ReplayDecoder = class {
15355
14973
  }
15356
14974
  this.inFlight += 1;
15357
14975
  try {
15358
- return await this.runOnce(args, input);
14976
+ const ffmpegPath = await this.resolveFfmpegPath();
14977
+ return await this.runOnce(ffmpegPath, args, input, opts.timeoutMs ?? this.timeoutMs);
15359
14978
  } finally {
15360
14979
  this.inFlight -= 1;
15361
14980
  }
15362
14981
  }
15363
- runOnce(args, input) {
15364
- return new Promise((resolve2, reject) => {
14982
+ runOnce(ffmpegPath, args, input, timeoutMs) {
14983
+ return new Promise((resolve, reject) => {
15365
14984
  const chunks = [];
15366
14985
  const stderrTail = [];
15367
14986
  let settled = false;
15368
14987
  const child = (() => {
15369
14988
  try {
15370
- return this.spawnFn(this.ffmpegPath, args);
14989
+ return this.spawnFn(ffmpegPath, args);
15371
14990
  } catch (err) {
15372
14991
  reject(new Error(`replay decode: ffmpeg spawn failed: ${String(err)}`));
15373
14992
  return null;
@@ -15398,7 +15017,7 @@ var ReplayDecoder = class {
15398
15017
  const timer = setTimeout(() => {
15399
15018
  settle(() => reject(new Error("replay decode: timed out")));
15400
15019
  kill();
15401
- }, this.timeoutMs);
15020
+ }, timeoutMs);
15402
15021
  timer.unref?.();
15403
15022
  child.stdout?.on("data", (chunk) => chunks.push(chunk));
15404
15023
  child.stderr?.on("data", (data) => {
@@ -15414,7 +15033,7 @@ var ReplayDecoder = class {
15414
15033
  child.on("exit", (code, signal) => {
15415
15034
  const stdout = Buffer.concat(chunks);
15416
15035
  if (code === 0) {
15417
- settle(() => resolve2(stdout));
15036
+ settle(() => resolve(stdout));
15418
15037
  return;
15419
15038
  }
15420
15039
  settle(
@@ -15436,6 +15055,7 @@ var ReplayDecoder = class {
15436
15055
 
15437
15056
  // src/replay/replay-store.ts
15438
15057
  var REPLAY_RUNS_COLLECTION = "benchmark:replay-runs";
15058
+ var REPLAY_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
15439
15059
  var REPLAY_RUNS_COLUMNS = [
15440
15060
  { name: "id", type: "TEXT", primaryKey: true, notNull: true },
15441
15061
  { name: "deviceId", type: "INTEGER", notNull: true },
@@ -15450,7 +15070,14 @@ var REPLAY_RUNS_INDEXES = [
15450
15070
  { name: "idx_replay_runs_device_created", columns: ["deviceId", "createdAtMs"] },
15451
15071
  { name: "idx_replay_runs_created", columns: ["createdAtMs"] }
15452
15072
  ];
15453
- var ReplayStore = class {
15073
+ var StoredRowSchema = external_exports.object({
15074
+ deviceId: external_exports.number(),
15075
+ nodeId: external_exports.string(),
15076
+ createdAtMs: external_exports.number(),
15077
+ source: external_exports.object({ kind: external_exports.literal("motion-session") }).loose(),
15078
+ verdict: SessionReplayReportSchema
15079
+ });
15080
+ var ReplayStore = class _ReplayStore {
15454
15081
  constructor(store) {
15455
15082
  this.store = store;
15456
15083
  }
@@ -15465,24 +15092,57 @@ var ReplayStore = class {
15465
15092
  });
15466
15093
  }
15467
15094
  async save(run) {
15468
- const { id, ...rest } = run;
15095
+ const { report } = run;
15469
15096
  await this.store.insert.mutate({
15470
15097
  collection: REPLAY_RUNS_COLLECTION,
15471
- record: { id, data: rest }
15472
- // TECH-DEBT: settings-store boundary, see the comment above.
15098
+ record: {
15099
+ id: run.id,
15100
+ data: {
15101
+ deviceId: run.deviceId,
15102
+ nodeId: run.nodeId,
15103
+ clipId: `session:${report.deviceId}:${report.motionEventId}`,
15104
+ createdAtMs: run.createdAtMs,
15105
+ source: { kind: "motion-session", motionEventId: report.motionEventId, ...report.window },
15106
+ plan: {
15107
+ pin: report.pin,
15108
+ rootStep: report.rootStep,
15109
+ cadence: report.cadence,
15110
+ frameSize: report.frameSize
15111
+ },
15112
+ verdict: report
15113
+ }
15114
+ }
15473
15115
  });
15474
15116
  }
15475
- /** Runs newest-first, optionally scoped to one device. */
15117
+ /** Session replays newest-first, optionally scoped to one camera. */
15476
15118
  async list(query) {
15477
- const filter = { orderBy: { field: "createdAtMs", direction: "desc" } };
15478
- if (query.deviceId !== void 0) filter.where = { deviceId: query.deviceId };
15479
- if (query.limit !== void 0) filter.limit = query.limit;
15480
- const rows = await this.store.query.query({ collection: REPLAY_RUNS_COLLECTION, filter });
15481
- return rows.map((r) => ({ id: r.id, ...r.data }));
15119
+ const rows = await this.store.query.query({
15120
+ collection: REPLAY_RUNS_COLLECTION,
15121
+ filter: {
15122
+ orderBy: { field: "createdAtMs", direction: "desc" },
15123
+ ...query.deviceId !== void 0 ? { where: { deviceId: query.deviceId } } : {},
15124
+ ...query.limit !== void 0 ? { limit: query.limit } : {}
15125
+ }
15126
+ });
15127
+ const runs = [];
15128
+ let skipped = 0;
15129
+ for (const row of rows) {
15130
+ const parsed = _ReplayStore.readRow(row.id, row.data);
15131
+ if (parsed === null) skipped += 1;
15132
+ else runs.push(parsed);
15133
+ }
15134
+ return { runs, skipped };
15135
+ }
15136
+ async get(id) {
15137
+ const rows = await this.store.query.query({
15138
+ collection: REPLAY_RUNS_COLLECTION,
15139
+ filter: { where: { id }, limit: 1 }
15140
+ });
15141
+ const row = rows[0];
15142
+ return row === void 0 ? null : _ReplayStore.readRow(row.id, row.data);
15482
15143
  }
15483
15144
  /** Delete every run created before `cutoffMs`. This store's OWN retention
15484
- * clock — see the file header for why that matters. Returns the count so
15485
- * a caller can log it, the way `OpsLogStore`-adjacent sweeps do. */
15145
+ * clock — see the file header for why that matters. */
15486
15146
  async pruneBefore(cutoffMs) {
15487
15147
  const { deleted } = await this.store.deleteWhere.mutate({
15488
15148
  collection: REPLAY_RUNS_COLLECTION,
@@ -15490,449 +15150,845 @@ var ReplayStore = class {
15490
15150
  });
15491
15151
  return deleted;
15492
15152
  }
15493
- };
15494
-
15495
- // src/replay/replay-plan.ts
15496
- function readPreflight(result) {
15497
- if (!result.ok) {
15498
- const detail = result.issues.map((i) => `${i.addonId}: ${i.detail}`).join("; ");
15499
- return { kind: "refused", reason: "step-unrunnable", detail };
15500
- }
15501
- if (result.substitutions.length > 0) {
15502
- const detail = result.substitutions.map((s) => `${s.addonId}: chose \`${s.chosen}\`, running \`${s.running}\` (${s.format})`).join("; ");
15503
- return { kind: "refused", reason: "model-substituted", detail };
15504
- }
15505
- return { kind: "runnable", format: result.format };
15506
- }
15507
- var PACKAGE_DETECTION_STEP_ID = "package-detection";
15508
- function applyPackageZoneOverlay(steps, overlay) {
15509
- if (overlay.packageDetection === void 0) return steps;
15510
- const existing = steps.find((s) => s.addonId === PACKAGE_DETECTION_STEP_ID);
15511
- if (!overlay.packageDetection) {
15512
- if (!existing) return steps;
15513
- return steps.filter((s) => s.addonId !== PACKAGE_DETECTION_STEP_ID);
15514
- }
15515
- if (existing) {
15516
- if (existing.enabled === true) return steps;
15517
- return steps.map((s) => s.addonId === PACKAGE_DETECTION_STEP_ID ? { ...s, enabled: true } : s);
15518
- }
15519
- const node = {
15520
- addonId: PACKAGE_DETECTION_STEP_ID,
15521
- enabled: true,
15522
- children: []
15523
- };
15524
- return [...steps, node];
15525
- }
15526
- function applySettingsAndModels(steps, overlay, ignored) {
15527
- const settingsKeys = new Set(Object.keys(overlay.stepSettings ?? {}));
15528
- const modelKeys = new Set(Object.keys(overlay.stepModels ?? {}));
15529
- const presentIds = new Set(steps.map((s) => s.addonId));
15530
- for (const id of settingsKeys) if (!presentIds.has(id)) ignored.add(id);
15531
- for (const id of modelKeys) if (!presentIds.has(id)) ignored.add(id);
15532
- return steps.map((s) => {
15533
- const settingsPatch = overlay.stepSettings?.[s.addonId];
15534
- const modelOverride = overlay.stepModels?.[s.addonId];
15535
- if (settingsPatch === void 0 && modelOverride === void 0) return s;
15153
+ static readRow(id, data) {
15154
+ const parsed = StoredRowSchema.safeParse(data);
15155
+ if (!parsed.success) return null;
15536
15156
  return {
15537
- ...s,
15538
- ...modelOverride !== void 0 ? { modelId: modelOverride } : {},
15539
- ...settingsPatch !== void 0 ? { settings: { ...s.settings, ...settingsPatch } } : {}
15157
+ id,
15158
+ deviceId: parsed.data.deviceId,
15159
+ nodeId: parsed.data.nodeId,
15160
+ createdAtMs: parsed.data.createdAtMs,
15161
+ report: parsed.data.verdict
15540
15162
  };
15541
- });
15542
- }
15543
- function buildReplayPlan(base, overlay) {
15544
- const ignored = /* @__PURE__ */ new Set();
15545
- const withPackageZone = applyPackageZoneOverlay(base.steps, overlay);
15546
- const steps = applySettingsAndModels(withPackageZone, overlay, ignored);
15547
- return { steps, ignoredOverlays: [...ignored] };
15548
- }
15549
-
15550
- // src/replay/replay-source.ts
15551
- function planReplayFrames(source, fps, maxFrames) {
15552
- const durationMs = source.kind === "recording" ? source.toMs - source.fromMs : source.durationMs;
15553
- if (durationMs <= 0) return { kind: "refused", reason: "empty-window" };
15554
- const stepMs = 1e3 / fps;
15555
- const startAt = source.kind === "recording" ? source.fromMs : 0;
15556
- const rawCount = Math.max(1, Math.floor(durationMs / stepMs));
15557
- const truncated = rawCount > maxFrames;
15558
- const count = Math.min(rawCount, maxFrames);
15559
- const instants = [];
15560
- for (let i = 0; i < count; i++) instants.push(startAt + Math.round(i * stepMs));
15561
- return { kind: "planned", deviceId: source.deviceId, instants, truncated };
15562
- }
15563
-
15564
- // src/replay/mjpeg-split.ts
15565
- var SOI = Buffer.from([255, 216]);
15566
- var EOI = Buffer.from([255, 217]);
15567
- function splitMjpegStream(bytes) {
15568
- const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
15569
- const out = [];
15570
- let pos = 0;
15571
- for (; ; ) {
15572
- const soi = buf.indexOf(SOI, pos);
15573
- if (soi === -1) break;
15574
- const eoi = buf.indexOf(EOI, soi + SOI.length);
15575
- if (eoi === -1) break;
15576
- out.push(buf.subarray(soi, eoi + EOI.length));
15577
- pos = eoi + EOI.length;
15578
15163
  }
15579
- return out;
15580
- }
15164
+ };
15581
15165
 
15582
- // src/replay/replay-run.ts
15583
- var DEFAULT_REPLAY_BUDGET_MS = 12e4;
15584
- var DEFAULT_REPLAY_PACING_MS = 50;
15585
- function sleep(ms) {
15586
- return new Promise((resolve2) => setTimeout(resolve2, ms));
15587
- }
15588
- async function runReplay(deps, frames) {
15589
- const now = deps.now ?? Date.now;
15590
- const budgetMs = deps.budgetMs ?? DEFAULT_REPLAY_BUDGET_MS;
15591
- const pacingMs = deps.pacingMs ?? DEFAULT_REPLAY_PACING_MS;
15592
- const startedAt = now();
15593
- const tags = { deviceId: deps.deviceId };
15594
- deps.logger.info("replay: run starting", {
15595
- tags,
15596
- meta: { framesPlanned: frames.length, nodeId: deps.nodeId, pacingMs, budgetMs }
15597
- });
15598
- const succeeded = [];
15599
- const discarded = [];
15600
- let framesFailed = 0;
15601
- let stoppedOnBudget = false;
15602
- for (let i = 0; i < frames.length; i++) {
15603
- if (now() - startedAt >= budgetMs) {
15604
- stoppedOnBudget = true;
15605
- deps.logger.warn("replay: run stopped on budget \u2014 a partial census, reported as such", {
15606
- tags,
15607
- meta: {
15608
- framesPlanned: frames.length,
15609
- framesRan: succeeded.length,
15610
- framesFailed,
15611
- budgetMs
15612
- }
15613
- });
15614
- break;
15166
+ // src/session-replay/recording-reader.ts
15167
+ async function readRecordingForInstants(deps, instants, stepMs) {
15168
+ const parts = [];
15169
+ let withoutFootage = 0;
15170
+ let truncated = false;
15171
+ let i = 0;
15172
+ while (i < instants.length) {
15173
+ const t = instants[i];
15174
+ if (t === void 0) break;
15175
+ const located = await deps.locateSegment(t);
15176
+ if (located.kind !== "segment") {
15177
+ const edge = located.nearestEdgeMs;
15178
+ let j2 = i + 1;
15179
+ if (edge === null) j2 = instants.length;
15180
+ else if (edge > t) while (j2 < instants.length && (instants[j2] ?? edge) < edge) j2++;
15181
+ withoutFootage += j2 - i;
15182
+ i = j2;
15183
+ continue;
15615
15184
  }
15616
- const f = frames[i];
15617
- if (f === void 0) continue;
15618
- try {
15619
- const frame = await deps.runPipeline({
15620
- timestamp: f.timestamp,
15621
- image: f.image,
15622
- nodeId: deps.nodeId
15623
- });
15624
- succeeded.push({ timestamp: f.timestamp, frame });
15625
- if (frame.discarded !== void 0) discarded.push(...frame.discarded);
15626
- } catch (err) {
15627
- framesFailed += 1;
15628
- deps.logger.warn("replay: frame failed on the node \u2014 counted, run continues", {
15629
- tags,
15630
- meta: { timestamp: f.timestamp, error: err instanceof Error ? err.message : String(err) }
15631
- });
15185
+ const segEnd = located.startMs + located.durMs;
15186
+ let j = i;
15187
+ while (j < instants.length && (instants[j] ?? segEnd) < segEnd) j++;
15188
+ const covered = instants.slice(i, j);
15189
+ const lastCovered = covered[covered.length - 1] ?? t;
15190
+ const window = await deps.readWindowBytes({
15191
+ startMs: located.startMs,
15192
+ fromMs: t,
15193
+ toMs: Math.min(segEnd, lastCovered + Math.round(stepMs))
15194
+ });
15195
+ if (window.kind === "ok") {
15196
+ if (!window.reachesRequestedEnd) truncated = true;
15197
+ parts.push({ bytes: window.data, gopStartMs: window.gopStartMs, instants: covered });
15198
+ } else {
15199
+ withoutFootage += covered.length;
15632
15200
  }
15633
- if (pacingMs > 0 && i < frames.length - 1) await sleep(pacingMs);
15201
+ i = j === i ? i + 1 : j;
15202
+ }
15203
+ if (parts.length === 0) {
15204
+ return {
15205
+ kind: "refused",
15206
+ reason: "no-footage",
15207
+ detail: `no recording covers any of the ${instants.length} planned instants`
15208
+ };
15634
15209
  }
15635
- const tracks = succeeded.length > 0 ? (await deps.runTracking(succeeded)).tracks : [];
15636
- const complete = !stoppedOnBudget && framesFailed === 0;
15637
- deps.logger.info("replay: run finished", {
15638
- tags,
15639
- meta: {
15640
- framesPlanned: frames.length,
15641
- framesRan: succeeded.length,
15642
- framesFailed,
15643
- complete,
15644
- tracks: tracks.length
15645
- }
15646
- });
15647
15210
  return {
15648
- kind: succeeded.length === 0 ? "no-frame-ran" : "ran",
15649
- deviceId: deps.deviceId,
15650
- nodeId: deps.nodeId,
15651
- complete,
15652
- tracks,
15653
- discarded,
15654
- counters: { framesPlanned: frames.length, framesRan: succeeded.length, framesFailed }
15211
+ kind: "ok",
15212
+ parts,
15213
+ instantsWithoutFootage: withoutFootage,
15214
+ truncatedBySafetyCap: truncated
15655
15215
  };
15656
15216
  }
15657
15217
 
15658
- // src/replay/replay-wiring.ts
15659
- function clipIdOf(source) {
15660
- return source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`;
15661
- }
15662
- async function orchestrateReplayRun(deps, input) {
15663
- const tags = { deviceId: input.deviceId };
15664
- const base = await deps.resolveBaseTree(input.deviceId);
15665
- const plan = buildReplayPlan(base, input.overlay);
15666
- if (plan.ignoredOverlays.length > 0) {
15667
- deps.logger.warn("replay: overlay named a step the resolved tree does not run", {
15668
- tags,
15669
- meta: { ignoredOverlays: plan.ignoredOverlays }
15670
- });
15218
+ // src/session-replay/cadence.ts
15219
+ var DEFAULT_DETECTION_FPS = 10;
15220
+ var MIN_DETECTION_FPS = 1;
15221
+ var MAX_DETECTION_FPS = 30;
15222
+ var MIN_GAPS_TO_MEASURE = 3;
15223
+ function clampFps(fps) {
15224
+ return Math.min(MAX_DETECTION_FPS, Math.max(MIN_DETECTION_FPS, fps));
15225
+ }
15226
+ function resolveReplayCadence(requestedFps, positionTimesByTrack) {
15227
+ if (requestedFps !== void 0) return { fps: clampFps(requestedFps), source: "operator" };
15228
+ const gaps = [];
15229
+ for (const times of positionTimesByTrack) {
15230
+ for (let i = 1; i < times.length; i++) {
15231
+ const prev = times[i - 1];
15232
+ const cur = times[i];
15233
+ if (prev !== void 0 && cur !== void 0 && cur > prev) gaps.push(cur - prev);
15234
+ }
15235
+ }
15236
+ if (gaps.length < MIN_GAPS_TO_MEASURE) return { fps: DEFAULT_DETECTION_FPS, source: "default" };
15237
+ const sorted = gaps.toSorted((a, b) => a - b);
15238
+ const quartile = sorted[Math.floor((sorted.length - 1) / 4)] ?? 0;
15239
+ if (!(quartile > 0)) return { fps: DEFAULT_DETECTION_FPS, source: "default" };
15240
+ return { fps: clampFps(Math.round(1e3 / quartile)), source: "stored-positions" };
15241
+ }
15242
+
15243
+ // src/session-replay/compare.ts
15244
+ var MATCH_IOU_MIN = 0.5;
15245
+ var RELAXED_IOU_MIN = 0.3;
15246
+ var MAX_INTERPOLATION_GAP_MS = 5e3;
15247
+ function iou(a, b) {
15248
+ const x1 = Math.max(a.x, b.x);
15249
+ const y1 = Math.max(a.y, b.y);
15250
+ const x2 = Math.min(a.x + a.w, b.x + b.w);
15251
+ const y2 = Math.min(a.y + a.h, b.y + b.h);
15252
+ const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
15253
+ if (inter === 0) return 0;
15254
+ const union2 = a.w * a.h + b.w * b.h - inter;
15255
+ return union2 > 0 ? inter / union2 : 0;
15256
+ }
15257
+ function lerp(a, b, f) {
15258
+ return a + (b - a) * f;
15259
+ }
15260
+ function entry(p, t, basis) {
15261
+ return { ...p, t, basis };
15262
+ }
15263
+ function trackAt(track, t, halfStepMs) {
15264
+ let prev;
15265
+ let next;
15266
+ let nearest;
15267
+ for (const p of track.positions) {
15268
+ if (p.t <= t) prev = p;
15269
+ if (p.t >= t && next === void 0) next = p;
15270
+ if (nearest === void 0 || Math.abs(p.t - t) < Math.abs(nearest.t - t)) nearest = p;
15671
15271
  }
15672
- const preflight = await deps.api.pipelineExecutor.validatePipeline.query({
15673
- steps: [...plan.steps]
15674
- });
15675
- const preflightVerdict = readPreflight(preflight);
15676
- if (preflightVerdict.kind === "refused") {
15677
- deps.logger.warn("replay: run refused at preflight", {
15678
- tags,
15679
- meta: { reason: preflightVerdict.reason, detail: preflightVerdict.detail }
15680
- });
15681
- return { kind: "refused", reason: `${preflightVerdict.reason}: ${preflightVerdict.detail}` };
15272
+ if (nearest !== void 0 && Math.abs(nearest.t - t) <= halfStepMs) {
15273
+ return entry(nearest, nearest.t, "exact");
15682
15274
  }
15683
- const framePlan = planReplayFrames(input.source, input.fps, input.maxFrames);
15684
- if (framePlan.kind === "refused") {
15685
- deps.logger.warn("replay: run refused \u2014 empty window", { tags });
15686
- return { kind: "refused", reason: framePlan.reason };
15275
+ if (t < track.firstSeen || t > track.lastSeen || nearest === void 0) return null;
15276
+ if (prev !== void 0 && next !== void 0 && next.t - prev.t <= MAX_INTERPOLATION_GAP_MS) {
15277
+ const f = next.t === prev.t ? 0 : (t - prev.t) / (next.t - prev.t);
15278
+ return {
15279
+ trackId: track.trackId,
15280
+ className: track.className,
15281
+ t,
15282
+ box: {
15283
+ x: lerp(prev.box.x, next.box.x, f),
15284
+ y: lerp(prev.box.y, next.box.y, f),
15285
+ w: lerp(prev.box.w, next.box.w, f),
15286
+ h: lerp(prev.box.h, next.box.h, f)
15287
+ },
15288
+ score: prev.score !== null && next.score !== null ? lerp(prev.score, next.score, f) : null,
15289
+ basis: "interpolated"
15290
+ };
15687
15291
  }
15688
- if (framePlan.truncated) {
15689
- deps.logger.warn("replay: frame plan truncated by maxFrames", {
15690
- tags,
15691
- meta: { maxFrames: input.maxFrames, planned: framePlan.instants.length }
15692
- });
15292
+ return entry(nearest, t, "uncertain");
15293
+ }
15294
+ function referenceAt(tracks, t, halfStepMs) {
15295
+ const entries = [];
15296
+ let live = false;
15297
+ for (const track of tracks) {
15298
+ if (t >= track.firstSeen && t <= track.lastSeen) live = true;
15299
+ const e = trackAt(track, t, halfStepMs);
15300
+ if (e !== null) {
15301
+ entries.push(e);
15302
+ live = true;
15303
+ }
15693
15304
  }
15694
- const sourceFetch = await deps.fetchSourceBytes(input.source);
15695
- if (sourceFetch.kind === "refused") {
15696
- deps.logger.warn("replay: run refused \u2014 could not fetch source bytes", {
15697
- tags,
15698
- meta: { reason: sourceFetch.reason }
15305
+ return { live, entries };
15306
+ }
15307
+ function sameClass(a, b) {
15308
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
15309
+ }
15310
+ function barFor(basis, iouMin) {
15311
+ if (basis === "exact") return iouMin;
15312
+ if (basis === "interpolated") return Math.min(iouMin, RELAXED_IOU_MIN);
15313
+ return 0;
15314
+ }
15315
+ function pairUp(replay, stored, accept, bar) {
15316
+ const candidates = [];
15317
+ replay.forEach((r, ri) => {
15318
+ stored.forEach((s, si) => {
15319
+ if (!accept(r, s)) return;
15320
+ const v = iou(r.box, s.box);
15321
+ if (v >= bar(s)) candidates.push({ r: ri, s: si, iou: v });
15699
15322
  });
15700
- return { kind: "refused", reason: sourceFetch.reason };
15701
- }
15702
- const sourceBytes = sourceFetch.bytes;
15703
- const first = framePlan.instants[0] ?? 0;
15704
- const last = framePlan.instants[framePlan.instants.length - 1] ?? first;
15705
- const startSec = (first - sourceFetch.startMs) / 1e3;
15706
- const durationSec = Math.max(1 / input.fps, (last - first) / 1e3 + 1 / input.fps);
15707
- const args = buildReplaySequenceArgs({
15708
- startSec,
15709
- durationSec,
15710
- fps: input.fps,
15711
- maxWidth: input.maxWidth
15712
15323
  });
15713
- const mjpeg = await deps.decoder.decodeSequence(args, sourceBytes);
15714
- const jpegFrames = splitMjpegStream(mjpeg);
15715
- const frames = [];
15716
- for (let i = 0; i < framePlan.instants.length; i++) {
15717
- const timestamp = framePlan.instants[i];
15718
- const image = jpegFrames[i];
15719
- if (timestamp === void 0 || image === void 0) continue;
15720
- frames.push({ timestamp, image });
15721
- }
15722
- if (frames.length < framePlan.instants.length) {
15723
- deps.logger.warn("replay: decode produced fewer frames than planned", {
15724
- tags,
15725
- meta: { planned: framePlan.instants.length, decoded: frames.length }
15324
+ const ranked = candidates.toSorted((a, b) => b.iou - a.iou);
15325
+ const usedR = /* @__PURE__ */ new Set();
15326
+ const usedS = /* @__PURE__ */ new Set();
15327
+ const pairs = [];
15328
+ for (const c of ranked) {
15329
+ if (usedR.has(c.r) || usedS.has(c.s)) continue;
15330
+ const r = replay[c.r];
15331
+ const s = stored[c.s];
15332
+ if (r === void 0 || s === void 0) continue;
15333
+ usedR.add(c.r);
15334
+ usedS.add(c.s);
15335
+ pairs.push({
15336
+ replay: r,
15337
+ stored: s,
15338
+ iou: c.iou,
15339
+ scoreDelta: s.score === null ? null : r.score - s.score
15726
15340
  });
15727
15341
  }
15728
- const verdict = await runReplay(
15729
- {
15730
- deviceId: input.deviceId,
15731
- nodeId: input.nodeId,
15732
- runPipeline: async (call) => deps.api.pipelineExecutor.runPipeline.mutate({
15733
- nodeId: call.nodeId,
15734
- steps: [...plan.steps],
15735
- // Copy into a fresh `Uint8Array<ArrayBuffer>` — `mjpeg-split.ts`
15736
- // hands back a `Buffer` view (`Uint8Array<ArrayBufferLike>`), and
15737
- // the generated cap input is typed against the narrower
15738
- // `ArrayBuffer` backing.
15739
- image: new Uint8Array(call.image),
15740
- deviceId: input.deviceId,
15741
- ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
15742
- }),
15743
- // THE cross-addon call this whole track exists for: the FrameProcessor
15744
- // pass lives in `addon-post-analysis`, reached only through this cap
15745
- // method — see the file header.
15746
- runTracking: async (tracked) => deps.api.pipelineAnalytics.runReplayFrameProcessor.mutate({
15747
- deviceId: input.deviceId,
15748
- source: "pipeline",
15749
- frames: [...tracked]
15750
- }),
15751
- logger: deps.logger
15752
- },
15753
- frames
15342
+ return {
15343
+ pairs,
15344
+ restReplay: replay.filter((_r, i) => !usedR.has(i)),
15345
+ restStored: stored.filter((_s, i) => !usedS.has(i))
15346
+ };
15347
+ }
15348
+ function compareFrame(t, replay, reference, iouMin = MATCH_IOU_MIN) {
15349
+ const same = pairUp(
15350
+ replay,
15351
+ reference.entries,
15352
+ (r, s) => sameClass(r.className, s.className),
15353
+ (s) => barFor(s.basis, iouMin)
15354
+ );
15355
+ const changed = pairUp(
15356
+ same.restReplay,
15357
+ same.restStored,
15358
+ (_r, s) => s.basis !== "uncertain",
15359
+ (s) => barFor(s.basis, iouMin)
15754
15360
  );
15755
- const now = deps.now ?? Date.now;
15756
- const newId = deps.newId ?? (() => `replay-${now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
15757
- const replayRunId = newId();
15758
- const record2 = {
15759
- id: replayRunId,
15760
- deviceId: input.deviceId,
15761
- nodeId: input.nodeId,
15762
- clipId: clipIdOf(input.source),
15763
- createdAtMs: now(),
15764
- source: input.source,
15765
- plan,
15766
- verdict
15767
- };
15768
- await deps.store.save(record2);
15769
- return { kind: "ran", replayRunId, verdict };
15361
+ return {
15362
+ t,
15363
+ referenced: reference.live,
15364
+ matched: same.pairs,
15365
+ classChanged: changed.pairs,
15366
+ added: reference.live ? changed.restReplay : [],
15367
+ unreferenced: reference.live ? [] : changed.restReplay,
15368
+ missed: changed.restStored
15369
+ };
15370
+ }
15371
+ function percentile(sorted, q) {
15372
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1));
15373
+ return sorted[idx] ?? 0;
15374
+ }
15375
+ function summarizeReplay(frames, inferenceMs) {
15376
+ const perClass = /* @__PURE__ */ new Map();
15377
+ const bump = (cls, field) => {
15378
+ const key = cls.trim().toLowerCase();
15379
+ const e = perClass.get(key) ?? {
15380
+ replay: 0,
15381
+ stored: 0,
15382
+ matched: 0,
15383
+ added: 0,
15384
+ missed: 0,
15385
+ unreferenced: 0,
15386
+ interpolated: 0
15387
+ };
15388
+ e[field] += 1;
15389
+ perClass.set(key, e);
15390
+ };
15391
+ const deltas = [];
15392
+ let matched = 0;
15393
+ let classChanged = 0;
15394
+ let added = 0;
15395
+ let missed = 0;
15396
+ let unreferenced = 0;
15397
+ let interpolated = 0;
15398
+ const noteBasis = (s) => {
15399
+ if (s.basis === "exact") return;
15400
+ interpolated += 1;
15401
+ bump(s.className, "interpolated");
15402
+ };
15403
+ for (const f of frames) {
15404
+ for (const p of f.matched) {
15405
+ matched += 1;
15406
+ bump(p.replay.className, "replay");
15407
+ bump(p.stored.className, "stored");
15408
+ bump(p.replay.className, "matched");
15409
+ noteBasis(p.stored);
15410
+ if (p.scoreDelta !== null) deltas.push(p.scoreDelta);
15411
+ }
15412
+ for (const p of f.classChanged) {
15413
+ classChanged += 1;
15414
+ bump(p.replay.className, "replay");
15415
+ bump(p.stored.className, "stored");
15416
+ noteBasis(p.stored);
15417
+ }
15418
+ for (const r of f.added) {
15419
+ added += 1;
15420
+ bump(r.className, "replay");
15421
+ bump(r.className, "added");
15422
+ }
15423
+ for (const r of f.unreferenced) {
15424
+ unreferenced += 1;
15425
+ bump(r.className, "replay");
15426
+ bump(r.className, "unreferenced");
15427
+ }
15428
+ for (const s of f.missed) {
15429
+ missed += 1;
15430
+ bump(s.className, "stored");
15431
+ bump(s.className, "missed");
15432
+ noteBasis(s);
15433
+ }
15434
+ }
15435
+ const sortedDeltas = deltas.toSorted((a, b) => a - b);
15436
+ const sortedInfer = [...inferenceMs].toSorted((a, b) => a - b);
15437
+ return {
15438
+ totals: { matched, classChanged, added, missed, unreferenced, interpolated },
15439
+ perClass: Object.fromEntries(
15440
+ [...perClass.entries()].toSorted(([a], [b]) => a.localeCompare(b))
15441
+ ),
15442
+ scoreDelta: sortedDeltas.length === 0 ? null : {
15443
+ mean: sortedDeltas.reduce((s, v) => s + v, 0) / sortedDeltas.length,
15444
+ p10: percentile(sortedDeltas, 0.1),
15445
+ p50: percentile(sortedDeltas, 0.5),
15446
+ p90: percentile(sortedDeltas, 0.9)
15447
+ },
15448
+ inferenceMs: sortedInfer.length === 0 ? null : {
15449
+ p50: percentile(sortedInfer, 0.5),
15450
+ p95: percentile(sortedInfer, 0.95),
15451
+ count: sortedInfer.length
15452
+ }
15453
+ };
15770
15454
  }
15771
15455
 
15772
- // src/replay/replay-diff.ts
15773
- function iou(a, b) {
15774
- const x1 = Math.max(a.x, b.x);
15775
- const y1 = Math.max(a.y, b.y);
15776
- const x2 = Math.min(a.x + a.w, b.x + b.w);
15777
- const y2 = Math.min(a.y + a.h, b.y + b.h);
15778
- const interW = Math.max(0, x2 - x1);
15779
- const interH = Math.max(0, y2 - y1);
15780
- const inter = interW * interH;
15781
- if (inter <= 0) return 0;
15782
- const union2 = a.w * a.h + b.w * b.h - inter;
15783
- return union2 <= 0 ? 0 : inter / union2;
15784
- }
15785
- function windowsOverlap(a, b) {
15786
- return a.firstSeenMs <= b.lastSeenMs && b.firstSeenMs <= a.lastSeenMs;
15787
- }
15788
- var TRACK_PAIR_MIN_IOU = 0.3;
15789
- function diffTracks(a, b) {
15790
- const bMatched = /* @__PURE__ */ new Set();
15791
- const lost = [];
15792
- for (const ta of a) {
15793
- let bestIdx = -1;
15794
- let bestIou = 0;
15795
- for (let i = 0; i < b.length; i++) {
15796
- if (bMatched.has(i)) continue;
15797
- const tb = b[i];
15798
- if (tb === void 0 || tb.className !== ta.className) continue;
15799
- if (!windowsOverlap(ta, tb)) continue;
15800
- const score = iou(ta.bbox, tb.bbox);
15801
- if (score >= TRACK_PAIR_MIN_IOU && score > bestIou) {
15802
- bestIou = score;
15803
- bestIdx = i;
15804
- }
15805
- }
15806
- if (bestIdx === -1) lost.push(ta);
15807
- else bMatched.add(bestIdx);
15808
- }
15809
- const gained = b.filter((_, i) => !bMatched.has(i));
15810
- return { gained, lost };
15811
- }
15812
- function diffKnobs(a, b) {
15813
- const stepIds = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
15814
- const changed = [];
15815
- for (const stepId of stepIds) {
15816
- const aStep = a[stepId] ?? {};
15817
- const bStep = b[stepId] ?? {};
15818
- const keys = /* @__PURE__ */ new Set([...Object.keys(aStep), ...Object.keys(bStep)]);
15819
- for (const key of keys) {
15820
- const from = aStep[key];
15821
- const to = bStep[key];
15822
- if (from !== to) changed.push({ stepId, key, from, to });
15823
- }
15824
- }
15825
- return changed;
15826
- }
15827
- function diffPromoted(discardedA, tracksB) {
15828
- const promoted = [];
15829
- for (const d of discardedA) {
15830
- const nowTracked = tracksB.some((t) => t.className === d.macroClass);
15831
- if (nowTracked)
15832
- promoted.push({ macroClass: d.macroClass, score: d.score, wasDiscardedBy: d.reason });
15833
- }
15834
- return promoted;
15835
- }
15836
- function diffReplayRuns(a, b) {
15837
- if (a.clipId !== b.clipId) return { kind: "incomparable", reason: "different-input" };
15838
- if (a.nodeId !== b.nodeId) return { kind: "incomparable", reason: "different-node" };
15839
- return {
15840
- kind: "compared",
15841
- changedKnobs: diffKnobs(a.settings, b.settings),
15842
- tracks: diffTracks(a.tracks, b.tracks),
15843
- promoted: diffPromoted(a.discarded, b.tracks)
15456
+ // src/session-replay/frame-plan.ts
15457
+ var REPLAY_CHUNK_MS = 1e4;
15458
+ function planReplayFrames(window, fps, maxFrames, chunkMs = REPLAY_CHUNK_MS) {
15459
+ const stepMs = 1e3 / fps;
15460
+ const instants = [];
15461
+ let truncated = false;
15462
+ for (let k = 0; ; k++) {
15463
+ const t = window.startMs + Math.round(k * stepMs);
15464
+ if (t >= window.endMs) break;
15465
+ if (instants.length >= maxFrames) {
15466
+ truncated = true;
15467
+ break;
15468
+ }
15469
+ instants.push(t);
15470
+ }
15471
+ const chunks = [];
15472
+ let current = [];
15473
+ let chunkStart = window.startMs;
15474
+ const flush = (toMs) => {
15475
+ if (current.length > 0) chunks.push({ fromMs: chunkStart, toMs, instants: current });
15476
+ current = [];
15844
15477
  };
15478
+ for (const t of instants) {
15479
+ while (t >= chunkStart + chunkMs) {
15480
+ flush(chunkStart + chunkMs);
15481
+ chunkStart += chunkMs;
15482
+ }
15483
+ current.push(t);
15484
+ }
15485
+ const last = instants[instants.length - 1];
15486
+ flush(
15487
+ truncated && last !== void 0 ? last + Math.round(stepMs) : Math.min(window.endMs, chunkStart + chunkMs)
15488
+ );
15489
+ return { chunks, planned: instants.length, truncated };
15845
15490
  }
15846
15491
 
15847
- // src/throughput/sustained-runner.ts
15848
- async function runSustainedLoop(opts) {
15849
- const concurrency = Math.max(1, Math.floor(opts.concurrency));
15850
- const start = opts.now();
15851
- const deadline = start + Math.max(0, opts.durationMs);
15852
- const callMsList = [];
15853
- let totalFrames = 0;
15854
- let totalBatches = 0;
15855
- let totalDetections = 0;
15856
- let detectionSamples = 0;
15857
- let errors = 0;
15858
- const worker = async () => {
15859
- while (!opts.shouldStop() && opts.now() < deadline) {
15860
- const t0 = opts.now();
15861
- try {
15862
- const out = await opts.dispatchBatch();
15863
- const t1 = opts.now();
15864
- callMsList.push(t1 - t0);
15865
- totalBatches += 1;
15866
- totalFrames += out.frameCount;
15867
- for (const c of out.detectionCounts) {
15868
- totalDetections += c;
15869
- detectionSamples += 1;
15870
- }
15871
- } catch {
15872
- errors += 1;
15873
- }
15492
+ // src/session-replay/replay-steps.ts
15493
+ var REPLAY_ROOT_STEP = "object-detection";
15494
+ function selectRootReplayStep(tree, requestedModelId) {
15495
+ const root = tree.steps.find((s) => s.addonId === REPLAY_ROOT_STEP);
15496
+ if (root === void 0 || !root.enabled) {
15497
+ return {
15498
+ kind: "refused",
15499
+ reason: "no-root-step",
15500
+ detail: `the camera does not run ${REPLAY_ROOT_STEP} as an enabled root step`
15501
+ };
15502
+ }
15503
+ const cameraModelId = root.modelId ?? null;
15504
+ const modelId = requestedModelId ?? cameraModelId;
15505
+ if (modelId === null || modelId.length === 0) {
15506
+ return {
15507
+ kind: "refused",
15508
+ reason: "model-unpinned",
15509
+ detail: `the camera's ${REPLAY_ROOT_STEP} names no model (it runs the node's format default); pick the model to replay explicitly`
15510
+ };
15511
+ }
15512
+ return {
15513
+ kind: "ok",
15514
+ cameraModelId,
15515
+ step: {
15516
+ addonId: root.addonId,
15517
+ modelId,
15518
+ enabled: true,
15519
+ ...root.settings !== void 0 ? { settings: root.settings } : {},
15520
+ children: []
15874
15521
  }
15875
15522
  };
15876
- await Promise.all(Array.from({ length: concurrency }, () => worker()));
15877
- const wallMs = opts.now() - start;
15523
+ }
15524
+
15525
+ // src/session-replay/session-window.ts
15526
+ var SESSION_PREROLL_MS = 5e3;
15527
+ var SESSION_TAIL_MS = 3e4;
15528
+ var SESSION_MAX_WINDOW_MS = 10 * 6e4;
15529
+ function motionSessionWindow(session, camera = {}, opts = {}) {
15530
+ if (session.durationMs === null) {
15531
+ return {
15532
+ kind: "refused",
15533
+ reason: "session-open",
15534
+ detail: `motion session ${session.id} is still open \u2014 its end is not known yet`
15535
+ };
15536
+ }
15537
+ if (session.durationMs === void 0) {
15538
+ return {
15539
+ kind: "refused",
15540
+ reason: "session-length-unknown",
15541
+ detail: `motion session ${session.id} predates the episode duration column \u2014 its length is unknown`
15542
+ };
15543
+ }
15544
+ const motionEndMs = session.timestamp + session.durationMs;
15545
+ const prerollSource = camera.preRollInferenceEnabled === false ? "camera" : "default";
15546
+ const prerollMs = camera.preRollInferenceEnabled === false ? 0 : SESSION_PREROLL_MS;
15547
+ const tailSource = camera.motionCooldownMs !== void 0 ? "camera" : "default";
15548
+ const tailMs = camera.motionCooldownMs ?? SESSION_TAIL_MS;
15549
+ const startMs = session.timestamp - prerollMs;
15550
+ const endMs = motionEndMs + tailMs;
15551
+ const maxWindowMs = opts.maxWindowMs ?? SESSION_MAX_WINDOW_MS;
15552
+ if (endMs - startMs > maxWindowMs) {
15553
+ return {
15554
+ kind: "refused",
15555
+ reason: "window-too-long",
15556
+ detail: `motion session ${session.id} spans ${Math.round((endMs - startMs) / 1e3)} s \u2014 longer than the ${Math.round(maxWindowMs / 1e3)} s a replay reads`
15557
+ };
15558
+ }
15878
15559
  return {
15879
- callMsList,
15880
- totalFrames,
15881
- totalBatches,
15882
- totalDetections,
15883
- detectionSamples,
15884
- errors,
15885
- wallMs,
15886
- aborted: opts.shouldStop()
15560
+ kind: "window",
15561
+ motionEventId: session.id,
15562
+ motionStartMs: session.timestamp,
15563
+ motionEndMs,
15564
+ startMs,
15565
+ endMs,
15566
+ prerollSource,
15567
+ tailSource
15887
15568
  };
15888
15569
  }
15889
15570
 
15890
- // src/throughput/throughput-stats.ts
15891
- var EMPTY_LATENCY = { p50: 0, p95: 0, p99: 0, mean: 0, min: 0, max: 0 };
15892
- function percentile(sortedAsc, p) {
15893
- const n = sortedAsc.length;
15894
- if (n === 0) return 0;
15895
- const clamped = p < 0 ? 0 : p > 1 ? 1 : p;
15896
- if (clamped <= 0) return sortedAsc[0] ?? 0;
15897
- if (clamped >= 1) return sortedAsc[n - 1] ?? 0;
15898
- const rank = Math.ceil(clamped * n);
15899
- const idx = Math.min(n - 1, Math.max(0, rank - 1));
15900
- return sortedAsc[idx] ?? 0;
15901
- }
15902
- function computeLatencyStats(samplesMs) {
15903
- if (samplesMs.length === 0) return EMPTY_LATENCY;
15904
- const sorted = [...samplesMs].sort((a, b) => a - b);
15905
- const sum = sorted.reduce((acc, x) => acc + x, 0);
15571
+ // src/session-replay/stored-reference.ts
15572
+ var UNKNOWN_MODEL = "unknown";
15573
+ function buildStoredReference(tracks, window) {
15574
+ const refs = [];
15575
+ const models = /* @__PURE__ */ new Map();
15576
+ const sizes = /* @__PURE__ */ new Map();
15577
+ const timesByTrack = [];
15578
+ let recoveryExcluded = 0;
15579
+ let withoutGeometry = 0;
15580
+ let trackCount = 0;
15581
+ for (const track of tracks) {
15582
+ const inWindow = track.positions.filter(
15583
+ (p) => p.timestamp >= window.startMs && p.timestamp < window.endMs
15584
+ );
15585
+ const aliveInWindow = track.firstSeen < window.endMs && track.lastSeen >= window.startMs;
15586
+ if (inWindow.length === 0 && !aliveInWindow) continue;
15587
+ trackCount += 1;
15588
+ const model = track.modelId ?? UNKNOWN_MODEL;
15589
+ models.set(model, (models.get(model) ?? 0) + 1);
15590
+ const fw = track.frameWidth;
15591
+ const fh = track.frameHeight;
15592
+ const hasGeometry = fw !== void 0 && fh !== void 0 && fw > 0 && fh > 0;
15593
+ if (hasGeometry) {
15594
+ const key = `${fw}x${fh}`;
15595
+ const entry2 = sizes.get(key) ?? { width: fw, height: fh, count: 0 };
15596
+ entry2.count += 1;
15597
+ sizes.set(key, entry2);
15598
+ }
15599
+ const times = [];
15600
+ const positions = [];
15601
+ for (const p of inWindow) {
15602
+ if (p.source === "recovery") {
15603
+ recoveryExcluded += 1;
15604
+ continue;
15605
+ }
15606
+ times.push(p.timestamp);
15607
+ if (!hasGeometry) {
15608
+ withoutGeometry += 1;
15609
+ continue;
15610
+ }
15611
+ positions.push({
15612
+ trackId: track.trackId,
15613
+ className: track.className,
15614
+ t: p.timestamp,
15615
+ box: { x: p.bbox.x / fw, y: p.bbox.y / fh, w: p.bbox.w / fw, h: p.bbox.h / fh },
15616
+ score: p.score ?? null
15617
+ });
15618
+ }
15619
+ timesByTrack.push(times.toSorted((a, b) => a - b));
15620
+ if (hasGeometry) {
15621
+ const sorted = positions.toSorted((a, b) => a.t - b.t);
15622
+ refs.push({
15623
+ trackId: track.trackId,
15624
+ className: track.className,
15625
+ firstSeen: Math.min(track.firstSeen, sorted[0]?.t ?? track.firstSeen),
15626
+ lastSeen: Math.max(track.lastSeen, sorted[sorted.length - 1]?.t ?? track.lastSeen),
15627
+ positions: sorted
15628
+ });
15629
+ }
15630
+ }
15631
+ const commonest = [...sizes.values()].toSorted((a, b) => b.count - a.count)[0];
15906
15632
  return {
15907
- p50: percentile(sorted, 0.5),
15908
- p95: percentile(sorted, 0.95),
15909
- p99: percentile(sorted, 0.99),
15910
- mean: sum / sorted.length,
15911
- min: sorted[0] ?? 0,
15912
- max: sorted[sorted.length - 1] ?? 0
15633
+ tracks: refs,
15634
+ trackCount,
15635
+ recoveryExcluded,
15636
+ withoutGeometry,
15637
+ models: Object.fromEntries(models),
15638
+ frameSize: commonest ? { width: commonest.width, height: commonest.height } : null,
15639
+ positionTimesByTrack: timesByTrack
15913
15640
  };
15914
15641
  }
15915
- function computeFps(totalFrames, wallMs) {
15916
- if (wallMs <= 0) return 0;
15917
- return totalFrames * 1e3 / wallMs;
15642
+
15643
+ // src/session-replay/run-session-replay.ts
15644
+ var TRACK_LOOKBACK_MS = 12e4;
15645
+ var DEFAULT_REPLAY_WIDTH = 1280;
15646
+ var SUPPRESSED_BIRTH_PROXIMITY_MS = 2e3;
15647
+ var MAX_REPORTED_FAILURES = 20;
15648
+ var MAX_CONSECUTIVE_FAILURES = 10;
15649
+ var STORED_TRACKS_READ_LIMIT = 1e3;
15650
+ var REAL_PACE = {
15651
+ now: () => Date.now(),
15652
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
15653
+ };
15654
+ var PIN_REFUSALS = ["model-not-servable", "device-not-servable"];
15655
+ function pinRefusalOf(err) {
15656
+ const message = err instanceof Error ? err.message : String(err);
15657
+ for (const reason of PIN_REFUSALS) {
15658
+ if (message.includes(`${reason}:`)) return { reason, detail: message };
15659
+ }
15660
+ return null;
15661
+ }
15662
+ function toReplayBoxes(frame) {
15663
+ const w = frame.width;
15664
+ const h = frame.height;
15665
+ if (!(w > 0 && h > 0)) return [];
15666
+ return frame.detections.filter((d) => d.kind === "first-level").map((d) => ({
15667
+ className: d.macroClass,
15668
+ score: d.score,
15669
+ box: { x: d.bbox.x / w, y: d.bbox.y / h, w: d.bbox.width / w, h: d.bbox.height / h }
15670
+ }));
15671
+ }
15672
+ function sameClass2(a, b) {
15673
+ return a.trim().toLowerCase() === b.trim().toLowerCase();
15918
15674
  }
15919
- function computeDetsPerSec(totalDetections, wallMs) {
15920
- if (wallMs <= 0) return 0;
15921
- return totalDetections * 1e3 / wallMs;
15675
+ async function runSessionReplay(deps, request, run) {
15676
+ const tags = { deviceId: request.deviceId };
15677
+ const refuse = (reason, detail) => {
15678
+ deps.logger.warn("session replay refused", {
15679
+ tags,
15680
+ meta: { reason, detail, motionEventId: request.motionEventId, nodeId: request.nodeId }
15681
+ });
15682
+ return { kind: "refused", reason, detail };
15683
+ };
15684
+ run.report({ phase: "preparing", framesPlanned: 0, framesDone: 0 });
15685
+ const session = await deps.findMotionSession(
15686
+ request.deviceId,
15687
+ request.motionEventId,
15688
+ request.motionStartMs
15689
+ );
15690
+ if (session === null) {
15691
+ return refuse(
15692
+ "session-not-found",
15693
+ `no stored motion session ${request.motionEventId} for camera ${request.deviceId}`
15694
+ );
15695
+ }
15696
+ const window = motionSessionWindow(session, await deps.cameraSessionSettings(request.deviceId));
15697
+ if (window.kind === "refused") return refuse(window.reason, window.detail);
15698
+ const tree = await deps.resolveCameraTree(request.deviceId, request.nodeId);
15699
+ const selection = selectRootReplayStep(tree, request.modelId);
15700
+ if (selection.kind === "refused") return refuse(selection.reason, selection.detail);
15701
+ const step = selection.step;
15702
+ const modelId = step.modelId ?? "";
15703
+ const validation = await deps.validate([step], request.nodeId, request.deviceKey);
15704
+ const pool = `${request.nodeId}/${request.deviceKey ?? "node default"}`;
15705
+ if (!validation.ok) {
15706
+ return refuse(
15707
+ "device-not-servable",
15708
+ `${pool} cannot run ${step.addonId}: ${validation.issues.map((i) => i.detail).join("; ")}`
15709
+ );
15710
+ }
15711
+ const substitution = validation.substitutions[0];
15712
+ if (substitution !== void 0) {
15713
+ return refuse(
15714
+ "model-not-servable",
15715
+ `${pool} would run ${substitution.running} instead of ${substitution.chosen} (${substitution.format})`
15716
+ );
15717
+ }
15718
+ if (!await deps.isRecording(request.deviceId)) {
15719
+ return refuse("no-recording", `camera ${request.deviceId} does not have recording enabled`);
15720
+ }
15721
+ const [tracks, births] = await Promise.all([
15722
+ deps.listStoredTracks(request.deviceId, window.startMs - TRACK_LOOKBACK_MS, window.endMs),
15723
+ deps.listBirthDecisions(request.deviceId, window.startMs)
15724
+ ]);
15725
+ const reference = buildStoredReference(tracks, window);
15726
+ const readTruncated = tracks.length >= STORED_TRACKS_READ_LIMIT;
15727
+ if (readTruncated) {
15728
+ deps.logger.warn(
15729
+ "session replay: stored-track read hit its limit \u2014 the reference may be incomplete",
15730
+ {
15731
+ tags,
15732
+ meta: { limit: STORED_TRACKS_READ_LIMIT, motionEventId: request.motionEventId }
15733
+ }
15734
+ );
15735
+ }
15736
+ const cadence = resolveReplayCadence(request.fps, reference.positionTimesByTrack);
15737
+ const frameSize = reference.frameSize !== null ? { width: reference.frameSize.width, source: "stored-tracks" } : { width: DEFAULT_REPLAY_WIDTH, source: "default" };
15738
+ const windowBirths = births.filter((b) => b.at >= window.startMs && b.at < window.endMs);
15739
+ const suppressedBirths = windowBirths.filter((b) => b.verdict === "suppressed");
15740
+ const liveVerdicts = {};
15741
+ for (const b of windowBirths) liveVerdicts[b.verdict] = (liveVerdicts[b.verdict] ?? 0) + 1;
15742
+ const plan = planReplayFrames(window, cadence.fps, request.maxFrames);
15743
+ if (plan.truncated) {
15744
+ deps.logger.warn("session replay: frame plan truncated by maxFrames", {
15745
+ tags,
15746
+ meta: { maxFrames: request.maxFrames, motionEventId: request.motionEventId }
15747
+ });
15748
+ }
15749
+ const stepMs = 1e3 / cadence.fps;
15750
+ const halfStepMs = stepMs / 2;
15751
+ const timeline = [];
15752
+ const comparisons = [];
15753
+ const inferenceMs = [];
15754
+ const failures = [];
15755
+ let decoded = 0;
15756
+ let ran = 0;
15757
+ let failed = 0;
15758
+ let consecutiveFailures = 0;
15759
+ let lastCallAt = null;
15760
+ const pace = deps.pace ?? REAL_PACE;
15761
+ let withoutFootage = 0;
15762
+ let notDecoded = 0;
15763
+ let truncatedBySafetyCap = false;
15764
+ let nearSuppressedBirth = 0;
15765
+ let positionsCompared = 0;
15766
+ let partsRead = 0;
15767
+ const progress = () => run.report({ phase: "replaying", framesPlanned: plan.planned, framesDone: ran + failed });
15768
+ progress();
15769
+ for (const chunk of plan.chunks) {
15770
+ if (run.signal.aborted) break;
15771
+ const recording = await deps.readRecording(request.deviceId, chunk.instants, stepMs);
15772
+ if (recording.kind === "refused") {
15773
+ withoutFootage += chunk.instants.length;
15774
+ deps.logger.warn("session replay: no footage for a stretch of the session", {
15775
+ tags,
15776
+ meta: { fromMs: chunk.fromMs, toMs: chunk.toMs, detail: recording.detail }
15777
+ });
15778
+ continue;
15779
+ }
15780
+ withoutFootage += recording.instantsWithoutFootage;
15781
+ partsRead += recording.parts.length;
15782
+ if (recording.truncatedBySafetyCap) truncatedBySafetyCap = true;
15783
+ for (const part of recording.parts) {
15784
+ if (run.signal.aborted) break;
15785
+ const first = part.instants[0];
15786
+ if (first === void 0) continue;
15787
+ const frames = await deps.decode(part.bytes, {
15788
+ startSec: (first - part.gopStartMs) / 1e3,
15789
+ durationSec: part.instants.length / cadence.fps,
15790
+ fps: cadence.fps,
15791
+ maxWidth: frameSize.width
15792
+ });
15793
+ decoded += Math.min(frames.length, part.instants.length);
15794
+ if (frames.length < part.instants.length) {
15795
+ notDecoded += part.instants.length - frames.length;
15796
+ deps.logger.warn("session replay: decode produced fewer frames than planned", {
15797
+ tags,
15798
+ meta: { planned: part.instants.length, decoded: frames.length, fromMs: first }
15799
+ });
15800
+ }
15801
+ for (let k = 0; k < part.instants.length; k++) {
15802
+ if (run.signal.aborted) break;
15803
+ const t = part.instants[k];
15804
+ const image = frames[k];
15805
+ if (t === void 0 || image === void 0) break;
15806
+ if (lastCallAt !== null) {
15807
+ const wait = lastCallAt + stepMs - pace.now();
15808
+ if (wait > 0) await pace.sleep(wait);
15809
+ }
15810
+ lastCallAt = pace.now();
15811
+ let result;
15812
+ try {
15813
+ result = await deps.runRootDetector({
15814
+ nodeId: request.nodeId,
15815
+ ...request.deviceKey !== void 0 ? { deviceKey: request.deviceKey } : {},
15816
+ steps: [step],
15817
+ image,
15818
+ deviceId: request.deviceId,
15819
+ t
15820
+ });
15821
+ } catch (err) {
15822
+ const pinRefusal = pinRefusalOf(err);
15823
+ if (pinRefusal !== null) return refuse(pinRefusal.reason, pinRefusal.detail);
15824
+ failed += 1;
15825
+ consecutiveFailures += 1;
15826
+ const error51 = err instanceof Error ? err.message : String(err);
15827
+ if (failures.length < MAX_REPORTED_FAILURES) failures.push({ t, error: error51 });
15828
+ deps.logger.warn("session replay: frame failed \u2014 counted, the run continues", {
15829
+ tags,
15830
+ meta: { t, error: error51, consecutiveFailures }
15831
+ });
15832
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
15833
+ return refuse(
15834
+ "pool-failing",
15835
+ `${pool} failed ${consecutiveFailures} frames in a row (last: ${error51}) \u2014 stopped rather than keep loading a live pool`
15836
+ );
15837
+ }
15838
+ progress();
15839
+ continue;
15840
+ }
15841
+ consecutiveFailures = 0;
15842
+ if (result.rootModelId !== modelId) {
15843
+ return refuse(
15844
+ "model-not-servable",
15845
+ result.rootModelId === void 0 ? `${pool} does not report the model it ran (an executor older than D646) \u2014 the pin cannot be verified` : `${pool} reported ${result.rootModelId} for a frame pinned to ${modelId}`
15846
+ );
15847
+ }
15848
+ ran += 1;
15849
+ const frameInferMs = result.debug?.totalInferenceMs;
15850
+ if (frameInferMs !== void 0) inferenceMs.push(frameInferMs);
15851
+ const replayBoxes = toReplayBoxes(result);
15852
+ const instantRef = referenceAt(reference.tracks, t, halfStepMs);
15853
+ positionsCompared += instantRef.entries.length;
15854
+ const cmp = compareFrame(t, replayBoxes, instantRef);
15855
+ comparisons.push(cmp);
15856
+ for (const unmatched of [...cmp.added, ...cmp.unreferenced]) {
15857
+ if (suppressedBirths.some(
15858
+ (b) => sameClass2(b.className, unmatched.className) && Math.abs(b.at - t) <= SUPPRESSED_BIRTH_PROXIMITY_MS
15859
+ )) {
15860
+ nearSuppressedBirth += 1;
15861
+ }
15862
+ }
15863
+ timeline.push({
15864
+ t,
15865
+ referenced: cmp.referenced,
15866
+ inferenceMs: frameInferMs ?? null,
15867
+ replay: [
15868
+ ...cmp.matched.map((p) => ({ ...p.replay, outcome: "matched" })),
15869
+ ...cmp.classChanged.map((p) => ({ ...p.replay, outcome: "class-changed" })),
15870
+ ...cmp.added.map((r) => ({ ...r, outcome: "added" })),
15871
+ ...cmp.unreferenced.map((r) => ({ ...r, outcome: "unreferenced" }))
15872
+ ],
15873
+ stored: [
15874
+ ...cmp.matched.map((p) => ({ ...p.stored, outcome: "matched" })),
15875
+ ...cmp.classChanged.map((p) => ({ ...p.stored, outcome: "class-changed" })),
15876
+ ...cmp.missed.map((s) => ({ ...s, outcome: "missed" }))
15877
+ ]
15878
+ });
15879
+ progress();
15880
+ }
15881
+ }
15882
+ }
15883
+ const aborted2 = run.signal.aborted;
15884
+ if (partsRead === 0 && !aborted2) {
15885
+ return refuse(
15886
+ "no-footage",
15887
+ `no recording covers any of the ${plan.planned} planned instants of motion session ${request.motionEventId}`
15888
+ );
15889
+ }
15890
+ if (aborted2) {
15891
+ deps.logger.info("session replay aborted \u2014 the report covers the frames that ran", {
15892
+ tags,
15893
+ meta: { ran, planned: plan.planned }
15894
+ });
15895
+ }
15896
+ run.report({ phase: "done", framesPlanned: plan.planned, framesDone: ran + failed });
15897
+ return {
15898
+ kind: "ran",
15899
+ report: {
15900
+ deviceId: request.deviceId,
15901
+ motionEventId: request.motionEventId,
15902
+ window: {
15903
+ startMs: window.startMs,
15904
+ endMs: window.endMs,
15905
+ motionStartMs: window.motionStartMs,
15906
+ motionEndMs: window.motionEndMs,
15907
+ prerollSource: window.prerollSource,
15908
+ tailSource: window.tailSource
15909
+ },
15910
+ pin: {
15911
+ nodeId: request.nodeId,
15912
+ deviceKey: request.deviceKey ?? null,
15913
+ modelId,
15914
+ cameraModelId: selection.cameraModelId
15915
+ },
15916
+ rootStep: step,
15917
+ cadence,
15918
+ frameSize,
15919
+ frames: {
15920
+ planned: plan.planned,
15921
+ decoded,
15922
+ ran,
15923
+ failed,
15924
+ withoutFootage,
15925
+ notDecoded,
15926
+ truncated: plan.truncated,
15927
+ truncatedBySafetyCap,
15928
+ aborted: aborted2
15929
+ },
15930
+ reference: {
15931
+ tracks: reference.trackCount,
15932
+ positionsCompared,
15933
+ recoveryExcluded: reference.recoveryExcluded,
15934
+ withoutGeometry: reference.withoutGeometry,
15935
+ models: reference.models,
15936
+ read: { tracks: tracks.length, truncated: readTruncated }
15937
+ },
15938
+ confirmation: { liveVerdicts, nearSuppressedBirth },
15939
+ summary: summarizeReplay(comparisons, inferenceMs),
15940
+ timeline,
15941
+ failures
15942
+ }
15943
+ };
15922
15944
  }
15923
- function computeAvgDetections(totalDetections, sampledFrames) {
15924
- if (sampledFrames <= 0) return 0;
15925
- return totalDetections / sampledFrames;
15945
+
15946
+ // src/session-replay/camera-settings.ts
15947
+ var FieldSchema = external_exports.object({
15948
+ key: external_exports.string().optional(),
15949
+ value: external_exports.unknown().optional(),
15950
+ fields: external_exports.array(external_exports.unknown()).optional(),
15951
+ tabs: external_exports.array(external_exports.object({ fields: external_exports.array(external_exports.unknown()).optional() }).loose()).optional()
15952
+ }).loose();
15953
+ var FormSchema = external_exports.object({
15954
+ sections: external_exports.array(external_exports.object({ fields: external_exports.array(external_exports.unknown()) }).loose())
15955
+ });
15956
+ function collect(fields, out) {
15957
+ for (const raw of fields) {
15958
+ const field = FieldSchema.safeParse(raw);
15959
+ if (!field.success) continue;
15960
+ if (field.data.key !== void 0 && "value" in field.data)
15961
+ out.set(field.data.key, field.data.value);
15962
+ if (field.data.fields !== void 0) collect(field.data.fields, out);
15963
+ for (const tab of field.data.tabs ?? []) collect(tab.fields ?? [], out);
15964
+ }
15965
+ }
15966
+ function readCameraSessionSettings(raw) {
15967
+ const form = FormSchema.safeParse(raw);
15968
+ if (!form.success) return {};
15969
+ const values = /* @__PURE__ */ new Map();
15970
+ for (const section of form.data.sections) collect(section.fields, values);
15971
+ const cooldown = values.get("motionCooldownMs");
15972
+ const preRoll = values.get("preRollInferenceEnabled");
15973
+ return {
15974
+ ...typeof cooldown === "number" && Number.isFinite(cooldown) && cooldown >= 0 ? { motionCooldownMs: cooldown } : {},
15975
+ ...typeof preRoll === "boolean" ? { preRollInferenceEnabled: preRoll } : {}
15976
+ };
15926
15977
  }
15927
15978
 
15928
15979
  // src/benchmark-addon.ts
15929
15980
  var KEY_BENCHMARK_HISTORY = "benchmarkHistory";
15930
15981
  var MAX_HISTORY_ENTRIES = 100;
15931
15982
  var EMPTY_HISTORY_JSON = "[]";
15932
- var BENCH_RUN_TIMEOUT_MS = Math.max(
15933
- 1e4,
15934
- Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 12e4
15983
+ var BENCH_RUN_TIMEOUT_MS = Math.min(
15984
+ 55e3,
15985
+ Math.max(1e4, Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 55e3)
15935
15986
  );
15987
+ var REPLAY_RECORDING_PROFILE = "high";
15988
+ var REPLAY_BIRTHS_LIMIT = 1e3;
15989
+ var REPLAY_DECODE_TIMEOUT_FLOOR_MS = 15e3;
15990
+ var REPLAY_DECODE_TIMEOUT_PER_SEC_MS = 3e3;
15991
+ var MOTION_SESSIONS_DEFAULT_SPAN_MS = 24 * 60 * 60 * 1e3;
15936
15992
  async function withHardTimeout(promise2, ms, label) {
15937
15993
  let timer;
15938
15994
  const timeout = new Promise((_resolve, reject) => {
@@ -15949,60 +16005,35 @@ async function withHardTimeout(promise2, ms, label) {
15949
16005
  }
15950
16006
  }
15951
16007
  var HistoryArraySchema = external_exports.array(HistoryEntrySchema);
15952
- var FIXTURE_META = {
15953
- "camera-sim-360p.h264": {
15954
- codec: "h264",
15955
- resolutionLabel: "360p",
15956
- description: "Synthetic 360p H.264 \u2014 light workload"
15957
- },
15958
- "camera-sim-1080p.h264": {
15959
- codec: "h264",
15960
- resolutionLabel: "1080p",
15961
- description: "Synthetic 1080p H.264 \u2014 production-like"
15962
- },
15963
- "camera-sim-1440p-h265.hevc": {
15964
- codec: "h265",
15965
- resolutionLabel: "1440p",
15966
- description: "Synthetic 1440p H.265 \u2014 hardware decode stress"
15967
- }
15968
- };
15969
- function resolveFixturesDir() {
15970
- const here = (0, import_node_url.fileURLToPath)(importMetaUrl);
15971
- const pkgRoot = path2.resolve(path2.dirname(here), "..");
15972
- return path2.join(pkgRoot, "assets", "fixtures");
15973
- }
15974
- var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16008
+ var BenchmarkAddon = class extends import_types4.BaseAddon {
15975
16009
  id = "benchmark";
15976
- /** Per-sessionId abort flags for in-flight decoder perf runs. */
15977
- decoderPerfAbortFlags = /* @__PURE__ */ new Map();
15978
- /** Per-sessionId abort flags for in-flight sustained-throughput runs. */
15979
- sustainedAbortFlags = /* @__PURE__ */ new Map();
15980
- /**
15981
- * In-memory cache of original JPEG bytes keyed by `frameId`. Populated by
15982
- * `cacheBenchFrame` (UI uploads JPEG once at bench start). Subsequent
15983
- * `runPipelineTest` calls ship only `frameId` — bench addon forwards the
15984
- * cached JPEG buffer as `image: Uint8Array` to pipelineExecutor.runPipeline
15985
- * (Moleculer MsgPack ships binary efficiently). Why JPEG and not raw RGB:
15986
- * cross-process MsgPack of a 2.7MB raw RGB frame (1280×720×3) is 13×
15987
- * heavier than the 200KB JPEG, and the pipeline addon's per-call sharp
15988
- * decode (~5ms) is cheaper than serialising the raw buffer twice.
15989
- * Production's broker→detection path is in-process (same group) so neither
15990
- * cost matters there.
15991
- */
15992
- frameCache = /* @__PURE__ */ new Map();
15993
- /** Periodic GC handle — clears expired frames every 60s. */
15994
- frameCacheSweeper = null;
15995
- /** The replay run collection — declared in `onInitialize`, before any
15996
- * `runReplayClip` call can save into it. See `replay/replay-store.ts`. */
15997
16010
  replayStore = null;
15998
- /** The replay's one ffmpeg spawn, bounded — see `replay/replay-decode.ts`. */
16011
+ /** The replay's ffmpeg spawns — the PINNED binary (`ctx.deps.ensureFfmpeg`, D540). */
15999
16012
  replayDecoder = new ReplayDecoder({
16000
- ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg"
16013
+ resolveFfmpegPath: () => this.ctx.deps.ensureFfmpeg()
16001
16014
  });
16002
- /** `replay:<StoredReplayRun>` — an in-memory index would drift from the
16003
- * store; this is a thin cache over `replayStore.list`, keyed for the
16004
- * diff action's two-id lookup. */
16005
- replayRunCache = /* @__PURE__ */ new Map();
16015
+ /**
16016
+ * Session replays, start-then-poll (D646). One at a time — a replay drives
16017
+ * one inference call after another on a node that also serves live
16018
+ * cameras — and the last few finished runs kept for a late poll.
16019
+ */
16020
+ replays = new import_types4.BackgroundRunRegistry(
16021
+ {
16022
+ maxConcurrent: 1,
16023
+ keepFinished: 10,
16024
+ onSettled: (status) => {
16025
+ if (status.state === "failed") {
16026
+ this.ctx.logger.warn("session replay failed", {
16027
+ meta: { runId: status.runId, error: status.error }
16028
+ });
16029
+ return;
16030
+ }
16031
+ const outcome = status.result;
16032
+ if (outcome === null || outcome.kind !== "ran") return;
16033
+ void this.persistReplay(status.runId, outcome.report.pin.nodeId, outcome);
16034
+ }
16035
+ }
16036
+ );
16006
16037
  pages = [
16007
16038
  {
16008
16039
  id: "benchmark",
@@ -16025,99 +16056,50 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16025
16056
  };
16026
16057
  await ReplayStore.declare(this.api.settingsStore);
16027
16058
  this.replayStore = new ReplayStore(this.api.settingsStore);
16059
+ await this.pruneReplays();
16028
16060
  return {
16029
16061
  providers: [{ capability: import_types4.addonPagesSourceCapability, provider }],
16030
16062
  customActions: benchmarkActions,
16031
16063
  actionHandlers: {
16032
16064
  runPipelineTest: async (input) => this.runPipelineTest(input),
16033
- runPipelineBatchTest: async (input) => this.runPipelineBatchTest(input),
16034
- runMultiCameraStressTest: async (input) => this.runMultiCameraStressTest(input),
16035
- runSustainedThroughput: async (input) => this.runSustainedThroughput(input),
16036
- abortSustainedThroughput: async (input) => this.abortSustainedThroughput(input),
16037
- cacheBenchFrame: async (input) => this.cacheBenchFrame(input),
16038
- releaseBenchFrame: async (input) => this.releaseBenchFrame(input),
16039
- listBenchmarkHistory: async () => this.listBenchmarkHistory(),
16065
+ listBenchmarkHistory: async () => this.loadHistory(),
16040
16066
  saveBenchmarkResult: async (input) => this.saveBenchmarkResult(input),
16041
- saveDecoderPerformanceResult: async (input) => this.saveDecoderPerformanceResult(input),
16042
16067
  deleteBenchmarkResult: async (input) => this.deleteBenchmarkResult(input),
16043
16068
  clearBenchmarkHistory: async () => this.clearBenchmarkHistory(),
16044
- listDecoderFixtures: async () => this.listDecoderFixtures(),
16045
- runDecoderPerformanceTest: async (input) => this.runDecoderPerformanceTest(input),
16046
- abortDecoderPerformanceTest: async (input) => this.abortDecoderPerformanceTest(input),
16047
- uploadReplayClip: async (input) => this.uploadReplayClip(input),
16048
- runReplayClip: async (input) => this.runReplayClip(input),
16049
- listReplayRuns: async (input) => this.listReplayRuns(input),
16050
- diffReplayRuns: async (input) => this.diffReplayRunsAction(input)
16069
+ listMotionSessions: async (input) => this.listMotionSessions(input),
16070
+ startSessionReplay: async (input) => this.startSessionReplay(input),
16071
+ getSessionReplayStatus: async (input) => this.getSessionReplayStatus(input),
16072
+ abortSessionReplay: async (input) => ({ aborted: this.replays.abort(input.runId) }),
16073
+ listSessionReplays: async (input) => this.listSessionReplays(input),
16074
+ getSessionReplay: async (input) => this.replayStore === null ? null : this.replayStore.get(input.id)
16051
16075
  }
16052
16076
  };
16053
16077
  }
16054
16078
  async onShutdown() {
16055
- if (this.frameCacheSweeper) {
16056
- clearInterval(this.frameCacheSweeper);
16057
- this.frameCacheSweeper = null;
16058
- }
16059
- this.frameCache.clear();
16060
- for (const flag of this.sustainedAbortFlags.values()) flag.aborted = true;
16061
- this.sustainedAbortFlags.clear();
16062
- }
16063
- // ── Frame cache (raw RGB) ────────────────────────────────────────────────
16064
- async cacheBenchFrame(input) {
16065
- const jpeg = Buffer.from(input.imageBase64, "base64");
16066
- const sharp = (await import("sharp")).default;
16067
- const meta3 = await sharp(jpeg).metadata();
16068
- const width = meta3.width ?? 0;
16069
- const height = meta3.height ?? 0;
16070
- if (width <= 0 || height <= 0) {
16071
- throw new Error("cacheBenchFrame: failed to probe JPEG dimensions");
16072
- }
16073
- const ttlMs = Math.max(6e4, (input.ttlSeconds ?? 600) * 1e3);
16074
- const frameId = `bench-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
16075
- const expiresAt = Date.now() + ttlMs;
16076
- this.frameCache.set(frameId, { jpeg, width, height, expiresAt });
16077
- if (!this.frameCacheSweeper) {
16078
- this.frameCacheSweeper = setInterval(() => this.sweepFrameCache(), 6e4);
16079
- this.frameCacheSweeper.unref?.();
16080
- }
16081
- this.ctx.logger.info("cached bench frame", {
16082
- meta: { frameId, width, height, jpegBytes: jpeg.length, ttlMs }
16083
- });
16084
- return { frameId, width, height, expiresAt };
16085
- }
16086
- async releaseBenchFrame(input) {
16087
- return { released: this.frameCache.delete(input.frameId) };
16079
+ this.replays.abortAll();
16088
16080
  }
16089
- sweepFrameCache() {
16090
- const now = Date.now();
16091
- for (const [id, entry] of this.frameCache) {
16092
- if (entry.expiresAt < now) this.frameCache.delete(id);
16093
- }
16094
- }
16095
- // ── Handler implementations ──────────────────────────────────────────────
16096
16081
  get api() {
16097
16082
  const api = this.ctx.api;
16098
16083
  if (!api) throw new Error("BenchmarkAddon: ctx.api not available");
16099
16084
  return api;
16100
16085
  }
16086
+ // ── Detection Pipeline — one run ─────────────────────────────────────────
16101
16087
  async runPipelineTest(input) {
16102
- const { nodeId, steps, imageBase64, referenceImage, frameId, sessionId, engine } = input;
16103
- const { deviceKey, plane } = input;
16088
+ const { nodeId, steps, imageBase64, referenceImage, sessionId, deviceKey, plane } = input;
16104
16089
  this.ctx.logger.debug("runPipelineTest payload", {
16105
16090
  meta: {
16106
16091
  nodeId: nodeId ?? null,
16107
16092
  deviceKey: deviceKey ?? null,
16108
16093
  plane: plane ?? null,
16109
16094
  sessionId: sessionId ?? null,
16110
- engine: engine ?? null,
16111
16095
  stepsCount: steps.length,
16112
- steps: steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`),
16113
- imageSource: frameId ? `frame:${frameId}` : referenceImage ? `ref:${referenceImage}` : imageBase64 ? `base64:${Math.round(imageBase64.length / 1024)}KB` : "none"
16096
+ steps: steps.map(
16097
+ (s) => `${s.addonId}/${s.modelId ?? "(format default)"}${s.enabled ? "" : "[off]"}`
16098
+ ),
16099
+ imageSource: referenceImage ? `ref:${referenceImage}` : imageBase64 ? `base64:${Math.round(imageBase64.length / 1024)}KB` : "none"
16114
16100
  }
16115
16101
  });
16116
- const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
16117
- if (frameId && !cached2)
16118
- throw new Error(`runPipelineTest: frameId ${frameId} not in cache (expired or never cached)`);
16119
- const cachedImage = cached2 ? new Uint8Array(cached2.jpeg) : void 0;
16120
- const image = cachedImage ?? (imageBase64 ? new Uint8Array(Buffer.from(imageBase64, "base64")) : void 0);
16102
+ const image = imageBase64 ? new Uint8Array(Buffer.from(imageBase64, "base64")) : void 0;
16121
16103
  const stepsToRun = steps.length > 0 ? [...steps] : await (async () => {
16122
16104
  const globalSteps = await this.api.pipelineExecutor.getGlobalSteps.query({ nodeId });
16123
16105
  if (!globalSteps || globalSteps.length === 0) {
@@ -16136,10 +16118,8 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16136
16118
  nodeId,
16137
16119
  steps: stepsToRun,
16138
16120
  ...image ? { image } : {},
16139
- ...!image && imageBase64 ? { imageBase64 } : {},
16140
16121
  referenceImage,
16141
16122
  sessionId,
16142
- ...engine ? { engine } : {},
16143
16123
  // Absent, never present-and-undefined: the generated router takes
16144
16124
  // `.loose()` input and an explicit `undefined` is not the same thing
16145
16125
  // as "the node's default pool" / "the cap's default plane".
@@ -16150,579 +16130,235 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16150
16130
  "runPipelineTest"
16151
16131
  );
16152
16132
  }
16153
- // ── Replay ────────────────────────────────────────────────────────────
16154
- /**
16155
- * Storage location a replay upload lands on. `replayClips` is a NEW
16156
- * `StorageLocationType` (an open string, no schema change needed — see
16157
- * `storage-location.ts:19`) but it still needs an actual location
16158
- * REGISTERED for that type before `storage.write`/`.read` can resolve it
16159
- * (`storage.upsertLocation`, `cardinality: 'single'`, its own retention).
16160
- * That registration is an operator/boot-time provisioning step this
16161
- * change does not perform — see the report's open items.
16162
- */
16163
- static REPLAY_CLIPS_LOCATION = "replayClips";
16164
- async uploadReplayClip(input) {
16165
- const bytes = Buffer.from(input.dataBase64, "base64");
16166
- const clipId = `clip:${(0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex").slice(0, 32)}`;
16167
- await this.api.storage.write.mutate({
16168
- location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16169
- relativePath: `${clipId}.bin`,
16170
- data: new Uint8Array(bytes)
16133
+ // ── Session replay (D646) ─────────────────────────────────────────────────
16134
+ async listMotionSessions(input) {
16135
+ const untilMs = input.untilMs ?? Date.now();
16136
+ const sinceMs = input.sinceMs ?? untilMs - MOTION_SESSIONS_DEFAULT_SPAN_MS;
16137
+ const events = await this.api.pipelineAnalytics.getMotionEvents.query({
16138
+ deviceId: input.deviceId,
16139
+ since: sinceMs,
16140
+ until: untilMs,
16141
+ limit: input.limit,
16142
+ projection: "full"
16171
16143
  });
16172
- this.ctx.logger.info("replay: clip uploaded", { meta: { clipId, bytes: bytes.length } });
16173
- return { clipId };
16174
- }
16175
- async runReplayClip(input) {
16176
- if (this.replayStore === null) throw new Error("replay: store not initialized");
16177
- const store = this.replayStore;
16178
- const source = input.source;
16179
- const result = await orchestrateReplayRun(
16180
- {
16181
- api: this.api,
16182
- logger: this.ctx.logger,
16183
- decoder: this.replayDecoder,
16184
- store,
16185
- ffmpegPath: process.env["FFMPEG_PATH"] ?? "ffmpeg",
16186
- resolveBaseTree: (deviceId) => this.api.pipelineOrchestrator.resolvePipeline.query({ deviceId }),
16187
- fetchSourceBytes: (src) => this.fetchReplaySourceBytes(src)
16188
- },
16189
- {
16190
- deviceId: input.deviceId,
16191
- nodeId: input.nodeId,
16192
- source,
16193
- overlay: input.overlay,
16194
- fps: input.fps,
16195
- maxFrames: input.maxFrames,
16196
- maxWidth: input.maxWidth,
16197
- ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {}
16198
- }
16144
+ const camera = await this.cameraSessionSettings(input.deviceId);
16145
+ const windows = events.map((ev) => ({ ev, window: motionSessionWindow(ev, camera) }));
16146
+ const starts = windows.map(
16147
+ ({ ev, window }) => window.kind === "window" ? window.startMs : ev.timestamp
16199
16148
  );
16200
- if (result.kind === "ran")
16201
- this.replayRunCache.set(result.replayRunId, {
16202
- id: result.replayRunId,
16203
- deviceId: input.deviceId,
16204
- nodeId: input.nodeId,
16205
- clipId: source.kind === "upload" ? source.clipId : `recording:${source.deviceId}:${source.fromMs}`,
16206
- createdAtMs: Date.now(),
16207
- source,
16208
- plan: null,
16209
- verdict: result.verdict
16210
- });
16211
- return result;
16212
- }
16213
- /**
16214
- * The profile a replay window is read from. `high` — same choice
16215
- * `RECORDED_STILL_PROFILE` makes in `addon-post-analysis` — because the
16216
- * whole point of `recording` over `upload` is NATIVE pixels (`-c:v copy`
16217
- * means the recorded frame IS the live frame); a window cut from a
16218
- * lower-rate rendition would be a worse version of the thing this source
16219
- * exists to avoid.
16220
- */
16221
- static REPLAY_RECORDING_PROFILE = "high";
16222
- /**
16223
- * Byte source for the replay decode step.
16224
- *
16225
- * `upload` is `storage.read` against the `replayClips` location. `recording`
16226
- * is `recording.getDeviceConfig` → `recording.locateSegment` →
16227
- * `recording.readWindowBytes` — the same "cheap question first" ordering
16228
- * `RecordedStillService` uses (a camera that is not recording costs a
16229
- * settings read, never a disk read), through `ctx.api` because addons never
16230
- * import each other.
16231
- *
16232
- * Every expected failure — not recording, no footage covering `fromMs`, a
16233
- * window that runs past its covering segment — returns a NAMED `refused`,
16234
- * never a thrown error: "the window has no segments" is a RESULT this
16235
- * whole feature exists to make legible (see the plan's stated unknown about
16236
- * whether device 615's footage covers 2026-08-26 18:12), not a failure to
16237
- * hide behind a generic exception.
16238
- */
16239
- async fetchReplaySourceBytes(source) {
16240
- if (source.kind === "upload") {
16241
- try {
16242
- const bytes = await this.api.storage.read.query({
16243
- location: _BenchmarkAddon.REPLAY_CLIPS_LOCATION,
16244
- relativePath: `${source.clipId}.bin`
16245
- });
16246
- return { kind: "ok", bytes, startMs: 0 };
16247
- } catch (err) {
16248
- return { kind: "refused", reason: `clip-not-found: ${(0, import_types4.errMsg)(err)}` };
16249
- }
16250
- }
16251
- const profile = _BenchmarkAddon.REPLAY_RECORDING_PROFILE;
16252
- const tags = { deviceId: source.deviceId };
16253
- const config2 = await this.api.recordingArchive.getDeviceConfig.query({
16254
- deviceId: source.deviceId
16255
- });
16256
- if (config2.enabled === false) {
16257
- this.ctx.logger.warn("replay: recording source refused \u2014 camera is not recording", {
16258
- tags
16259
- });
16260
- return {
16261
- kind: "refused",
16262
- reason: `no-recording: device ${source.deviceId} does not have recording enabled`
16263
- };
16264
- }
16265
- const located = await this.api.recordingArchive.locateSegment.query({
16266
- deviceId: source.deviceId,
16267
- profile,
16268
- epochMs: source.fromMs
16269
- });
16270
- if (located.kind !== "segment") {
16271
- this.ctx.logger.warn(
16272
- "replay: recording source refused \u2014 no footage covers the window start",
16273
- { tags, meta: { fromMs: source.fromMs, toMs: source.toMs, profile } }
16274
- );
16275
- return {
16276
- kind: "refused",
16277
- reason: `no-footage: device ${source.deviceId} has no ${profile} recording covering ${new Date(source.fromMs).toISOString()}`
16278
- };
16279
- }
16280
- const window = await this.api.recordingArchive.readWindowBytes.query({
16281
- deviceId: source.deviceId,
16282
- profile,
16283
- startMs: located.startMs,
16284
- fromMs: source.fromMs,
16285
- toMs: source.toMs
16286
- });
16287
- if (window.kind !== "ok") {
16288
- this.ctx.logger.warn(
16289
- "replay: recording source refused \u2014 window runs past the covering segment",
16290
- {
16291
- tags,
16292
- meta: { fromMs: source.fromMs, toMs: source.toMs, segmentEndMs: window.segmentEndMs }
16293
- }
16294
- );
16295
- return {
16296
- kind: "refused",
16297
- reason: `window-spans-multiple-segments: footage for device ${source.deviceId} changes segment at ${window.segmentEndMs}, before the requested end ${source.toMs} \u2014 shorten the window or align it to one segment`
16298
- };
16299
- }
16300
- if (!window.reachesRequestedEnd) {
16301
- this.ctx.logger.warn(
16302
- "replay: recording window bytes truncated by the recorder read safety cap",
16303
- { tags, meta: { fromMs: source.fromMs, toMs: source.toMs, gopDurMs: window.gopDurMs } }
16304
- );
16305
- }
16306
- return { kind: "ok", bytes: window.data, startMs: window.gopStartMs };
16307
- }
16308
- async listReplayRuns(input) {
16309
- if (this.replayStore === null) return [];
16310
- const rows = await this.replayStore.list({ deviceId: input.deviceId });
16311
- for (const row of rows) this.replayRunCache.set(row.id, row);
16312
- return rows;
16313
- }
16314
- /** `verdict` round-trips through the store as opaque JSON
16315
- * (`StoredReplayRun.verdict: unknown`) — this is the ONE narrowing back
16316
- * to the shape `diffReplayRuns` needs, at the ONE place that reads it
16317
- * for that purpose. Same documented boundary `ReplayStore.list` itself
16318
- * crosses (`replay-store.ts`): this addon is the only reader that knows
16319
- * what it wrote. */
16320
- static verdictTracksAndDiscarded(verdict) {
16321
- return verdict;
16322
- }
16323
- async diffReplayRunsAction(input) {
16324
- const a = this.replayRunCache.get(input.runIdA);
16325
- const b = this.replayRunCache.get(input.runIdB);
16326
- if (a === void 0 || b === void 0) {
16327
- throw new Error(
16328
- "replay: diff \u2014 one or both run ids are not loaded (call listReplayRuns first)"
16329
- );
16330
- }
16331
- return diffReplayRuns(
16332
- {
16333
- clipId: a.clipId,
16334
- nodeId: a.nodeId,
16335
- settings: {},
16336
- ..._BenchmarkAddon.verdictTracksAndDiscarded(a.verdict)
16337
- },
16338
- {
16339
- clipId: b.clipId,
16340
- nodeId: b.nodeId,
16341
- settings: {},
16342
- ..._BenchmarkAddon.verdictTracksAndDiscarded(b.verdict)
16343
- }
16149
+ const ends = windows.map(
16150
+ ({ ev, window }) => window.kind === "window" ? window.endMs : ev.timestamp
16344
16151
  );
16345
- }
16346
- /**
16347
- * Batched pipeline tester.
16348
- *
16349
- * Decodes the JPEG once via sharp into RGB24 at native dimensions,
16350
- * then ships N copies as `FrameInput[]` in a single
16351
- * `pipelineExecutor.runPipelineBatch` cap call. The Python pool
16352
- * collapses the N predicts into one `MSG_INFER_BATCH = 0x03` IPC
16353
- * frame — per-call header + JSON envelope cost is amortised N:1.
16354
- *
16355
- * Returns N `FrameResult`s plus aggregate timing so the UI can
16356
- * compute fps directly from the wall clock.
16357
- */
16358
- async runPipelineBatchTest(input) {
16359
- this.ctx.logger.debug("runPipelineBatchTest payload", {
16360
- meta: {
16361
- nodeId: input.nodeId ?? null,
16362
- sessionId: input.sessionId ?? null,
16363
- engine: input.engine ?? null,
16364
- batchSize: input.batchSize,
16365
- targetSize: input.targetSize ?? null,
16366
- stepsCount: input.steps.length,
16367
- steps: input.steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`),
16368
- imageBytesKB: Math.round(input.imageBase64.length / 1024)
16369
- }
16370
- });
16371
- const { nodeId, steps, imageBase64, batchSize, sessionId, engine } = input;
16372
- const targetSize = input.targetSize ?? 640;
16373
- if (steps.length === 0) {
16374
- throw new Error("runPipelineBatchTest: empty steps");
16375
- }
16376
- const sharp = (await import("sharp")).default;
16377
- const jpeg = Buffer.from(imageBase64, "base64");
16378
- const rgb = await sharp(jpeg).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16379
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16380
- this.ctx.logger.debug("runPipelineBatchTest dispatch", {
16381
- meta: {
16382
- phase: "batch",
16383
- batchSize,
16384
- targetSize,
16385
- rgbBytes: rgb.byteLength,
16386
- totalBytes: rgb.byteLength * batchSize
16387
- }
16152
+ const tracks = events.length === 0 ? [] : await this.api.pipelineAnalytics.listTracks.query({
16153
+ deviceId: input.deviceId,
16154
+ since: Math.min(...starts),
16155
+ until: Math.max(...ends),
16156
+ projection: "slim",
16157
+ excludeSources: ["sensor", "audio"],
16158
+ limit: STORED_TRACKS_READ_LIMIT
16388
16159
  });
16389
- const frames = Array.from({ length: batchSize }, () => ({
16390
- data: rgbBytes,
16391
- format: "rgb",
16392
- width: targetSize,
16393
- height: targetSize,
16394
- timestamp: Date.now()
16160
+ return windows.map(({ ev, window }) => ({
16161
+ motionEventId: ev.id,
16162
+ deviceId: ev.deviceId,
16163
+ startMs: ev.timestamp,
16164
+ durationMs: ev.durationMs ?? null,
16165
+ open: ev.durationMs === null,
16166
+ refusal: window.kind === "refused" ? window.reason : null,
16167
+ storedTracks: window.kind === "window" ? tracks.filter((t) => t.firstSeen >= window.startMs && t.firstSeen < window.endMs).length : 0
16395
16168
  }));
16396
- const callStart = Date.now();
16397
- const out = await withHardTimeout(
16398
- this.api.pipelineExecutor.runPipelineBatch.mutate({
16399
- nodeId,
16400
- steps: [...steps],
16401
- frames,
16402
- sessionId,
16403
- ...engine ? { engine } : {}
16404
- }),
16405
- BENCH_RUN_TIMEOUT_MS,
16406
- "runPipelineBatchTest"
16407
- );
16408
- const callMs = Date.now() - callStart;
16409
- const inferMs = out.results.reduce((acc, r) => Math.max(acc, r.debug?.totalInferenceMs ?? 0), 0);
16410
- return {
16411
- results: out.results,
16412
- callMs,
16413
- inferMs
16414
- };
16415
16169
  }
16416
- /**
16417
- * Multi-camera stress tester.
16418
- *
16419
- * Runs the entire stress workload IN-PROCESS:
16420
- * 1. Decode + sharp-resize the JPEG once.
16421
- * 2. Build pre-fabricated `FrameInput[]` templates for the batched
16422
- * and single-frame call shapes.
16423
- * 3. Spawn `simulatedCameras` parallel async loops; each loop runs
16424
- * `batchesPerCamera` rounds. Per round: fan out 1 batched call
16425
- * + `extraSinglesPerRound` single calls via `Promise.all`.
16426
- * 4. Aggregate detsPerSec from `(simulatedCameras × batchesPerCamera
16427
- * × batchSize) / totalMs` so the metric is comparable to the
16428
- * external detector-engine benchmark we're calibrating against.
16429
- *
16430
- * Because the dispatch goes through `this.api.pipelineExecutor.
16431
- * runPipelineBatch.mutate(...)` and not over the wire, `localProvider
16432
- * Link` short-circuits when detection-pipeline is co-located on the
16433
- * same node. Removes per-call JSON marshalling, JPEG decode, and TLS.
16434
- */
16435
- async runMultiCameraStressTest(input) {
16436
- const targetSize = input.targetSize ?? 640;
16437
- const simulatedCameras = input.simulatedCameras ?? 4;
16438
- const batchesPerCamera = input.batchesPerCamera ?? 125;
16439
- const batchSize = input.batchSize ?? 4;
16440
- const extraSinglesPerRound = input.extraSinglesPerRound ?? 3;
16441
- const { nodeId, steps, sessionId } = input;
16442
- if (steps.length === 0) {
16443
- throw new Error("runMultiCameraStressTest: empty steps");
16444
- }
16445
- this.ctx.logger.info("runMultiCameraStressTest start", {
16170
+ startSessionReplay(input) {
16171
+ const runId = `replay-${input.deviceId}-${input.motionEventId}-${Date.now().toString(36)}`;
16172
+ this.ctx.logger.info("session replay started", {
16173
+ tags: { deviceId: input.deviceId },
16446
16174
  meta: {
16447
- nodeId: nodeId ?? null,
16448
- simulatedCameras,
16449
- batchesPerCamera,
16450
- batchSize,
16451
- extraSinglesPerRound,
16452
- targetSize,
16453
- steps: steps.map((s) => `${s.addonId}/${s.modelId}${s.enabled ? "" : "[off]"}`)
16175
+ runId,
16176
+ motionEventId: input.motionEventId,
16177
+ nodeId: input.nodeId,
16178
+ deviceKey: input.deviceKey ?? null,
16179
+ modelId: input.modelId ?? null,
16180
+ fps: input.fps ?? null,
16181
+ maxFrames: input.maxFrames
16454
16182
  }
16455
16183
  });
16456
- const sharp = (await import("sharp")).default;
16457
- const jpeg = Buffer.from(input.imageBase64, "base64");
16458
- const rgb = await sharp(jpeg).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16459
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16460
- const buildFrames = (count) => Array.from({ length: count }, () => ({
16461
- data: rgbBytes,
16462
- format: "rgb",
16463
- width: targetSize,
16464
- height: targetSize,
16465
- timestamp: Date.now()
16466
- }));
16467
- const batchedFrames = buildFrames(batchSize);
16468
- const singleFrames = buildFrames(1);
16469
- const callMsList = [];
16470
- const inferMsList = [];
16471
- let errors = 0;
16472
- let countedDetections = 0;
16473
- let totalCalls = 0;
16474
- let totalFrames = 0;
16475
- const dispatchOne = async (frames, isCounted, tag) => {
16476
- const callStart = Date.now();
16477
- try {
16478
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
16479
- nodeId,
16480
- steps: [...steps],
16481
- frames: [...frames],
16482
- sessionId: sessionId ? `${sessionId}-${tag}` : tag
16483
- });
16484
- const callMs = Date.now() - callStart;
16485
- if (isCounted) {
16486
- callMsList.push(callMs);
16487
- let maxInfer = 0;
16488
- for (const r of out.results) {
16489
- const ms = r.debug?.totalInferenceMs ?? 0;
16490
- if (ms > maxInfer) maxInfer = ms;
16491
- }
16492
- inferMsList.push(maxInfer);
16493
- }
16494
- } catch (err) {
16495
- errors++;
16496
- this.ctx.logger.warn("runMultiCameraStressTest dispatch error", {
16497
- meta: { tag, error: err instanceof Error ? err.message : String(err) }
16498
- });
16499
- }
16500
- };
16501
- const start = Date.now();
16502
- const cameraTasks = Array.from(
16503
- { length: simulatedCameras },
16504
- (_, camIdx) => (async () => {
16505
- for (let i = 0; i < batchesPerCamera; i++) {
16506
- const dispatches = [
16507
- dispatchOne(batchedFrames, true, `c${camIdx}-r${i}-b`)
16508
- ];
16509
- for (let j = 0; j < extraSinglesPerRound; j++) {
16510
- dispatches.push(dispatchOne(singleFrames, false, `c${camIdx}-r${i}-s${j}`));
16511
- }
16512
- await Promise.all(dispatches);
16513
- countedDetections += batchSize;
16514
- totalCalls += 1 + extraSinglesPerRound;
16515
- totalFrames += batchSize + extraSinglesPerRound;
16516
- }
16517
- })()
16184
+ this.replays.start(
16185
+ runId,
16186
+ (run) => runSessionReplay(
16187
+ this.sessionReplayDeps(),
16188
+ {
16189
+ deviceId: input.deviceId,
16190
+ motionEventId: input.motionEventId,
16191
+ motionStartMs: input.motionStartMs,
16192
+ nodeId: input.nodeId,
16193
+ maxFrames: input.maxFrames,
16194
+ ...input.deviceKey !== void 0 ? { deviceKey: input.deviceKey } : {},
16195
+ ...input.modelId !== void 0 ? { modelId: input.modelId } : {},
16196
+ ...input.fps !== void 0 ? { fps: input.fps } : {}
16197
+ },
16198
+ run
16199
+ )
16518
16200
  );
16519
- await Promise.all(cameraTasks);
16520
- const totalMs = Date.now() - start;
16521
- const sortedCall = [...callMsList].toSorted((a, b) => a - b);
16522
- const pct = (xs, p) => {
16523
- if (xs.length === 0) return 0;
16524
- const idx = Math.min(xs.length - 1, Math.floor(xs.length * p));
16525
- return xs[idx] ?? 0;
16201
+ return { accepted: true, runId };
16202
+ }
16203
+ getSessionReplayStatus(input) {
16204
+ const status = this.replays.status(input.runId);
16205
+ if (status === null) return null;
16206
+ return {
16207
+ runId: status.runId,
16208
+ state: status.state,
16209
+ startedAtMs: status.startedAtMs,
16210
+ finishedAtMs: status.finishedAtMs,
16211
+ progress: status.progress,
16212
+ outcome: status.result,
16213
+ error: status.error
16526
16214
  };
16527
- const inferMean = inferMsList.length === 0 ? 0 : inferMsList.reduce((s, x) => s + x, 0) / inferMsList.length;
16528
- const detsPerSec = totalMs > 0 ? countedDetections * 1e3 / totalMs : 0;
16529
- this.ctx.logger.info("runMultiCameraStressTest done", {
16530
- meta: {
16531
- totalMs,
16532
- countedDetections,
16533
- detsPerSec: Number(detsPerSec.toFixed(2)),
16534
- totalCalls,
16535
- totalFrames,
16536
- errors
16537
- }
16538
- });
16215
+ }
16216
+ async listSessionReplays(input) {
16217
+ if (this.replayStore === null) return { runs: [], skipped: 0 };
16218
+ const { runs, skipped } = await this.replayStore.list({ deviceId: input.deviceId, limit: 50 });
16219
+ if (skipped > 0) {
16220
+ this.ctx.logger.info("session replays: rows skipped that are not session replays", {
16221
+ tags: { deviceId: input.deviceId },
16222
+ meta: { skipped }
16223
+ });
16224
+ }
16539
16225
  return {
16540
- totalMs,
16541
- countedDetections,
16542
- detsPerSec,
16543
- totalCalls,
16544
- totalFrames,
16545
- callMsP50: pct(sortedCall, 0.5),
16546
- callMsP95: pct(sortedCall, 0.95),
16547
- callMsP99: pct(sortedCall, 0.99),
16548
- inferMsMean: inferMean,
16549
- errors
16226
+ runs: runs.map((r) => {
16227
+ const { timeline: _timeline, ...report } = r.report;
16228
+ return {
16229
+ id: r.id,
16230
+ deviceId: r.deviceId,
16231
+ nodeId: r.nodeId,
16232
+ createdAtMs: r.createdAtMs,
16233
+ report
16234
+ };
16235
+ }),
16236
+ skipped
16550
16237
  };
16551
16238
  }
16552
- // ── Sustained throughput (root-detection e2e) ────────────────────────────
16553
- /**
16554
- * SUSTAINED THROUGHPUT test of ROOT detection.
16555
- *
16556
- * 1. Pin ONE frame in memory: resolve `frameId` (from `cacheBenchFrame`)
16557
- * or the one-shot `imageBase64`, decode + square-resize ONCE to raw
16558
- * RGB. Every node + every iteration consumes the IDENTICAL buffer — no
16559
- * per-iteration decode or upload.
16560
- * 2. For each target node, run a `concurrency`-bounded loop of
16561
- * `runPipelineBatch` (batchSize frames/call) on the node's ROOT-ONLY
16562
- * steps until `durationSec` elapses (or the stop flag fires). Root-only
16563
- * steps stay on the batch single-root fast path == live `plane:'frame'`
16564
- * root detection.
16565
- * 3. Aggregate sustained fps / det/s + latency percentiles per node on the
16566
- * identical pinned frame for apples-to-apples comparison.
16567
- *
16568
- * Non-destructive: targets the benchmark's own cached frame (never a live
16569
- * camera session), is explicitly started, and is bounded by N + the stop
16570
- * button (`abortSustainedThroughput`).
16571
- */
16572
- async runSustainedThroughput(input) {
16573
- const { frameId, imageBase64, durationSec, batchSize, concurrency, sessionId, nodes } = input;
16574
- const targetSize = input.targetSize ?? 640;
16575
- const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
16576
- if (frameId && !cached2)
16577
- throw new Error(
16578
- `runSustainedThroughput: frameId ${frameId} not in cache (expired or never cached)`
16579
- );
16580
- const jpegSource = cached2 ? cached2.jpeg : imageBase64 ? Buffer.from(imageBase64, "base64") : null;
16581
- if (!jpegSource)
16582
- throw new Error("runSustainedThroughput: no frame source (need frameId or imageBase64)");
16583
- const sharp = (await import("sharp")).default;
16584
- const rgb = await sharp(jpegSource).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
16585
- const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
16586
- const batchFrames = Array.from({ length: batchSize }, () => ({
16587
- data: rgbBytes,
16588
- format: "rgb",
16589
- width: targetSize,
16590
- height: targetSize,
16591
- timestamp: Date.now()
16592
- }));
16593
- const benchFrameId = Math.floor(Math.random() * 4294967294) + 1 >>> 0;
16594
- const abortFlag = { aborted: false };
16595
- this.sustainedAbortFlags.set(sessionId, abortFlag);
16596
- this.ctx.logger.info("runSustainedThroughput start", {
16597
- meta: {
16598
- sessionId,
16599
- durationSec,
16600
- batchSize,
16601
- concurrency,
16602
- targetSize,
16603
- nodeCount: nodes.length,
16604
- nodes: nodes.map((n) => n.nodeId ?? "hub"),
16605
- deviceKeys: nodes.map((n) => n.deviceKey ?? "(default)")
16606
- }
16607
- });
16239
+ async persistReplay(runId, nodeId, outcome) {
16240
+ const store = this.replayStore;
16241
+ if (store === null) return;
16242
+ const tags = { deviceId: outcome.report.deviceId };
16608
16243
  try {
16609
- const perNode = await Promise.all(
16610
- nodes.map(async (node) => {
16611
- if (node.steps.length === 0)
16612
- throw new Error(
16613
- `runSustainedThroughput: node ${node.nodeId ?? "hub"} has no root steps`
16614
- );
16615
- const dispatchBatch = async () => {
16616
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
16617
- nodeId: node.nodeId,
16618
- steps: [...node.steps],
16619
- frames: [...batchFrames],
16620
- sessionId,
16621
- frameId: benchFrameId,
16622
- ...node.engine ? { engine: node.engine } : {},
16623
- ...node.deviceKey ? { deviceKey: node.deviceKey } : {}
16624
- });
16625
- return {
16626
- frameCount: out.results.length,
16627
- detectionCounts: out.results.map((r) => r.detections.length)
16628
- };
16629
- };
16630
- let raw;
16631
- try {
16632
- raw = await runSustainedLoop({
16633
- dispatchBatch,
16634
- concurrency,
16635
- durationMs: durationSec * 1e3,
16636
- now: () => Date.now(),
16637
- shouldStop: () => abortFlag.aborted
16638
- });
16639
- } finally {
16640
- await this.api.pipelineExecutor.uncacheFrame.mutate({ nodeId: node.nodeId, frameId: benchFrameId }).catch(() => {
16641
- });
16642
- }
16643
- const summary = {
16644
- nodeId: node.nodeId ?? null,
16645
- ...node.label ? { label: node.label } : {},
16646
- totalFrames: raw.totalFrames,
16647
- totalBatches: raw.totalBatches,
16648
- wallMs: raw.wallMs,
16649
- fps: computeFps(raw.totalFrames, raw.wallMs),
16650
- detsPerSec: computeDetsPerSec(raw.totalDetections, raw.wallMs),
16651
- avgDetections: computeAvgDetections(raw.totalDetections, raw.detectionSamples),
16652
- latency: computeLatencyStats(raw.callMsList),
16653
- errors: raw.errors,
16654
- aborted: raw.aborted
16655
- };
16656
- this.ctx.logger.info("runSustainedThroughput node done", {
16657
- meta: {
16658
- nodeId: summary.nodeId,
16659
- fps: Number(summary.fps.toFixed(2)),
16660
- detsPerSec: Number(summary.detsPerSec.toFixed(2)),
16661
- p95Ms: Number(summary.latency.p95.toFixed(2)),
16662
- totalFrames: summary.totalFrames,
16663
- errors: summary.errors,
16664
- aborted: summary.aborted
16665
- }
16666
- });
16667
- return summary;
16668
- })
16669
- );
16670
- return { perNode, durationSec, batchSize, concurrency };
16671
- } finally {
16672
- this.sustainedAbortFlags.delete(sessionId);
16244
+ await store.save({
16245
+ id: runId,
16246
+ deviceId: outcome.report.deviceId,
16247
+ nodeId,
16248
+ createdAtMs: Date.now(),
16249
+ report: outcome.report
16250
+ });
16251
+ await this.pruneReplays();
16252
+ } catch (err) {
16253
+ this.ctx.logger.warn("session replay: report NOT saved", {
16254
+ tags,
16255
+ meta: { runId, error: (0, import_types4.errMsg)(err) }
16256
+ });
16673
16257
  }
16674
16258
  }
16675
- async abortSustainedThroughput(input) {
16676
- const flag = this.sustainedAbortFlags.get(input.sessionId);
16677
- if (!flag) return { aborted: false };
16678
- flag.aborted = true;
16679
- return { aborted: true };
16680
- }
16681
- // ── Decoder perf — fixture discovery + run orchestration ────────────────
16682
- async listDecoderFixtures() {
16683
- const dir = resolveFixturesDir();
16684
- if (!fs2.existsSync(dir)) {
16685
- this.ctx.logger.warn("decoder fixtures directory missing", { meta: { dir } });
16686
- return [];
16687
- }
16688
- const out = [];
16689
- for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
16690
- if (!entry.isFile()) continue;
16691
- const meta3 = FIXTURE_META[entry.name];
16692
- if (!meta3) continue;
16693
- const full = path2.join(dir, entry.name);
16694
- const stat = fs2.statSync(full);
16695
- out.push({
16696
- filename: entry.name,
16697
- codec: meta3.codec,
16698
- resolutionLabel: meta3.resolutionLabel,
16699
- sizeBytes: stat.size,
16700
- description: meta3.description
16701
- });
16259
+ async pruneReplays() {
16260
+ const store = this.replayStore;
16261
+ if (store === null) return;
16262
+ try {
16263
+ const deleted = await store.pruneBefore(Date.now() - REPLAY_RETENTION_MS);
16264
+ if (deleted > 0) this.ctx.logger.info("session replays pruned", { meta: { deleted } });
16265
+ } catch (err) {
16266
+ this.ctx.logger.warn("session replays: prune failed", { meta: { error: (0, import_types4.errMsg)(err) } });
16702
16267
  }
16703
- out.sort((a, b) => a.sizeBytes - b.sizeBytes);
16704
- return out;
16705
16268
  }
16706
- async runDecoderPerformanceTest(input) {
16707
- const abortFlag = { aborted: false };
16708
- this.decoderPerfAbortFlags.set(input.sessionId, abortFlag);
16269
+ /**
16270
+ * The camera's own cooldown + pre-roll switch, read from the orchestrator's
16271
+ * hydrated device settings (the authority, not a copy). A failed read is
16272
+ * `{}` — the fleet defaults apply and the report says `default`.
16273
+ */
16274
+ async cameraSessionSettings(deviceId) {
16709
16275
  try {
16710
- return await runDecoderPerfTest(input, {
16711
- api: this.api,
16712
- eventBus: this.ctx.eventBus,
16713
- logger: this.ctx.logger,
16714
- fixturesDir: resolveFixturesDir(),
16715
- abortFlag
16276
+ const raw = await this.api.addonSettings.getDeviceSettings.query({
16277
+ addonId: "pipeline-orchestrator",
16278
+ deviceId
16716
16279
  });
16717
- } finally {
16718
- this.decoderPerfAbortFlags.delete(input.sessionId);
16280
+ return readCameraSessionSettings(raw);
16281
+ } catch (err) {
16282
+ this.ctx.logger.warn("session replay: camera session settings unreadable \u2014 fleet defaults", {
16283
+ tags: { deviceId },
16284
+ meta: { error: (0, import_types4.errMsg)(err) }
16285
+ });
16286
+ return {};
16719
16287
  }
16720
16288
  }
16721
- async abortDecoderPerformanceTest(input) {
16722
- const flag = this.decoderPerfAbortFlags.get(input.sessionId);
16723
- if (!flag) return { aborted: false };
16724
- flag.aborted = true;
16725
- return { aborted: true };
16289
+ /** The replay's outside world — every call through `ctx.api`, node-pinned where it matters. */
16290
+ sessionReplayDeps() {
16291
+ const api = this.api;
16292
+ const profile = REPLAY_RECORDING_PROFILE;
16293
+ return {
16294
+ logger: this.ctx.logger,
16295
+ cameraSessionSettings: (deviceId) => this.cameraSessionSettings(deviceId),
16296
+ findMotionSession: async (deviceId, motionEventId, motionStartMs) => {
16297
+ const events = await api.pipelineAnalytics.getMotionEvents.query({
16298
+ deviceId,
16299
+ since: motionStartMs - 1,
16300
+ until: motionStartMs + 1,
16301
+ limit: 50,
16302
+ projection: "full"
16303
+ });
16304
+ return events.find((e) => e.id === motionEventId) ?? null;
16305
+ },
16306
+ resolveCameraTree: (deviceId, nodeId) => api.pipelineOrchestrator.resolvePipeline.query({ deviceId, agentNodeId: nodeId }),
16307
+ validate: (steps, nodeId, deviceKey) => api.pipelineExecutor.validatePipeline.query(
16308
+ { steps: [...steps], ...deviceKey !== void 0 ? { deviceKey } : {} },
16309
+ (0, import_types4.nodePin)(nodeId)
16310
+ ),
16311
+ isRecording: async (deviceId) => {
16312
+ const config2 = await api.recordingArchive.getDeviceConfig.query({ deviceId });
16313
+ return config2.enabled !== false;
16314
+ },
16315
+ listStoredTracks: async (deviceId, sinceMs, untilMs) => api.pipelineAnalytics.listTracks.query({
16316
+ deviceId,
16317
+ since: sinceMs,
16318
+ until: untilMs,
16319
+ projection: "full",
16320
+ excludeSources: ["sensor", "audio"],
16321
+ limit: STORED_TRACKS_READ_LIMIT
16322
+ }),
16323
+ listBirthDecisions: (deviceId, sinceMs) => api.pipelineAnalytics.listBirthDecisions.query({
16324
+ deviceId,
16325
+ since: sinceMs,
16326
+ limit: REPLAY_BIRTHS_LIMIT
16327
+ }),
16328
+ readRecording: (deviceId, instants, stepMs) => readRecordingForInstants(
16329
+ {
16330
+ locateSegment: (epochMs) => api.recordingArchive.locateSegment.query({ deviceId, profile, epochMs }),
16331
+ readWindowBytes: (q) => api.recordingArchive.readWindowBytes.query({ deviceId, profile, ...q })
16332
+ },
16333
+ instants,
16334
+ stepMs
16335
+ ),
16336
+ decode: async (bytes, args) => {
16337
+ const mjpeg = await this.replayDecoder.decodeSequence(
16338
+ buildReplaySequenceArgs(args),
16339
+ bytes,
16340
+ {
16341
+ timeoutMs: Math.max(
16342
+ REPLAY_DECODE_TIMEOUT_FLOOR_MS,
16343
+ Math.round(args.durationSec * REPLAY_DECODE_TIMEOUT_PER_SEC_MS)
16344
+ )
16345
+ }
16346
+ );
16347
+ return splitMjpegStream(mjpeg);
16348
+ },
16349
+ runRootDetector: (call) => api.pipelineExecutor.runPipeline.mutate({
16350
+ nodeId: call.nodeId,
16351
+ steps: [...call.steps],
16352
+ // A fresh `Uint8Array<ArrayBuffer>`: `mjpeg-split` hands back a
16353
+ // `Buffer` view, and the generated cap input is typed against the
16354
+ // narrower `ArrayBuffer` backing.
16355
+ image: new Uint8Array(call.image),
16356
+ deviceId: call.deviceId,
16357
+ plane: "frame",
16358
+ replay: true,
16359
+ ...call.deviceKey !== void 0 ? { deviceKey: call.deviceKey } : {}
16360
+ })
16361
+ };
16726
16362
  }
16727
16363
  // ── History persistence (per-addon store on the hub) ──────────────────
16728
16364
  /**
@@ -16730,9 +16366,7 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16730
16366
  * VERBATIM: the value under `benchmarkHistory` is a JSON **string** (not
16731
16367
  * the array itself), so the handle is typed over the string — the
16732
16368
  * encode/decode + entry-level recovery below stays the codec, the handle
16733
- * owns store access (retry budget + validated fallback). Absent key or a
16734
- * non-string value falls back to the empty-history encoding, exactly the
16735
- * `[]` the raw read produced.
16369
+ * owns store access (retry budget + validated fallback).
16736
16370
  */
16737
16371
  historyState() {
16738
16372
  return this.state(KEY_BENCHMARK_HISTORY, external_exports.string(), EMPTY_HISTORY_JSON);
@@ -16746,14 +16380,10 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16746
16380
  const arr = Array.isArray(parsed) ? parsed : [];
16747
16381
  const accepted = [];
16748
16382
  let rejected = 0;
16749
- for (const entry of arr) {
16750
- const normalized = entry !== null && typeof entry === "object" && !("kind" in entry) ? { ...entry, kind: "pipeline" } : entry;
16751
- const parsedEntry = HistoryEntrySchema.safeParse(normalized);
16752
- if (parsedEntry.success) {
16753
- accepted.push(parsedEntry.data);
16754
- } else {
16755
- rejected += 1;
16756
- }
16383
+ for (const entry2 of arr) {
16384
+ const parsedEntry = HistoryEntrySchema.safeParse(entry2);
16385
+ if (parsedEntry.success) accepted.push(parsedEntry.data);
16386
+ else rejected += 1;
16757
16387
  }
16758
16388
  if (rejected > 0) {
16759
16389
  this.ctx.logger.warn("benchmark history: dropped unparseable entries", {
@@ -16769,13 +16399,10 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16769
16399
  async writeHistory(history) {
16770
16400
  await this.historyState().set(JSON.stringify(history));
16771
16401
  }
16772
- async listBenchmarkHistory() {
16773
- return this.loadHistory();
16774
- }
16775
- async saveBenchmarkResult(entry) {
16402
+ async saveBenchmarkResult(entry2) {
16776
16403
  const history = await this.loadHistory();
16777
16404
  const saved = {
16778
- ...entry,
16405
+ ...entry2,
16779
16406
  kind: "pipeline",
16780
16407
  id: `bench-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
16781
16408
  date: (/* @__PURE__ */ new Date()).toISOString()
@@ -16785,19 +16412,6 @@ var BenchmarkAddon = class _BenchmarkAddon extends import_types4.BaseAddon {
16785
16412
  await this.writeHistory(next);
16786
16413
  return saved;
16787
16414
  }
16788
- async saveDecoderPerformanceResult(entry) {
16789
- const history = await this.loadHistory();
16790
- const saved = {
16791
- kind: "decoder-perf",
16792
- id: `decoder-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
16793
- date: (/* @__PURE__ */ new Date()).toISOString(),
16794
- ...entry
16795
- };
16796
- const next = [saved, ...history];
16797
- if (next.length > MAX_HISTORY_ENTRIES) next.length = MAX_HISTORY_ENTRIES;
16798
- await this.writeHistory(next);
16799
- return saved;
16800
- }
16801
16415
  async deleteBenchmarkResult(input) {
16802
16416
  const history = (await this.loadHistory()).filter((e) => e.id !== input.id);
16803
16417
  await this.writeHistory(history);