@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.mjs CHANGED
@@ -516,7 +516,7 @@ __export(core_exports2, {
516
516
  parse: () => parse,
517
517
  parseAsync: () => parseAsync,
518
518
  prettifyError: () => prettifyError,
519
- process: () => process,
519
+ process: () => process2,
520
520
  regexes: () => regexes_exports,
521
521
  registry: () => registry,
522
522
  safeDecode: () => safeDecode,
@@ -11441,7 +11441,7 @@ function initializeContext(params) {
11441
11441
  external: params?.external ?? void 0
11442
11442
  };
11443
11443
  }
11444
- function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
11444
+ function process2(schema, ctx, _params = { path: [], schemaPath: [] }) {
11445
11445
  var _a3;
11446
11446
  const def = schema._zod.def;
11447
11447
  const seen = ctx.seen.get(schema);
@@ -11478,7 +11478,7 @@ function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
11478
11478
  if (parent) {
11479
11479
  if (!result.ref)
11480
11480
  result.ref = parent;
11481
- process(parent, ctx, params);
11481
+ process2(parent, ctx, params);
11482
11482
  ctx.seen.get(parent).isParent = true;
11483
11483
  }
11484
11484
  }
@@ -11766,14 +11766,14 @@ function isTransforming(_schema, _ctx) {
11766
11766
  }
11767
11767
  var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
11768
11768
  const ctx = initializeContext({ ...params, processors });
11769
- process(schema, ctx);
11769
+ process2(schema, ctx);
11770
11770
  extractDefs(ctx, schema);
11771
11771
  return finalize(ctx, schema);
11772
11772
  };
11773
11773
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
11774
11774
  const { libraryOptions, target } = params ?? {};
11775
11775
  const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
11776
- process(schema, ctx);
11776
+ process2(schema, ctx);
11777
11777
  extractDefs(ctx, schema);
11778
11778
  return finalize(ctx, schema);
11779
11779
  };
@@ -12019,7 +12019,7 @@ var arrayProcessor = (schema, ctx, _json, params) => {
12019
12019
  if (typeof maximum === "number")
12020
12020
  json2.maxItems = maximum;
12021
12021
  json2.type = "array";
12022
- json2.items = process(def.element, ctx, {
12022
+ json2.items = process2(def.element, ctx, {
12023
12023
  ...params,
12024
12024
  path: [...params.path, "items"]
12025
12025
  });
@@ -12031,7 +12031,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12031
12031
  json2.properties = {};
12032
12032
  const shape = def.shape;
12033
12033
  for (const key in shape) {
12034
- json2.properties[key] = process(shape[key], ctx, {
12034
+ json2.properties[key] = process2(shape[key], ctx, {
12035
12035
  ...params,
12036
12036
  path: [...params.path, "properties", key]
12037
12037
  });
@@ -12054,7 +12054,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12054
12054
  if (ctx.io === "output")
12055
12055
  json2.additionalProperties = false;
12056
12056
  } else if (def.catchall) {
12057
- json2.additionalProperties = process(def.catchall, ctx, {
12057
+ json2.additionalProperties = process2(def.catchall, ctx, {
12058
12058
  ...params,
12059
12059
  path: [...params.path, "additionalProperties"]
12060
12060
  });
@@ -12063,7 +12063,7 @@ var objectProcessor = (schema, ctx, _json, params) => {
12063
12063
  var unionProcessor = (schema, ctx, json2, params) => {
12064
12064
  const def = schema._zod.def;
12065
12065
  const isExclusive = def.inclusive === false;
12066
- const options = def.options.map((x, i) => process(x, ctx, {
12066
+ const options = def.options.map((x, i) => process2(x, ctx, {
12067
12067
  ...params,
12068
12068
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i]
12069
12069
  }));
@@ -12075,11 +12075,11 @@ var unionProcessor = (schema, ctx, json2, params) => {
12075
12075
  };
12076
12076
  var intersectionProcessor = (schema, ctx, json2, params) => {
12077
12077
  const def = schema._zod.def;
12078
- const a = process(def.left, ctx, {
12078
+ const a = process2(def.left, ctx, {
12079
12079
  ...params,
12080
12080
  path: [...params.path, "allOf", 0]
12081
12081
  });
12082
- const b = process(def.right, ctx, {
12082
+ const b = process2(def.right, ctx, {
12083
12083
  ...params,
12084
12084
  path: [...params.path, "allOf", 1]
12085
12085
  });
@@ -12096,11 +12096,11 @@ var tupleProcessor = (schema, ctx, _json, params) => {
12096
12096
  json2.type = "array";
12097
12097
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
12098
12098
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
12099
- const prefixItems = def.items.map((x, i) => process(x, ctx, {
12099
+ const prefixItems = def.items.map((x, i) => process2(x, ctx, {
12100
12100
  ...params,
12101
12101
  path: [...params.path, prefixPath, i]
12102
12102
  }));
12103
- const rest = def.rest ? process(def.rest, ctx, {
12103
+ const rest = def.rest ? process2(def.rest, ctx, {
12104
12104
  ...params,
12105
12105
  path: [...params.path, restPath, ...ctx.target === "openapi-3.0" ? [def.items.length] : []]
12106
12106
  }) : null;
@@ -12140,7 +12140,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
12140
12140
  const keyBag = keyType._zod.bag;
12141
12141
  const patterns = keyBag?.patterns;
12142
12142
  if (def.mode === "loose" && patterns && patterns.size > 0) {
12143
- const valueSchema = process(def.valueType, ctx, {
12143
+ const valueSchema = process2(def.valueType, ctx, {
12144
12144
  ...params,
12145
12145
  path: [...params.path, "patternProperties", "*"]
12146
12146
  });
@@ -12150,12 +12150,12 @@ var recordProcessor = (schema, ctx, _json, params) => {
12150
12150
  }
12151
12151
  } else {
12152
12152
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
12153
- json2.propertyNames = process(def.keyType, ctx, {
12153
+ json2.propertyNames = process2(def.keyType, ctx, {
12154
12154
  ...params,
12155
12155
  path: [...params.path, "propertyNames"]
12156
12156
  });
12157
12157
  }
12158
- json2.additionalProperties = process(def.valueType, ctx, {
12158
+ json2.additionalProperties = process2(def.valueType, ctx, {
12159
12159
  ...params,
12160
12160
  path: [...params.path, "additionalProperties"]
12161
12161
  });
@@ -12170,7 +12170,7 @@ var recordProcessor = (schema, ctx, _json, params) => {
12170
12170
  };
12171
12171
  var nullableProcessor = (schema, ctx, json2, params) => {
12172
12172
  const def = schema._zod.def;
12173
- const inner = process(def.innerType, ctx, params);
12173
+ const inner = process2(def.innerType, ctx, params);
12174
12174
  const seen = ctx.seen.get(schema);
12175
12175
  if (ctx.target === "openapi-3.0") {
12176
12176
  seen.ref = def.innerType;
@@ -12181,20 +12181,20 @@ var nullableProcessor = (schema, ctx, json2, params) => {
12181
12181
  };
12182
12182
  var nonoptionalProcessor = (schema, ctx, _json, params) => {
12183
12183
  const def = schema._zod.def;
12184
- process(def.innerType, ctx, params);
12184
+ process2(def.innerType, ctx, params);
12185
12185
  const seen = ctx.seen.get(schema);
12186
12186
  seen.ref = def.innerType;
12187
12187
  };
12188
12188
  var defaultProcessor = (schema, ctx, json2, params) => {
12189
12189
  const def = schema._zod.def;
12190
- process(def.innerType, ctx, params);
12190
+ process2(def.innerType, ctx, params);
12191
12191
  const seen = ctx.seen.get(schema);
12192
12192
  seen.ref = def.innerType;
12193
12193
  json2.default = JSON.parse(JSON.stringify(def.defaultValue));
12194
12194
  };
12195
12195
  var prefaultProcessor = (schema, ctx, json2, params) => {
12196
12196
  const def = schema._zod.def;
12197
- process(def.innerType, ctx, params);
12197
+ process2(def.innerType, ctx, params);
12198
12198
  const seen = ctx.seen.get(schema);
12199
12199
  seen.ref = def.innerType;
12200
12200
  if (ctx.io === "input")
@@ -12202,7 +12202,7 @@ var prefaultProcessor = (schema, ctx, json2, params) => {
12202
12202
  };
12203
12203
  var catchProcessor = (schema, ctx, json2, params) => {
12204
12204
  const def = schema._zod.def;
12205
- process(def.innerType, ctx, params);
12205
+ process2(def.innerType, ctx, params);
12206
12206
  const seen = ctx.seen.get(schema);
12207
12207
  seen.ref = def.innerType;
12208
12208
  let catchValue;
@@ -12217,32 +12217,32 @@ var pipeProcessor = (schema, ctx, _json, params) => {
12217
12217
  const def = schema._zod.def;
12218
12218
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
12219
12219
  const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
12220
- process(innerType, ctx, params);
12220
+ process2(innerType, ctx, params);
12221
12221
  const seen = ctx.seen.get(schema);
12222
12222
  seen.ref = innerType;
12223
12223
  };
12224
12224
  var readonlyProcessor = (schema, ctx, json2, params) => {
12225
12225
  const def = schema._zod.def;
12226
- process(def.innerType, ctx, params);
12226
+ process2(def.innerType, ctx, params);
12227
12227
  const seen = ctx.seen.get(schema);
12228
12228
  seen.ref = def.innerType;
12229
12229
  json2.readOnly = true;
12230
12230
  };
12231
12231
  var promiseProcessor = (schema, ctx, _json, params) => {
12232
12232
  const def = schema._zod.def;
12233
- process(def.innerType, ctx, params);
12233
+ process2(def.innerType, ctx, params);
12234
12234
  const seen = ctx.seen.get(schema);
12235
12235
  seen.ref = def.innerType;
12236
12236
  };
12237
12237
  var optionalProcessor = (schema, ctx, _json, params) => {
12238
12238
  const def = schema._zod.def;
12239
- process(def.innerType, ctx, params);
12239
+ process2(def.innerType, ctx, params);
12240
12240
  const seen = ctx.seen.get(schema);
12241
12241
  seen.ref = def.innerType;
12242
12242
  };
12243
12243
  var lazyProcessor = (schema, ctx, _json, params) => {
12244
12244
  const innerType = schema._zod.innerType;
12245
- process(innerType, ctx, params);
12245
+ process2(innerType, ctx, params);
12246
12246
  const seen = ctx.seen.get(schema);
12247
12247
  seen.ref = innerType;
12248
12248
  };
@@ -12294,7 +12294,7 @@ function toJSONSchema(input, params) {
12294
12294
  const defs = {};
12295
12295
  for (const entry of registry2._idmap.entries()) {
12296
12296
  const [_, schema] = entry;
12297
- process(schema, ctx2);
12297
+ process2(schema, ctx2);
12298
12298
  }
12299
12299
  const schemas = {};
12300
12300
  const external = {
@@ -12317,7 +12317,7 @@ function toJSONSchema(input, params) {
12317
12317
  return { schemas };
12318
12318
  }
12319
12319
  const ctx = initializeContext({ ...params, processors: allProcessors });
12320
- process(input, ctx);
12320
+ process2(input, ctx);
12321
12321
  extractDefs(ctx, input);
12322
12322
  return finalize(ctx, input);
12323
12323
  }
@@ -12375,7 +12375,7 @@ var JSONSchemaGenerator = class {
12375
12375
  * This must be called before emit().
12376
12376
  */
12377
12377
  process(schema, _params = { path: [], schemaPath: [] }) {
12378
- return process(schema, this.ctx, _params);
12378
+ return process2(schema, this.ctx, _params);
12379
12379
  }
12380
12380
  /**
12381
12381
  * Emit the final JSON Schema after processing.
@@ -14740,6 +14740,62 @@ var MultiCameraStressResultSchema = external_exports.object({
14740
14740
  inferMsMean: external_exports.number(),
14741
14741
  errors: external_exports.number()
14742
14742
  });
14743
+ var SustainedEngineSchema = external_exports.object({
14744
+ runtime: external_exports.enum(["python", "node"]),
14745
+ backend: external_exports.string(),
14746
+ format: external_exports.enum(["onnx", "coreml", "openvino"]),
14747
+ device: external_exports.string().optional()
14748
+ }).optional();
14749
+ var SustainedNodeTargetSchema = external_exports.object({
14750
+ nodeId: external_exports.string().optional(),
14751
+ engine: SustainedEngineSchema,
14752
+ steps: external_exports.array(PipelineStepInputSchema).readonly(),
14753
+ label: external_exports.string().optional()
14754
+ });
14755
+ var SustainedThroughputInputSchema = external_exports.object({
14756
+ frameId: external_exports.string().optional(),
14757
+ imageBase64: external_exports.string().optional(),
14758
+ durationSec: external_exports.number().min(1).max(3600),
14759
+ batchSize: external_exports.number().int().min(1).max(255),
14760
+ concurrency: external_exports.number().int().min(1).max(64),
14761
+ targetSize: external_exports.number().int().min(64).max(2048).optional(),
14762
+ sessionId: external_exports.string(),
14763
+ nodes: external_exports.array(SustainedNodeTargetSchema).min(1).readonly()
14764
+ });
14765
+ var SustainedLatencyStatsSchema = external_exports.object({
14766
+ p50: external_exports.number(),
14767
+ p95: external_exports.number(),
14768
+ p99: external_exports.number(),
14769
+ mean: external_exports.number(),
14770
+ min: external_exports.number(),
14771
+ max: external_exports.number()
14772
+ });
14773
+ var SustainedNodeResultSchema = external_exports.object({
14774
+ nodeId: external_exports.string().nullable(),
14775
+ label: external_exports.string().optional(),
14776
+ /** Total frames processed by this node's loop. */
14777
+ totalFrames: external_exports.number(),
14778
+ /** Total completed batch calls. */
14779
+ totalBatches: external_exports.number(),
14780
+ /** Measured wall-clock window in ms. */
14781
+ wallMs: external_exports.number(),
14782
+ /** Sustained throughput. */
14783
+ fps: external_exports.number(),
14784
+ detsPerSec: external_exports.number(),
14785
+ /** Mean detections per frame — stable since the frame is fixed. */
14786
+ avgDetections: external_exports.number(),
14787
+ latency: SustainedLatencyStatsSchema,
14788
+ /** Batch calls that errored (transport / inference). */
14789
+ errors: external_exports.number(),
14790
+ /** True when this node ended via the stop button rather than the deadline. */
14791
+ aborted: external_exports.boolean()
14792
+ });
14793
+ var SustainedThroughputResultSchema = external_exports.object({
14794
+ perNode: external_exports.array(SustainedNodeResultSchema).readonly(),
14795
+ durationSec: external_exports.number(),
14796
+ batchSize: external_exports.number(),
14797
+ concurrency: external_exports.number()
14798
+ });
14743
14799
 
14744
14800
  // src/benchmark-actions.ts
14745
14801
  import { customAction, defineCustomActions, PipelineRunResultBridge } from "@camstack/types";
@@ -14788,6 +14844,25 @@ var benchmarkActions = defineCustomActions({
14788
14844
  MultiCameraStressResultSchema,
14789
14845
  { kind: "mutation" }
14790
14846
  ),
14847
+ /**
14848
+ * Sustained-throughput ROOT-detection e2e run. Pins ONE frame (decoded
14849
+ * once), then feeds the identical frame to root object-detection for
14850
+ * `durationSec` seconds — driven by BATCHES (`runPipelineBatch`,
14851
+ * `batchSize` frames/call) with `concurrency` calls in flight, fanned out
14852
+ * across `nodes`, each on its resolved runtime/model + root-only steps.
14853
+ * Returns per-node sustained fps / det/s + latency percentiles on the
14854
+ * identical pinned frame. Bounded by N + `abortSustainedThroughput`.
14855
+ */
14856
+ runSustainedThroughput: customAction(
14857
+ SustainedThroughputInputSchema,
14858
+ SustainedThroughputResultSchema,
14859
+ { kind: "mutation" }
14860
+ ),
14861
+ abortSustainedThroughput: customAction(
14862
+ external_exports.object({ sessionId: external_exports.string() }),
14863
+ external_exports.object({ aborted: external_exports.boolean() }),
14864
+ { kind: "mutation" }
14865
+ ),
14791
14866
  // History (shared across pipeline + decoder-perf entries)
14792
14867
  listBenchmarkHistory: customAction(external_exports.void(), external_exports.array(HistoryEntrySchema).readonly()),
14793
14868
  saveBenchmarkResult: customAction(
@@ -15118,10 +15193,110 @@ async function runDecoderPerfTest(input, deps) {
15118
15193
  };
15119
15194
  }
15120
15195
 
15196
+ // src/throughput/sustained-runner.ts
15197
+ async function runSustainedLoop(opts) {
15198
+ const concurrency = Math.max(1, Math.floor(opts.concurrency));
15199
+ const start = opts.now();
15200
+ const deadline = start + Math.max(0, opts.durationMs);
15201
+ const callMsList = [];
15202
+ let totalFrames = 0;
15203
+ let totalBatches = 0;
15204
+ let totalDetections = 0;
15205
+ let detectionSamples = 0;
15206
+ let errors = 0;
15207
+ const worker = async () => {
15208
+ while (!opts.shouldStop() && opts.now() < deadline) {
15209
+ const t0 = opts.now();
15210
+ try {
15211
+ const out = await opts.dispatchBatch();
15212
+ const t1 = opts.now();
15213
+ callMsList.push(t1 - t0);
15214
+ totalBatches += 1;
15215
+ totalFrames += out.frameCount;
15216
+ for (const c of out.detectionCounts) {
15217
+ totalDetections += c;
15218
+ detectionSamples += 1;
15219
+ }
15220
+ } catch {
15221
+ errors += 1;
15222
+ }
15223
+ }
15224
+ };
15225
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
15226
+ const wallMs = opts.now() - start;
15227
+ return {
15228
+ callMsList,
15229
+ totalFrames,
15230
+ totalBatches,
15231
+ totalDetections,
15232
+ detectionSamples,
15233
+ errors,
15234
+ wallMs,
15235
+ aborted: opts.shouldStop()
15236
+ };
15237
+ }
15238
+
15239
+ // src/throughput/throughput-stats.ts
15240
+ var EMPTY_LATENCY = { p50: 0, p95: 0, p99: 0, mean: 0, min: 0, max: 0 };
15241
+ function percentile(sortedAsc, p) {
15242
+ const n = sortedAsc.length;
15243
+ if (n === 0) return 0;
15244
+ const clamped = p < 0 ? 0 : p > 1 ? 1 : p;
15245
+ if (clamped <= 0) return sortedAsc[0] ?? 0;
15246
+ if (clamped >= 1) return sortedAsc[n - 1] ?? 0;
15247
+ const rank = Math.ceil(clamped * n);
15248
+ const idx = Math.min(n - 1, Math.max(0, rank - 1));
15249
+ return sortedAsc[idx] ?? 0;
15250
+ }
15251
+ function computeLatencyStats(samplesMs) {
15252
+ if (samplesMs.length === 0) return EMPTY_LATENCY;
15253
+ const sorted = [...samplesMs].sort((a, b) => a - b);
15254
+ const sum = sorted.reduce((acc, x) => acc + x, 0);
15255
+ return {
15256
+ p50: percentile(sorted, 0.5),
15257
+ p95: percentile(sorted, 0.95),
15258
+ p99: percentile(sorted, 0.99),
15259
+ mean: sum / sorted.length,
15260
+ min: sorted[0] ?? 0,
15261
+ max: sorted[sorted.length - 1] ?? 0
15262
+ };
15263
+ }
15264
+ function computeFps(totalFrames, wallMs) {
15265
+ if (wallMs <= 0) return 0;
15266
+ return totalFrames * 1e3 / wallMs;
15267
+ }
15268
+ function computeDetsPerSec(totalDetections, wallMs) {
15269
+ if (wallMs <= 0) return 0;
15270
+ return totalDetections * 1e3 / wallMs;
15271
+ }
15272
+ function computeAvgDetections(totalDetections, sampledFrames) {
15273
+ if (sampledFrames <= 0) return 0;
15274
+ return totalDetections / sampledFrames;
15275
+ }
15276
+
15121
15277
  // src/benchmark-addon.ts
15122
15278
  var KEY_BENCHMARK_HISTORY = "benchmarkHistory";
15123
15279
  var MAX_HISTORY_ENTRIES = 100;
15124
15280
  var EMPTY_HISTORY_JSON = "[]";
15281
+ var BENCH_RUN_TIMEOUT_MS = Math.max(
15282
+ 1e4,
15283
+ Number(process.env["CAMSTACK_BENCH_RUN_TIMEOUT_MS"]) || 12e4
15284
+ );
15285
+ async function withHardTimeout(promise2, ms, label) {
15286
+ let timer;
15287
+ const timeout = new Promise((_resolve, reject) => {
15288
+ timer = setTimeout(
15289
+ () => reject(new Error(`${label} exceeded ${ms}ms \u2014 aborting (likely a wedged step)`)),
15290
+ ms
15291
+ );
15292
+ timer.unref?.();
15293
+ });
15294
+ try {
15295
+ return await Promise.race([promise2, timeout]);
15296
+ } finally {
15297
+ if (timer) clearTimeout(timer);
15298
+ }
15299
+ }
15125
15300
  var HistoryArraySchema = external_exports.array(HistoryEntrySchema);
15126
15301
  var FIXTURE_META = {
15127
15302
  "camera-sim-360p.h264": {
@@ -15149,6 +15324,8 @@ var BenchmarkAddon = class extends BaseAddon {
15149
15324
  id = "benchmark";
15150
15325
  /** Per-sessionId abort flags for in-flight decoder perf runs. */
15151
15326
  decoderPerfAbortFlags = /* @__PURE__ */ new Map();
15327
+ /** Per-sessionId abort flags for in-flight sustained-throughput runs. */
15328
+ sustainedAbortFlags = /* @__PURE__ */ new Map();
15152
15329
  /**
15153
15330
  * In-memory cache of original JPEG bytes keyed by `frameId`. Populated by
15154
15331
  * `cacheBenchFrame` (UI uploads JPEG once at bench start). Subsequent
@@ -15191,6 +15368,8 @@ var BenchmarkAddon = class extends BaseAddon {
15191
15368
  runPipelineTest: async (input) => this.runPipelineTest(input),
15192
15369
  runPipelineBatchTest: async (input) => this.runPipelineBatchTest(input),
15193
15370
  runMultiCameraStressTest: async (input) => this.runMultiCameraStressTest(input),
15371
+ runSustainedThroughput: async (input) => this.runSustainedThroughput(input),
15372
+ abortSustainedThroughput: async (input) => this.abortSustainedThroughput(input),
15194
15373
  cacheBenchFrame: async (input) => this.cacheBenchFrame(input),
15195
15374
  releaseBenchFrame: async (input) => this.releaseBenchFrame(input),
15196
15375
  listBenchmarkHistory: async () => this.listBenchmarkHistory(),
@@ -15210,6 +15389,8 @@ var BenchmarkAddon = class extends BaseAddon {
15210
15389
  this.frameCacheSweeper = null;
15211
15390
  }
15212
15391
  this.frameCache.clear();
15392
+ for (const flag of this.sustainedAbortFlags.values()) flag.aborted = true;
15393
+ this.sustainedAbortFlags.clear();
15213
15394
  }
15214
15395
  // ── Frame cache (raw RGB) ────────────────────────────────────────────────
15215
15396
  async cacheBenchFrame(input) {
@@ -15279,15 +15460,19 @@ var BenchmarkAddon = class extends BaseAddon {
15279
15460
  });
15280
15461
  return globalSteps.map(toInput);
15281
15462
  })();
15282
- return this.api.pipelineExecutor.runPipeline.mutate({
15283
- nodeId,
15284
- steps: stepsToRun,
15285
- ...image ? { image } : {},
15286
- ...!image && imageBase64 ? { imageBase64 } : {},
15287
- referenceImage,
15288
- sessionId,
15289
- ...engine ? { engine } : {}
15290
- });
15463
+ return withHardTimeout(
15464
+ this.api.pipelineExecutor.runPipeline.mutate({
15465
+ nodeId,
15466
+ steps: stepsToRun,
15467
+ ...image ? { image } : {},
15468
+ ...!image && imageBase64 ? { imageBase64 } : {},
15469
+ referenceImage,
15470
+ sessionId,
15471
+ ...engine ? { engine } : {}
15472
+ }),
15473
+ BENCH_RUN_TIMEOUT_MS,
15474
+ "runPipelineTest"
15475
+ );
15291
15476
  }
15292
15477
  /**
15293
15478
  * Batched pipeline tester.
@@ -15340,13 +15525,17 @@ var BenchmarkAddon = class extends BaseAddon {
15340
15525
  timestamp: Date.now()
15341
15526
  }));
15342
15527
  const callStart = Date.now();
15343
- const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
15344
- nodeId,
15345
- steps: [...steps],
15346
- frames,
15347
- sessionId,
15348
- ...engine ? { engine } : {}
15349
- });
15528
+ const out = await withHardTimeout(
15529
+ this.api.pipelineExecutor.runPipelineBatch.mutate({
15530
+ nodeId,
15531
+ steps: [...steps],
15532
+ frames,
15533
+ sessionId,
15534
+ ...engine ? { engine } : {}
15535
+ }),
15536
+ BENCH_RUN_TIMEOUT_MS,
15537
+ "runPipelineBatchTest"
15538
+ );
15350
15539
  const callMs = Date.now() - callStart;
15351
15540
  const inferMs = out.results.reduce((acc, r) => Math.max(acc, r.debug?.totalInferenceMs ?? 0), 0);
15352
15541
  return {
@@ -15491,6 +15680,133 @@ var BenchmarkAddon = class extends BaseAddon {
15491
15680
  errors
15492
15681
  };
15493
15682
  }
15683
+ // ── Sustained throughput (root-detection e2e) ────────────────────────────
15684
+ /**
15685
+ * SUSTAINED THROUGHPUT test of ROOT detection.
15686
+ *
15687
+ * 1. Pin ONE frame in memory: resolve `frameId` (from `cacheBenchFrame`)
15688
+ * or the one-shot `imageBase64`, decode + square-resize ONCE to raw
15689
+ * RGB. Every node + every iteration consumes the IDENTICAL buffer — no
15690
+ * per-iteration decode or upload.
15691
+ * 2. For each target node, run a `concurrency`-bounded loop of
15692
+ * `runPipelineBatch` (batchSize frames/call) on the node's ROOT-ONLY
15693
+ * steps until `durationSec` elapses (or the stop flag fires). Root-only
15694
+ * steps stay on the batch single-root fast path == live `plane:'frame'`
15695
+ * root detection.
15696
+ * 3. Aggregate sustained fps / det/s + latency percentiles per node on the
15697
+ * identical pinned frame for apples-to-apples comparison.
15698
+ *
15699
+ * Non-destructive: targets the benchmark's own cached frame (never a live
15700
+ * camera session), is explicitly started, and is bounded by N + the stop
15701
+ * button (`abortSustainedThroughput`).
15702
+ */
15703
+ async runSustainedThroughput(input) {
15704
+ const { frameId, imageBase64, durationSec, batchSize, concurrency, sessionId, nodes } = input;
15705
+ const targetSize = input.targetSize ?? 640;
15706
+ const cached2 = frameId ? this.frameCache.get(frameId) : void 0;
15707
+ if (frameId && !cached2)
15708
+ throw new Error(
15709
+ `runSustainedThroughput: frameId ${frameId} not in cache (expired or never cached)`
15710
+ );
15711
+ const jpegSource = cached2 ? cached2.jpeg : imageBase64 ? Buffer.from(imageBase64, "base64") : null;
15712
+ if (!jpegSource)
15713
+ throw new Error("runSustainedThroughput: no frame source (need frameId or imageBase64)");
15714
+ const sharp = (await import("sharp")).default;
15715
+ const rgb = await sharp(jpegSource).resize({ width: targetSize, height: targetSize, fit: "fill" }).raw().toColorspace("srgb").toBuffer();
15716
+ const rgbBytes = new Uint8Array(rgb.buffer, rgb.byteOffset, rgb.byteLength);
15717
+ const batchFrames = Array.from({ length: batchSize }, () => ({
15718
+ data: rgbBytes,
15719
+ format: "rgb",
15720
+ width: targetSize,
15721
+ height: targetSize,
15722
+ timestamp: Date.now()
15723
+ }));
15724
+ const benchFrameId = Math.floor(Math.random() * 4294967294) + 1 >>> 0;
15725
+ const abortFlag = { aborted: false };
15726
+ this.sustainedAbortFlags.set(sessionId, abortFlag);
15727
+ this.ctx.logger.info("runSustainedThroughput start", {
15728
+ meta: {
15729
+ sessionId,
15730
+ durationSec,
15731
+ batchSize,
15732
+ concurrency,
15733
+ targetSize,
15734
+ nodeCount: nodes.length,
15735
+ nodes: nodes.map((n) => n.nodeId ?? "hub")
15736
+ }
15737
+ });
15738
+ try {
15739
+ const perNode = await Promise.all(
15740
+ nodes.map(async (node) => {
15741
+ if (node.steps.length === 0)
15742
+ throw new Error(
15743
+ `runSustainedThroughput: node ${node.nodeId ?? "hub"} has no root steps`
15744
+ );
15745
+ const dispatchBatch = async () => {
15746
+ const out = await this.api.pipelineExecutor.runPipelineBatch.mutate({
15747
+ nodeId: node.nodeId,
15748
+ steps: [...node.steps],
15749
+ frames: [...batchFrames],
15750
+ sessionId,
15751
+ frameId: benchFrameId,
15752
+ ...node.engine ? { engine: node.engine } : {}
15753
+ });
15754
+ return {
15755
+ frameCount: out.results.length,
15756
+ detectionCounts: out.results.map((r) => r.detections.length)
15757
+ };
15758
+ };
15759
+ let raw;
15760
+ try {
15761
+ raw = await runSustainedLoop({
15762
+ dispatchBatch,
15763
+ concurrency,
15764
+ durationMs: durationSec * 1e3,
15765
+ now: () => Date.now(),
15766
+ shouldStop: () => abortFlag.aborted
15767
+ });
15768
+ } finally {
15769
+ await this.api.pipelineExecutor.uncacheFrame.mutate({ nodeId: node.nodeId, frameId: benchFrameId }).catch(() => {
15770
+ });
15771
+ }
15772
+ const summary = {
15773
+ nodeId: node.nodeId ?? null,
15774
+ ...node.label ? { label: node.label } : {},
15775
+ totalFrames: raw.totalFrames,
15776
+ totalBatches: raw.totalBatches,
15777
+ wallMs: raw.wallMs,
15778
+ fps: computeFps(raw.totalFrames, raw.wallMs),
15779
+ detsPerSec: computeDetsPerSec(raw.totalDetections, raw.wallMs),
15780
+ avgDetections: computeAvgDetections(raw.totalDetections, raw.detectionSamples),
15781
+ latency: computeLatencyStats(raw.callMsList),
15782
+ errors: raw.errors,
15783
+ aborted: raw.aborted
15784
+ };
15785
+ this.ctx.logger.info("runSustainedThroughput node done", {
15786
+ meta: {
15787
+ nodeId: summary.nodeId,
15788
+ fps: Number(summary.fps.toFixed(2)),
15789
+ detsPerSec: Number(summary.detsPerSec.toFixed(2)),
15790
+ p95Ms: Number(summary.latency.p95.toFixed(2)),
15791
+ totalFrames: summary.totalFrames,
15792
+ errors: summary.errors,
15793
+ aborted: summary.aborted
15794
+ }
15795
+ });
15796
+ return summary;
15797
+ })
15798
+ );
15799
+ return { perNode, durationSec, batchSize, concurrency };
15800
+ } finally {
15801
+ this.sustainedAbortFlags.delete(sessionId);
15802
+ }
15803
+ }
15804
+ async abortSustainedThroughput(input) {
15805
+ const flag = this.sustainedAbortFlags.get(input.sessionId);
15806
+ if (!flag) return { aborted: false };
15807
+ flag.aborted = true;
15808
+ return { aborted: true };
15809
+ }
15494
15810
  // ── Decoder perf — fixture discovery + run orchestration ────────────────
15495
15811
  async listDecoderFixtures() {
15496
15812
  const dir = resolveFixturesDir();