@camstack/addon-benchmark 1.1.30 → 1.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -551,7 +551,7 @@ __export(core_exports2, {
551
551
  parse: () => parse,
552
552
  parseAsync: () => parseAsync,
553
553
  prettifyError: () => prettifyError,
554
- process: () => process,
554
+ process: () => process2,
555
555
  regexes: () => regexes_exports,
556
556
  registry: () => registry,
557
557
  safeDecode: () => safeDecode,
@@ -11476,7 +11476,7 @@ function initializeContext(params) {
11476
11476
  external: params?.external ?? void 0
11477
11477
  };
11478
11478
  }
11479
- function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
11479
+ function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
11480
11480
  var _a3;
11481
11481
  const def = schema._zod.def;
11482
11482
  const seen = ctx.seen.get(schema);
@@ -11513,7 +11513,7 @@ function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
11513
11513
  if (parent) {
11514
11514
  if (!result.ref)
11515
11515
  result.ref = parent;
11516
- process(parent, ctx, params);
11516
+ process2(parent, ctx, params);
11517
11517
  ctx.seen.get(parent).isParent = true;
11518
11518
  }
11519
11519
  }
@@ -11801,14 +11801,14 @@ function isTransforming(_schema, _ctx) {
11801
11801
  }
11802
11802
  var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
11803
11803
  const ctx = initializeContext({ ...params, processors });
11804
- process(schema, ctx);
11804
+ process2(schema, ctx);
11805
11805
  extractDefs(ctx, schema);
11806
11806
  return finalize(ctx, schema);
11807
11807
  };
11808
11808
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
11809
11809
  const { libraryOptions, target } = params ?? {};
11810
11810
  const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
11811
- process(schema, ctx);
11811
+ process2(schema, ctx);
11812
11812
  extractDefs(ctx, schema);
11813
11813
  return finalize(ctx, schema);
11814
11814
  };
@@ -12054,7 +12054,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
12054
12054
  if (typeof maximum === "number")
12055
12055
  json2.maxItems = maximum;
12056
12056
  json2.type = "array";
12057
- json2.items = process(def.element, ctx, {
12057
+ json2.items = process2(def.element, ctx, {
12058
12058
  ...params,
12059
12059
  path: [...params.path, "items"]
12060
12060
  });
@@ -12066,7 +12066,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12066
12066
  json2.properties = {};
12067
12067
  const shape = def.shape;
12068
12068
  for (const key in shape) {
12069
- json2.properties[key] = process(shape[key], ctx, {
12069
+ json2.properties[key] = process2(shape[key], ctx, {
12070
12070
  ...params,
12071
12071
  path: [...params.path, "properties", key]
12072
12072
  });
@@ -12089,7 +12089,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12089
12089
  if (ctx.io === "output")
12090
12090
  json2.additionalProperties = false;
12091
12091
  } else if (def.catchall) {
12092
- json2.additionalProperties = process(def.catchall, ctx, {
12092
+ json2.additionalProperties = process2(def.catchall, ctx, {
12093
12093
  ...params,
12094
12094
  path: [...params.path, "additionalProperties"]
12095
12095
  });
@@ -12098,7 +12098,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12098
12098
  var unionProcessor = (schema, ctx, json2, params) => {
12099
12099
  const def = schema._zod.def;
12100
12100
  const isExclusive = def.inclusive === false;
12101
- const options = def.options.map((x, i) => process(x, ctx, {
12101
+ const options = def.options.map((x, i) => process2(x, ctx, {
12102
12102
  ...params,
12103
12103
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i]
12104
12104
  }));
@@ -12110,11 +12110,11 @@ var unionProcessor = (schema, ctx, json2, params) => {
12110
12110
  };
12111
12111
  var intersectionProcessor = (schema, ctx, json2, params) => {
12112
12112
  const def = schema._zod.def;
12113
- const a = process(def.left, ctx, {
12113
+ const a = process2(def.left, ctx, {
12114
12114
  ...params,
12115
12115
  path: [...params.path, "allOf", 0]
12116
12116
  });
12117
- const b = process(def.right, ctx, {
12117
+ const b = process2(def.right, ctx, {
12118
12118
  ...params,
12119
12119
  path: [...params.path, "allOf", 1]
12120
12120
  });
@@ -12131,11 +12131,11 @@ var tupleProcessor = (schema, ctx, _json, params) => {
12131
12131
  json2.type = "array";
12132
12132
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
12133
12133
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
12134
- const prefixItems = def.items.map((x, i) => process(x, ctx, {
12134
+ const prefixItems = def.items.map((x, i) => process2(x, ctx, {
12135
12135
  ...params,
12136
12136
  path: [...params.path, prefixPath, i]
12137
12137
  }));
12138
- const rest = def.rest ? process(def.rest, ctx, {
12138
+ const rest = def.rest ? process2(def.rest, ctx, {
12139
12139
  ...params,
12140
12140
  path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
12141
12141
  }) : null;
@@ -12175,7 +12175,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
12175
12175
  const keyBag = keyType._zod.bag;
12176
12176
  const patterns = keyBag?.patterns;
12177
12177
  if (def.mode === "loose" && patterns && patterns.size > 0) {
12178
- const valueSchema = process(def.valueType, ctx, {
12178
+ const valueSchema = process2(def.valueType, ctx, {
12179
12179
  ...params,
12180
12180
  path: [...params.path, "patternProperties", "*"]
12181
12181
  });
@@ -12185,12 +12185,12 @@ var recordProcessor = (schema, ctx, _json, params) => {
12185
12185
  }
12186
12186
  } else {
12187
12187
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
12188
- json2.propertyNames = process(def.keyType, ctx, {
12188
+ json2.propertyNames = process2(def.keyType, ctx, {
12189
12189
  ...params,
12190
12190
  path: [...params.path, "propertyNames"]
12191
12191
  });
12192
12192
  }
12193
- json2.additionalProperties = process(def.valueType, ctx, {
12193
+ json2.additionalProperties = process2(def.valueType, ctx, {
12194
12194
  ...params,
12195
12195
  path: [...params.path, "additionalProperties"]
12196
12196
  });
@@ -12205,7 +12205,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
12205
12205
  };
12206
12206
  var nullableProcessor = (schema, ctx, json2, params) => {
12207
12207
  const def = schema._zod.def;
12208
- const inner = process(def.innerType, ctx, params);
12208
+ const inner = process2(def.innerType, ctx, params);
12209
12209
  const seen = ctx.seen.get(schema);
12210
12210
  if (ctx.target === "openapi-3.0") {
12211
12211
  seen.ref = def.innerType;
@@ -12216,20 +12216,20 @@ var nullableProcessor = (schema, ctx, json2, params) => {
12216
12216
  };
12217
12217
  var nonoptionalProcessor = (schema, ctx, _json, params) => {
12218
12218
  const def = schema._zod.def;
12219
- process(def.innerType, ctx, params);
12219
+ process2(def.innerType, ctx, params);
12220
12220
  const seen = ctx.seen.get(schema);
12221
12221
  seen.ref = def.innerType;
12222
12222
  };
12223
12223
  var defaultProcessor = (schema, ctx, json2, params) => {
12224
12224
  const def = schema._zod.def;
12225
- process(def.innerType, ctx, params);
12225
+ process2(def.innerType, ctx, params);
12226
12226
  const seen = ctx.seen.get(schema);
12227
12227
  seen.ref = def.innerType;
12228
12228
  json2.default = JSON.parse(JSON.stringify(def.defaultValue));
12229
12229
  };
12230
12230
  var prefaultProcessor = (schema, ctx, json2, params) => {
12231
12231
  const def = schema._zod.def;
12232
- process(def.innerType, ctx, params);
12232
+ process2(def.innerType, ctx, params);
12233
12233
  const seen = ctx.seen.get(schema);
12234
12234
  seen.ref = def.innerType;
12235
12235
  if (ctx.io === "input")
@@ -12237,7 +12237,7 @@ var prefaultProcessor = (schema, ctx, json2, params) => {
12237
12237
  };
12238
12238
  var catchProcessor = (schema, ctx, json2, params) => {
12239
12239
  const def = schema._zod.def;
12240
- process(def.innerType, ctx, params);
12240
+ process2(def.innerType, ctx, params);
12241
12241
  const seen = ctx.seen.get(schema);
12242
12242
  seen.ref = def.innerType;
12243
12243
  let catchValue;
@@ -12252,32 +12252,32 @@ var pipeProcessor = (schema, ctx, _json, params) => {
12252
12252
  const def = schema._zod.def;
12253
12253
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
12254
12254
  const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
12255
- process(innerType, ctx, params);
12255
+ process2(innerType, ctx, params);
12256
12256
  const seen = ctx.seen.get(schema);
12257
12257
  seen.ref = innerType;
12258
12258
  };
12259
12259
  var readonlyProcessor = (schema, ctx, json2, params) => {
12260
12260
  const def = schema._zod.def;
12261
- process(def.innerType, ctx, params);
12261
+ process2(def.innerType, ctx, params);
12262
12262
  const seen = ctx.seen.get(schema);
12263
12263
  seen.ref = def.innerType;
12264
12264
  json2.readOnly = true;
12265
12265
  };
12266
12266
  var promiseProcessor = (schema, ctx, _json, params) => {
12267
12267
  const def = schema._zod.def;
12268
- process(def.innerType, ctx, params);
12268
+ process2(def.innerType, ctx, params);
12269
12269
  const seen = ctx.seen.get(schema);
12270
12270
  seen.ref = def.innerType;
12271
12271
  };
12272
12272
  var optionalProcessor = (schema, ctx, _json, params) => {
12273
12273
  const def = schema._zod.def;
12274
- process(def.innerType, ctx, params);
12274
+ process2(def.innerType, ctx, params);
12275
12275
  const seen = ctx.seen.get(schema);
12276
12276
  seen.ref = def.innerType;
12277
12277
  };
12278
12278
  var lazyProcessor = (schema, ctx, _json, params) => {
12279
12279
  const innerType = schema._zod.innerType;
12280
- process(innerType, ctx, params);
12280
+ process2(innerType, ctx, params);
12281
12281
  const seen = ctx.seen.get(schema);
12282
12282
  seen.ref = innerType;
12283
12283
  };
@@ -12329,7 +12329,7 @@ function toJSONSchema(input, params) {
12329
12329
  const defs = {};
12330
12330
  for (const entry of registry2._idmap.entries()) {
12331
12331
  const [_, schema] = entry;
12332
- process(schema, ctx2);
12332
+ process2(schema, ctx2);
12333
12333
  }
12334
12334
  const schemas = {};
12335
12335
  const external = {
@@ -12352,7 +12352,7 @@ function toJSONSchema(input, params) {
12352
12352
  return { schemas };
12353
12353
  }
12354
12354
  const ctx = initializeContext({ ...params, processors: allProcessors });
12355
- process(input, ctx);
12355
+ process2(input, ctx);
12356
12356
  extractDefs(ctx, input);
12357
12357
  return finalize(ctx, input);
12358
12358
  }
@@ -12410,7 +12410,7 @@ var JSONSchemaGenerator = class {
12410
12410
  * This must be called before emit().
12411
12411
  */
12412
12412
  process(schema, _params = { path: [], schemaPath: [] }) {
12413
- return process(schema, this.ctx, _params);
12413
+ return process2(schema, this.ctx, _params);
12414
12414
  }
12415
12415
  /**
12416
12416
  * Emit the final JSON Schema after processing.
@@ -14775,6 +14775,62 @@ var MultiCameraStressResultSchema = external_exports.object({
14775
14775
  inferMsMean: external_exports.number(),
14776
14776
  errors: external_exports.number()
14777
14777
  });
14778
+ var SustainedEngineSchema = external_exports.object({
14779
+ runtime: external_exports.enum(["python", "node"]),
14780
+ backend: external_exports.string(),
14781
+ format: external_exports.enum(["onnx", "coreml", "openvino"]),
14782
+ device: external_exports.string().optional()
14783
+ }).optional();
14784
+ var SustainedNodeTargetSchema = external_exports.object({
14785
+ nodeId: external_exports.string().optional(),
14786
+ engine: SustainedEngineSchema,
14787
+ steps: external_exports.array(import_types.PipelineStepInputSchema).readonly(),
14788
+ label: external_exports.string().optional()
14789
+ });
14790
+ var SustainedThroughputInputSchema = external_exports.object({
14791
+ frameId: external_exports.string().optional(),
14792
+ imageBase64: external_exports.string().optional(),
14793
+ durationSec: external_exports.number().min(1).max(3600),
14794
+ batchSize: external_exports.number().int().min(1).max(255),
14795
+ concurrency: external_exports.number().int().min(1).max(64),
14796
+ targetSize: external_exports.number().int().min(64).max(2048).optional(),
14797
+ sessionId: external_exports.string(),
14798
+ nodes: external_exports.array(SustainedNodeTargetSchema).min(1).readonly()
14799
+ });
14800
+ var SustainedLatencyStatsSchema = external_exports.object({
14801
+ p50: external_exports.number(),
14802
+ p95: external_exports.number(),
14803
+ p99: external_exports.number(),
14804
+ mean: external_exports.number(),
14805
+ min: external_exports.number(),
14806
+ max: external_exports.number()
14807
+ });
14808
+ var SustainedNodeResultSchema = external_exports.object({
14809
+ nodeId: external_exports.string().nullable(),
14810
+ label: external_exports.string().optional(),
14811
+ /** Total frames processed by this node's loop. */
14812
+ totalFrames: external_exports.number(),
14813
+ /** Total completed batch calls. */
14814
+ totalBatches: external_exports.number(),
14815
+ /** Measured wall-clock window in ms. */
14816
+ wallMs: external_exports.number(),
14817
+ /** Sustained throughput. */
14818
+ fps: external_exports.number(),
14819
+ detsPerSec: external_exports.number(),
14820
+ /** Mean detections per frame — stable since the frame is fixed. */
14821
+ avgDetections: external_exports.number(),
14822
+ latency: SustainedLatencyStatsSchema,
14823
+ /** Batch calls that errored (transport / inference). */
14824
+ errors: external_exports.number(),
14825
+ /** True when this node ended via the stop button rather than the deadline. */
14826
+ aborted: external_exports.boolean()
14827
+ });
14828
+ var SustainedThroughputResultSchema = external_exports.object({
14829
+ perNode: external_exports.array(SustainedNodeResultSchema).readonly(),
14830
+ durationSec: external_exports.number(),
14831
+ batchSize: external_exports.number(),
14832
+ concurrency: external_exports.number()
14833
+ });
14778
14834
 
14779
14835
  // src/benchmark-actions.ts
14780
14836
  var import_types2 = require("@camstack/types");
@@ -14823,6 +14879,25 @@ var benchmarkActions = (0, import_types2.defineCustomActions)({
14823
14879
  MultiCameraStressResultSchema,
14824
14880
  { kind: "mutation" }
14825
14881
  ),
14882
+ /**
14883
+ * Sustained-throughput ROOT-detection e2e run. Pins ONE frame (decoded
14884
+ * once), then feeds the identical frame to root object-detection for
14885
+ * `durationSec` seconds — driven by BATCHES (`runPipelineBatch`,
14886
+ * `batchSize` frames/call) with `concurrency` calls in flight, fanned out
14887
+ * across `nodes`, each on its resolved runtime/model + root-only steps.
14888
+ * Returns per-node sustained fps / det/s + latency percentiles on the
14889
+ * identical pinned frame. Bounded by N + `abortSustainedThroughput`.
14890
+ */
14891
+ runSustainedThroughput: (0, import_types2.customAction)(
14892
+ SustainedThroughputInputSchema,
14893
+ SustainedThroughputResultSchema,
14894
+ { kind: "mutation" }
14895
+ ),
14896
+ abortSustainedThroughput: (0, import_types2.customAction)(
14897
+ external_exports.object({ sessionId: external_exports.string() }),
14898
+ external_exports.object({ aborted: external_exports.boolean() }),
14899
+ { kind: "mutation" }
14900
+ ),
14826
14901
  // History (shared across pipeline + decoder-perf entries)
14827
14902
  listBenchmarkHistory: (0, import_types2.customAction)(external_exports.void(), external_exports.array(HistoryEntrySchema).readonly()),
14828
14903
  saveBenchmarkResult: (0, import_types2.customAction)(
@@ -15153,10 +15228,110 @@ async function runDecoderPerfTest(input, deps) {
15153
15228
  };
15154
15229
  }
15155
15230
 
15231
+ // src/throughput/sustained-runner.ts
15232
+ async function runSustainedLoop(opts) {
15233
+ const concurrency = Math.max(1, Math.floor(opts.concurrency));
15234
+ const start = opts.now();
15235
+ const deadline = start + Math.max(0, opts.durationMs);
15236
+ const callMsList = [];
15237
+ let totalFrames = 0;
15238
+ let totalBatches = 0;
15239
+ let totalDetections = 0;
15240
+ let detectionSamples = 0;
15241
+ let errors = 0;
15242
+ const worker = async () => {
15243
+ while (!opts.shouldStop() && opts.now() < deadline) {
15244
+ const t0 = opts.now();
15245
+ try {
15246
+ const out = await opts.dispatchBatch();
15247
+ const t1 = opts.now();
15248
+ callMsList.push(t1 - t0);
15249
+ totalBatches += 1;
15250
+ totalFrames += out.frameCount;
15251
+ for (const c of out.detectionCounts) {
15252
+ totalDetections += c;
15253
+ detectionSamples += 1;
15254
+ }
15255
+ } catch {
15256
+ errors += 1;
15257
+ }
15258
+ }
15259
+ };
15260
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
15261
+ const wallMs = opts.now() - start;
15262
+ return {
15263
+ callMsList,
15264
+ totalFrames,
15265
+ totalBatches,
15266
+ totalDetections,
15267
+ detectionSamples,
15268
+ errors,
15269
+ wallMs,
15270
+ aborted: opts.shouldStop()
15271
+ };
15272
+ }
15273
+
15274
+ // src/throughput/throughput-stats.ts
15275
+ var EMPTY_LATENCY = { p50: 0, p95: 0, p99: 0, mean: 0, min: 0, max: 0 };
15276
+ function percentile(sortedAsc, p) {
15277
+ const n = sortedAsc.length;
15278
+ if (n === 0) return 0;
15279
+ const clamped = p < 0 ? 0 : p > 1 ? 1 : p;
15280
+ if (clamped <= 0) return sortedAsc[0] ?? 0;
15281
+ if (clamped >= 1) return sortedAsc[n - 1] ?? 0;
15282
+ const rank = Math.ceil(clamped * n);
15283
+ const idx = Math.min(n - 1, Math.max(0, rank - 1));
15284
+ return sortedAsc[idx] ?? 0;
15285
+ }
15286
+ function computeLatencyStats(samplesMs) {
15287
+ if (samplesMs.length === 0) return EMPTY_LATENCY;
15288
+ const sorted = [...samplesMs].sort((a, b) => a - b);
15289
+ const sum = sorted.reduce((acc, x) => acc + x, 0);
15290
+ return {
15291
+ p50: percentile(sorted, 0.5),
15292
+ p95: percentile(sorted, 0.95),
15293
+ p99: percentile(sorted, 0.99),
15294
+ mean: sum / sorted.length,
15295
+ min: sorted[0] ?? 0,
15296
+ max: sorted[sorted.length - 1] ?? 0
15297
+ };
15298
+ }
15299
+ function computeFps(totalFrames, wallMs) {
15300
+ if (wallMs <= 0) return 0;
15301
+ return totalFrames * 1e3 / wallMs;
15302
+ }
15303
+ function computeDetsPerSec(totalDetections, wallMs) {
15304
+ if (wallMs <= 0) return 0;
15305
+ return totalDetections * 1e3 / wallMs;
15306
+ }
15307
+ function computeAvgDetections(totalDetections, sampledFrames) {
15308
+ if (sampledFrames <= 0) return 0;
15309
+ return totalDetections / sampledFrames;
15310
+ }
15311
+
15156
15312
  // src/benchmark-addon.ts
15157
15313
  var KEY_BENCHMARK_HISTORY = "benchmarkHistory";
15158
15314
  var MAX_HISTORY_ENTRIES = 100;
15159
15315
  var EMPTY_HISTORY_JSON = "[]";
15316
+ var BENCH_RUN_TIMEOUT_MS = Math.max(
15317
+ 1e4,
15318
+ Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 12e4
15319
+ );
15320
+ async function withHardTimeout(promise2, ms, label) {
15321
+ let timer;
15322
+ const timeout = new Promise((_resolve, reject) => {
15323
+ timer = setTimeout(
15324
+ () => reject(new Error(`${label} exceeded ${ms}ms \u2014 aborting (likely a wedged step)`)),
15325
+ ms
15326
+ );
15327
+ timer.unref?.();
15328
+ });
15329
+ try {
15330
+ return await Promise.race([promise2, timeout]);
15331
+ } finally {
15332
+ if (timer) clearTimeout(timer);
15333
+ }
15334
+ }
15160
15335
  var HistoryArraySchema = external_exports.array(HistoryEntrySchema);
15161
15336
  var FIXTURE_META = {
15162
15337
  "camera-sim-360p.h264": {
@@ -15184,6 +15359,8 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15184
15359
  id = "benchmark";
15185
15360
  /** Per-sessionId abort flags for in-flight decoder perf runs. */
15186
15361
  decoderPerfAbortFlags = /* @__PURE__ */ new Map();
15362
+ /** Per-sessionId abort flags for in-flight sustained-throughput runs. */
15363
+ sustainedAbortFlags = /* @__PURE__ */ new Map();
15187
15364
  /**
15188
15365
  * In-memory cache of original JPEG bytes keyed by `frameId`. Populated by
15189
15366
  * `cacheBenchFrame` (UI uploads JPEG once at bench start). Subsequent
@@ -15226,6 +15403,8 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15226
15403
  runPipelineTest: async (input) => this.runPipelineTest(input),
15227
15404
  runPipelineBatchTest: async (input) => this.runPipelineBatchTest(input),
15228
15405
  runMultiCameraStressTest: async (input) => this.runMultiCameraStressTest(input),
15406
+ runSustainedThroughput: async (input) => this.runSustainedThroughput(input),
15407
+ abortSustainedThroughput: async (input) => this.abortSustainedThroughput(input),
15229
15408
  cacheBenchFrame: async (input) => this.cacheBenchFrame(input),
15230
15409
  releaseBenchFrame: async (input) => this.releaseBenchFrame(input),
15231
15410
  listBenchmarkHistory: async () => this.listBenchmarkHistory(),
@@ -15245,6 +15424,8 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15245
15424
  this.frameCacheSweeper = null;
15246
15425
  }
15247
15426
  this.frameCache.clear();
15427
+ for (const flag of this.sustainedAbortFlags.values()) flag.aborted = true;
15428
+ this.sustainedAbortFlags.clear();
15248
15429
  }
15249
15430
  // ── Frame cache (raw RGB) ────────────────────────────────────────────────
15250
15431
  async cacheBenchFrame(input) {
@@ -15314,15 +15495,19 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15314
15495
  });
15315
15496
  return globalSteps.map(toInput);
15316
15497
  })();
15317
- return this.api.pipelineExecutor.runPipeline.mutate({
15318
- nodeId,
15319
- steps: stepsToRun,
15320
- ...image ? { image } : {},
15321
- ...!image && imageBase64 ? { imageBase64 } : {},
15322
- referenceImage,
15323
- sessionId,
15324
- ...engine ? { engine } : {}
15325
- });
15498
+ return withHardTimeout(
15499
+ this.api.pipelineExecutor.runPipeline.mutate({
15500
+ nodeId,
15501
+ steps: stepsToRun,
15502
+ ...image ? { image } : {},
15503
+ ...!image && imageBase64 ? { imageBase64 } : {},
15504
+ referenceImage,
15505
+ sessionId,
15506
+ ...engine ? { engine } : {}
15507
+ }),
15508
+ BENCH_RUN_TIMEOUT_MS,
15509
+ "runPipelineTest"
15510
+ );
15326
15511
  }
15327
15512
  /**
15328
15513
  * Batched pipeline tester.
@@ -15375,13 +15560,17 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15375
15560
  timestamp: Date.now()
15376
15561
  }));
15377
15562
  const callStart = Date.now();
15378
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
15379
- nodeId,
15380
- steps: [...steps],
15381
- frames,
15382
- sessionId,
15383
- ...engine ? { engine } : {}
15384
- });
15563
+ const out = await withHardTimeout(
15564
+ this.api.pipelineExecutor.runPipelineBatch.mutate({
15565
+ nodeId,
15566
+ steps: [...steps],
15567
+ frames,
15568
+ sessionId,
15569
+ ...engine ? { engine } : {}
15570
+ }),
15571
+ BENCH_RUN_TIMEOUT_MS,
15572
+ "runPipelineBatchTest"
15573
+ );
15385
15574
  const callMs = Date.now() - callStart;
15386
15575
  const inferMs = out.results.reduce((acc, r) => Math.max(acc, r.debug?.totalInferenceMs ?? 0), 0);
15387
15576
  return {
@@ -15526,6 +15715,133 @@ var BenchmarkAddon = class extends import_types4.BaseAddon {
15526
15715
  errors
15527
15716
  };
15528
15717
  }
15718
+ // ── Sustained throughput (root-detection e2e) ────────────────────────────
15719
+ /**
15720
+ * SUSTAINED THROUGHPUT test of ROOT detection.
15721
+ *
15722
+ * 1. Pin ONE frame in memory: resolve `frameId` (from `cacheBenchFrame`)
15723
+ * or the one-shot `imageBase64`, decode + square-resize ONCE to raw
15724
+ * RGB. Every node + every iteration consumes the IDENTICAL buffer — no
15725
+ * per-iteration decode or upload.
15726
+ * 2. For each target node, run a `concurrency`-bounded loop of
15727
+ * `runPipelineBatch` (batchSize frames/call) on the node's ROOT-ONLY
15728
+ * steps until `durationSec` elapses (or the stop flag fires). Root-only
15729
+ * steps stay on the batch single-root fast path == live `plane:'frame'`
15730
+ * root detection.
15731
+ * 3. Aggregate sustained fps / det/s + latency percentiles per node on the
15732
+ * identical pinned frame for apples-to-apples comparison.
15733
+ *
15734
+ * Non-destructive: targets the benchmark's own cached frame (never a live
15735
+ * camera session), is explicitly started, and is bounded by N + the stop
15736
+ * button (`abortSustainedThroughput`).
15737
+ */
15738
+ async runSustainedThroughput(input) {
15739
+ const { frameId, imageBase64, durationSec, batchSize, concurrency, sessionId, nodes } = input;
15740
+ const targetSize = input.targetSize ?? 640;
15741
+ const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
15742
+ if (frameId && !cached2)
15743
+ throw new Error(
15744
+ `runSustainedThroughput: frameId ${frameId} not in cache (expired or never cached)`
15745
+ );
15746
+ const jpegSource = cached2 ? cached2.jpeg : imageBase64 ? Buffer.from(imageBase64, "base64") : null;
15747
+ if (!jpegSource)
15748
+ throw new Error("runSustainedThroughput: no frame source (need frameId or imageBase64)");
15749
+ const sharp = (await import("sharp")).default;
15750
+ const rgb = await sharp(jpegSource).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
15751
+ const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
15752
+ const batchFrames = Array.from({ length: batchSize }, () => ({
15753
+ data: rgbBytes,
15754
+ format: "rgb",
15755
+ width: targetSize,
15756
+ height: targetSize,
15757
+ timestamp: Date.now()
15758
+ }));
15759
+ const benchFrameId = Math.floor(Math.random() * 4294967294) + 1 >>> 0;
15760
+ const abortFlag = { aborted: false };
15761
+ this.sustainedAbortFlags.set(sessionId, abortFlag);
15762
+ this.ctx.logger.info("runSustainedThroughput start", {
15763
+ meta: {
15764
+ sessionId,
15765
+ durationSec,
15766
+ batchSize,
15767
+ concurrency,
15768
+ targetSize,
15769
+ nodeCount: nodes.length,
15770
+ nodes: nodes.map((n) => n.nodeId ?? "hub")
15771
+ }
15772
+ });
15773
+ try {
15774
+ const perNode = await Promise.all(
15775
+ nodes.map(async (node) => {
15776
+ if (node.steps.length === 0)
15777
+ throw new Error(
15778
+ `runSustainedThroughput: node ${node.nodeId ?? "hub"} has no root steps`
15779
+ );
15780
+ const dispatchBatch = async () => {
15781
+ const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
15782
+ nodeId: node.nodeId,
15783
+ steps: [...node.steps],
15784
+ frames: [...batchFrames],
15785
+ sessionId,
15786
+ frameId: benchFrameId,
15787
+ ...node.engine ? { engine: node.engine } : {}
15788
+ });
15789
+ return {
15790
+ frameCount: out.results.length,
15791
+ detectionCounts: out.results.map((r) => r.detections.length)
15792
+ };
15793
+ };
15794
+ let raw;
15795
+ try {
15796
+ raw = await runSustainedLoop({
15797
+ dispatchBatch,
15798
+ concurrency,
15799
+ durationMs: durationSec * 1e3,
15800
+ now: () => Date.now(),
15801
+ shouldStop: () => abortFlag.aborted
15802
+ });
15803
+ } finally {
15804
+ await this.api.pipelineExecutor.uncacheFrame.mutate({ nodeId: node.nodeId, frameId: benchFrameId }).catch(() => {
15805
+ });
15806
+ }
15807
+ const summary = {
15808
+ nodeId: node.nodeId ?? null,
15809
+ ...node.label ? { label: node.label } : {},
15810
+ totalFrames: raw.totalFrames,
15811
+ totalBatches: raw.totalBatches,
15812
+ wallMs: raw.wallMs,
15813
+ fps: computeFps(raw.totalFrames, raw.wallMs),
15814
+ detsPerSec: computeDetsPerSec(raw.totalDetections, raw.wallMs),
15815
+ avgDetections: computeAvgDetections(raw.totalDetections, raw.detectionSamples),
15816
+ latency: computeLatencyStats(raw.callMsList),
15817
+ errors: raw.errors,
15818
+ aborted: raw.aborted
15819
+ };
15820
+ this.ctx.logger.info("runSustainedThroughput node done", {
15821
+ meta: {
15822
+ nodeId: summary.nodeId,
15823
+ fps: Number(summary.fps.toFixed(2)),
15824
+ detsPerSec: Number(summary.detsPerSec.toFixed(2)),
15825
+ p95Ms: Number(summary.latency.p95.toFixed(2)),
15826
+ totalFrames: summary.totalFrames,
15827
+ errors: summary.errors,
15828
+ aborted: summary.aborted
15829
+ }
15830
+ });
15831
+ return summary;
15832
+ })
15833
+ );
15834
+ return { perNode, durationSec, batchSize, concurrency };
15835
+ } finally {
15836
+ this.sustainedAbortFlags.delete(sessionId);
15837
+ }
15838
+ }
15839
+ async abortSustainedThroughput(input) {
15840
+ const flag = this.sustainedAbortFlags.get(input.sessionId);
15841
+ if (!flag) return { aborted: false };
15842
+ flag.aborted = true;
15843
+ return { aborted: true };
15844
+ }
15529
15845
  // ── Decoder perf — fixture discovery + run orchestration ────────────────
15530
15846
  async listDecoderFixtures() {
15531
15847
  const dir = resolveFixturesDir();