@camstack/addon-pipeline 1.2.345 → 1.2.347

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-C7zLb7ze.mjs → custom-model-catalog-B_3TaGyS.mjs} +2 -2
  4. package/dist/{custom-model-catalog-CrEsRBe_.js → custom-model-catalog-VPiA2VLl.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +8 -8
  7. package/dist/{dist-_I6JkuJ8.js → dist-BK74zWWV.js} +3774 -111
  8. package/dist/{dist-cT7hhXh9.mjs → dist-mCRj-pzB.mjs} +3745 -112
  9. package/dist/{model-catalogs-BrKLOMBe.js → model-catalogs-0h3q8aik.js} +15 -4
  10. package/dist/{model-catalogs-CPAmiLRa.mjs → model-catalogs-CwzQMyNC.mjs} +15 -4
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-Dmga8QNL.mjs → node-BbmYK6W-.mjs} +1 -1
  14. package/dist/{node-6zL5ddry.js → node-BdTn3tlc.js} +1 -1
  15. package/dist/pipeline-runner/index.js +733 -6
  16. package/dist/pipeline-runner/index.mjs +739 -12
  17. package/dist/{process-memory-CstCtC3A.js → process-memory-CnZ_X4eQ.js} +1 -1
  18. package/dist/{process-memory-DZcnBnb1.mjs → process-memory-bOQf34ya.mjs} +1 -1
  19. package/dist/recorder/index.js +27 -7
  20. package/dist/recorder/index.mjs +27 -7
  21. package/dist/{segment-demux-js-qzo-pQK7.mjs → segment-demux-js-5MO-ia8o.mjs} +1 -1
  22. package/dist/{segment-demux-js-DPtDBph6.js → segment-demux-js-C4jnKQoX.js} +1 -1
  23. package/dist/stream-broker/_stub.js +2 -2
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Drg8iwTA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C8GBSIhY.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CNhTe1NS.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-uvybMzaE.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Bz5eB0VL.mjs → hostInit-IWX_cy7U.mjs} +3 -3
  30. package/dist/stream-broker/index.js +3 -3
  31. package/dist/stream-broker/index.mjs +5 -5
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C2VoXjqc.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DATpA6mb.mjs +0 -26
@@ -1,12 +1,16 @@
1
- import { $ as cameraOwnsMotionViaSensor, A as NativeLeaseAdmissionSchema, At as linkedStepOf, Dn as errMsg, Fn as defineCustomActions, Gn as _enum, Gt as pickDetailCropConvention, Jt as pipelineRunnerCapability, Kn as array, Kt as pickNativeLeaseOverride, Nn as createEvent, Pn as customAction, Qn as object, T as FailureCounters, Vn as nodePin, Xn as literal, Yn as lazy, Zn as number, _ as DEFAULT_DETAIL_CROP_CONVENTION, a as BackgroundRunRegistry, cn as rootSuppliesOf, ft as deriveRootFaceRefineRect, in as resolveModelBoundSetting, j as NativeLeaseSettingsSchema, jt as loadContributionCapability, kn as BaseAddon, m as CROP_SUPPLY_SOURCES, q as allowedSuppliedEffect, qn as boolean, rr as EventCategory, sn as resolveRootSupply, tr as string, ut as deriveDetailCropRect, xt as failureContributionCapability, y as DEFAULT_NATIVE_LEASE_SETTINGS, z as ROOT_SUPPLIABLE_STEP_IDS } from "../dist-cT7hhXh9.mjs";
2
- import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Dmga8QNL.mjs";
3
- import { a as stepsForElectedPool, b as getStepDefinition, d as rootObjectGate, f as localFrameRegistry, h as resolveFrameViewGeometry, i as placeDetailStep, l as resolveDefaultDetectionModel, m as createSessionDecodeCoordinator, p as isSessionDecodeReadGone, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, y as getStep } from "../custom-model-catalog-C7zLb7ze.mjs";
1
+ import { $t as pipelineRunnerCapability, Bn as defineCustomActions, E as FailureCounters, Et as failureContributionCapability, Ft as linkedStepOf, I as PIPELINE_RAMP, It as loadContributionCapability, Kn as nodePin, M as NativeLeaseAdmissionSchema, Mn as errMsg, N as NativeLeaseSettingsSchema, P as NodeBenchRunnerInputSchema, Pn as BaseAddon, Qn as boolean, Rn as createEvent, U as ROOT_SUPPLIABLE_STEP_IDS, Xn as _enum, Xt as pickDetailCropConvention, Z as allowedSuppliedEffect, Zn as array, Zt as pickNativeLeaseOverride, _ as DEFAULT_DETAIL_CROP_CONVENTION, _t as deriveRootFaceRefineRect, a as BackgroundRunRegistry, cr as EventCategory, er as lazy, fn as resolveRootSupply, ht as deriveDetailCropRect, it as cameraOwnsMotionViaSensor, j as MODEL_FORMATS, ln as resolveModelBoundSetting, m as CROP_SUPPLY_SOURCES, nr as number, or as string, pn as rootSuppliesOf, rr as object, rt as buildBenchStepTree, tr as literal, x as DEVICE_RAMP, y as DEFAULT_NATIVE_LEASE_SETTINGS, zn as customAction } from "../dist-mCRj-pzB.mjs";
2
+ import { i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-BbmYK6W-.mjs";
3
+ import { a as stepsForElectedPool, b as getStepDefinition, d as rootObjectGate, f as localFrameRegistry, h as resolveFrameViewGeometry, i as placeDetailStep, l as resolveDefaultDetectionModel, m as createSessionDecodeCoordinator, p as isSessionDecodeReadGone, s as resolveDeviceEngine, t as CustomModelCatalog, u as macroGateVerdict, y as getStep } from "../custom-model-catalog-B_3TaGyS.mjs";
4
4
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
5
5
  import { t as getSharp } from "../lazy-sharp-DXsqwpph.mjs";
6
6
  import { c as withDetectionIntent, o as preRollRequestOf, t as DETECTION_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
7
7
  import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
8
- import * as os from "node:os";
9
- import { fork } from "node:child_process";
8
+ import * as os$1 from "node:os";
9
+ import os from "node:os";
10
+ import { createHash } from "node:crypto";
11
+ import { createReadStream, promises } from "node:fs";
12
+ import path from "node:path";
13
+ import { fork, spawn } from "node:child_process";
10
14
  import sharp from "sharp";
11
15
  import { fileURLToPath } from "node:url";
12
16
  import { PerformanceObserver } from "node:perf_hooks";
@@ -3173,6 +3177,7 @@ async function executeCapacityRamp(opts) {
3173
3177
  onMeasureStart: () => {
3174
3178
  stallStart = opts.stallSnapshot();
3175
3179
  rssStart = opts.rssMb();
3180
+ opts.onMeasureStartHook?.();
3176
3181
  opts.onStep?.({
3177
3182
  stepIndex,
3178
3183
  cameras,
@@ -3181,15 +3186,25 @@ async function executeCapacityRamp(opts) {
3181
3186
  }
3182
3187
  });
3183
3188
  const stall = diffStallWindow(stallStart, opts.stallSnapshot());
3184
- const verdict = evaluateCapacityStep(raw, stall, opts.slo);
3185
- const step = {
3189
+ const evaluated = evaluateCapacityStep(raw, stall, opts.slo);
3190
+ const base = {
3186
3191
  cameras,
3187
3192
  raw,
3188
3193
  stall,
3189
- verdict,
3194
+ verdict: evaluated,
3190
3195
  rssStartMb: rssStart,
3191
3196
  rssEndMb: opts.rssMb()
3192
3197
  };
3198
+ const extra = opts.extraFailReasons?.(base) ?? [];
3199
+ const verdict = extra.length === 0 ? evaluated : {
3200
+ ...evaluated,
3201
+ pass: false,
3202
+ failReasons: [...evaluated.failReasons, ...extra]
3203
+ };
3204
+ const step = {
3205
+ ...base,
3206
+ verdict
3207
+ };
3193
3208
  steps.push(step);
3194
3209
  firstClass += raw.firstClassShed;
3195
3210
  fallback += raw.fallbackShed;
@@ -4526,7 +4541,7 @@ function selectExecutorFrameSource(coLocated, frameRef, inlineFrame) {
4526
4541
  * device balancer weights on. A null probe means "could not tell", which reports
4527
4542
  * as `false` — the same conservative floor the engine cascade takes.
4528
4543
  */
4529
- function summarizeRunnerHardware(hardware, cpuCores = os.cpus().length) {
4544
+ function summarizeRunnerHardware(hardware, cpuCores = os$1.cpus().length) {
4530
4545
  return {
4531
4546
  hasGpu: hardware?.gpu != null,
4532
4547
  cpuCores
@@ -7352,6 +7367,699 @@ async function runStatelessStep(deps, input) {
7352
7367
  details
7353
7368
  };
7354
7369
  }
7370
+ //#endregion
7371
+ //#region src/pipeline-runner/node-bench/bench-frames.ts
7372
+ /**
7373
+ * The frames a node benchmark runs on (D791), decoded ONCE on the node into
7374
+ * RAM at the detection size — never shipped across a process per call (D9).
7375
+ *
7376
+ * Source, in order:
7377
+ * 1. the public bench clip (`camstack-bench-v1`, CC-licensed composite with
7378
+ * a synthetic plate — `docs/benchmarks/bench-clip.md`), downloaded once
7379
+ * into the runner's data dir and accepted ONLY when its sha256 matches
7380
+ * the pinned revision; decoded with the node's pinned ffmpeg;
7381
+ * 2. otherwise the executor's reference images (always on the node). The
7382
+ * run says which (`clip.id`), so a result never claims the clip it did
7383
+ * not use.
7384
+ */
7385
+ /**
7386
+ * The published clip, pinned to the exact build uploaded to the HF dataset
7387
+ * repo (`scripts/bench-clip/build-bench-clip.sh` prints the hash). A download
7388
+ * that fails (not uploaded yet) or whose bytes differ falls back to the
7389
+ * reference images, and the run says so. `sha256: null` disables the clip.
7390
+ */
7391
+ var BENCH_CLIP = {
7392
+ id: "camstack-bench-v1",
7393
+ revision: "v1",
7394
+ fileName: "camstack-bench-v1-h264-1080p.mp4",
7395
+ url: "https://huggingface.co/datasets/camstack/camstack-bench-clips/resolve/main/camstack-bench-v1/camstack-bench-v1-h264-1080p.mp4",
7396
+ sha256: "677eea6ccb5711dafe70ac7259aa152b8fad3e5704da1a8a8c66e6b1717f1434"
7397
+ };
7398
+ async function sha256Of(file) {
7399
+ const hash = createHash("sha256");
7400
+ for await (const chunk of createReadStream(file)) hash.update(chunk);
7401
+ return hash.digest("hex");
7402
+ }
7403
+ /** The verified clip on disk, downloading it when absent or wrong. Null when unavailable. */
7404
+ async function ensureBenchClip(deps) {
7405
+ const clip = deps.clip ?? BENCH_CLIP;
7406
+ if (clip.sha256 === null) return null;
7407
+ const dir = path.join(deps.dataDir, "bench");
7408
+ const file = path.join(dir, clip.fileName);
7409
+ try {
7410
+ if (await sha256Of(file) === clip.sha256) return file;
7411
+ } catch {}
7412
+ await promises.mkdir(dir, { recursive: true });
7413
+ const staged = `${file}.downloading`;
7414
+ const res = await deps.fetch(clip.url);
7415
+ if (!res.ok || res.body === null) throw new Error(`bench clip download: HTTP ${res.status}`);
7416
+ await promises.writeFile(staged, new Uint8Array(await res.arrayBuffer()));
7417
+ const got = await sha256Of(staged);
7418
+ if (got !== clip.sha256) {
7419
+ await promises.rm(staged, { force: true });
7420
+ throw new Error(`bench clip download: sha256 ${got} is not the pinned ${clip.sha256}`);
7421
+ }
7422
+ await promises.rename(staged, file);
7423
+ return file;
7424
+ }
7425
+ /** Decode `count` evenly spread frames of `file` to raw RGB with the node's ffmpeg. */
7426
+ async function decodeClipFrames(ffmpeg, file, count) {
7427
+ const frameBytes = 640 * 360 * 3;
7428
+ const args = [
7429
+ "-v",
7430
+ "error",
7431
+ "-i",
7432
+ file,
7433
+ "-vf",
7434
+ `fps=1,scale=640:360`,
7435
+ "-frames:v",
7436
+ String(count),
7437
+ "-pix_fmt",
7438
+ "rgb24",
7439
+ "-f",
7440
+ "rawvideo",
7441
+ "pipe:1"
7442
+ ];
7443
+ const chunks = [];
7444
+ const stderr = [];
7445
+ const code = await new Promise((resolve, reject) => {
7446
+ const child = spawn(ffmpeg, args, { stdio: [
7447
+ "ignore",
7448
+ "pipe",
7449
+ "pipe"
7450
+ ] });
7451
+ child.stdout.on("data", (c) => chunks.push(c));
7452
+ child.stderr.on("data", (c) => stderr.push(c));
7453
+ child.on("error", reject);
7454
+ child.on("close", resolve);
7455
+ });
7456
+ if (code !== 0) throw new Error(`bench clip decode failed (${code}): ${Buffer.concat(stderr).toString().slice(0, 300)}`);
7457
+ const all = Buffer.concat(chunks);
7458
+ const frames = [];
7459
+ for (let off = 0; off + frameBytes <= all.length; off += frameBytes) frames.push({
7460
+ data: new Uint8Array(all.subarray(off, off + frameBytes)),
7461
+ width: 640,
7462
+ height: 360
7463
+ });
7464
+ return frames;
7465
+ }
7466
+ /** The clip's frames, else the reference images'. Throws only when neither yields a frame. */
7467
+ async function loadBenchFrames(deps) {
7468
+ const clip = deps.clip ?? BENCH_CLIP;
7469
+ try {
7470
+ const file = await ensureBenchClip(deps);
7471
+ if (file !== null) {
7472
+ const frames = await decodeClipFrames(await deps.ensureFfmpeg(), file, 120);
7473
+ if (frames.length > 0) return {
7474
+ frames,
7475
+ clip: {
7476
+ id: clip.id,
7477
+ revision: clip.revision,
7478
+ frames: frames.length,
7479
+ width: 640,
7480
+ height: 360
7481
+ }
7482
+ };
7483
+ } else deps.log("bench clip not published yet — measuring on the reference images");
7484
+ } catch (err) {
7485
+ deps.log("bench clip unavailable — measuring on the reference images", { error: err instanceof Error ? err.message : String(err) });
7486
+ }
7487
+ const frames = [];
7488
+ for (const jpeg of await deps.referenceImages()) try {
7489
+ frames.push(await deps.decodeJpeg(jpeg, 640));
7490
+ } catch {}
7491
+ if (frames.length === 0) throw new Error("node bench: neither the bench clip nor a reference image could be decoded");
7492
+ return {
7493
+ frames,
7494
+ clip: {
7495
+ id: "reference-images",
7496
+ revision: null,
7497
+ frames: frames.length,
7498
+ width: frames[0]?.width ?? 0,
7499
+ height: frames[0]?.height ?? 0
7500
+ }
7501
+ };
7502
+ }
7503
+ var WARMUP_CALLS = 3;
7504
+ var DETECTOR_STEP = "object-detection";
7505
+ function rootOnly(modelId) {
7506
+ return [{
7507
+ addonId: DETECTOR_STEP,
7508
+ modelId,
7509
+ enabled: true,
7510
+ children: []
7511
+ }];
7512
+ }
7513
+ /** Which limit a failing step hit first, from its reasons. */
7514
+ function limitOf(reasons) {
7515
+ const has = (prefix) => reasons.some((r) => r.startsWith(prefix));
7516
+ if (has("errors") || has("unresolved")) return "errors";
7517
+ if (has("cpu")) return "cpu";
7518
+ if (has("ram")) return "ram";
7519
+ if (has("event-loop")) return "stall";
7520
+ if (has("e2e p95")) return "p95";
7521
+ if (has("dropRatio")) return "drops";
7522
+ return "none";
7523
+ }
7524
+ function rampStepOf(step, measureSec, system) {
7525
+ return {
7526
+ load: step.cameras,
7527
+ pass: step.verdict.pass,
7528
+ p95Ms: Math.round(step.verdict.e2eMs.p95 * 10) / 10,
7529
+ dropRatio: Math.round(step.verdict.dropRatio * 1e4) / 1e4,
7530
+ fps: Math.round(step.raw.completed / Math.max(measureSec, 1e-9) * 10) / 10,
7531
+ cpuPct: system?.cpuPct ?? null,
7532
+ ramFreePct: system?.ramFreePct ?? null,
7533
+ failReasons: step.verdict.failReasons
7534
+ };
7535
+ }
7536
+ var NodeBenchDriver = class {
7537
+ deps;
7538
+ frameIndex = 0;
7539
+ cellsDone = 0;
7540
+ cellsTotal;
7541
+ constructor(deps) {
7542
+ this.deps = deps;
7543
+ const { input } = deps;
7544
+ const deviceCells = input.phases.includes("device") ? input.devices.reduce((n, d) => n + d.modelIds.length, 0) : 0;
7545
+ const pipelineCells = input.phases.includes("pipeline") ? input.devices.length : 0;
7546
+ this.cellsTotal = deviceCells + pipelineCells;
7547
+ }
7548
+ nextFrame() {
7549
+ const frames = this.deps.frames;
7550
+ const frame = frames[this.frameIndex % frames.length];
7551
+ this.frameIndex += 1;
7552
+ if (frame === void 0) throw new Error("node bench: no frames");
7553
+ return frame;
7554
+ }
7555
+ progress(p) {
7556
+ this.deps.onProgress({
7557
+ ...p,
7558
+ cellsDone: this.cellsDone,
7559
+ cellsTotal: this.cellsTotal
7560
+ });
7561
+ }
7562
+ async warmup(steps, deviceKey, plane) {
7563
+ const t0 = this.deps.now();
7564
+ let firstMs = 0;
7565
+ for (let i = 0; i < WARMUP_CALLS; i++) {
7566
+ const started = this.deps.now();
7567
+ await this.deps.dispatch({
7568
+ steps,
7569
+ deviceKey,
7570
+ plane,
7571
+ frame: this.nextFrame()
7572
+ });
7573
+ if (i === 0) firstMs = this.deps.now() - started;
7574
+ }
7575
+ return Math.max(firstMs, 0) || this.deps.now() - t0;
7576
+ }
7577
+ ramp(opts) {
7578
+ const { input } = this.deps;
7579
+ const frameIntervalMs = 1e3 / input.fpsPerStream;
7580
+ const system = /* @__PURE__ */ new Map();
7581
+ const dispatch = async () => {
7582
+ const r = await this.deps.dispatch({
7583
+ steps: opts.steps,
7584
+ deviceKey: opts.deviceKey,
7585
+ plane: opts.plane,
7586
+ frame: this.nextFrame()
7587
+ });
7588
+ opts.onStepsRan?.(r.stepsRan);
7589
+ if (r.shed) return {
7590
+ kind: "shed",
7591
+ shedSignal: "first-class"
7592
+ };
7593
+ return {
7594
+ kind: "ok",
7595
+ inferMs: r.inferMs,
7596
+ predictMs: r.predictMs,
7597
+ detections: r.detections
7598
+ };
7599
+ };
7600
+ return executeCapacityRamp({
7601
+ ramp: opts.loads,
7602
+ fpsPerCamera: input.fpsPerStream,
7603
+ settleMs: input.settleSec * 1e3,
7604
+ measureMs: input.measureSec * 1e3,
7605
+ drainTimeoutMs: Math.max(5e3, frameIntervalMs * 20),
7606
+ slo: {
7607
+ dropRatioMax: .01,
7608
+ e2eP95MsMax: frameIntervalMs,
7609
+ stallsMax: 0
7610
+ },
7611
+ stopOnFirstFail: true,
7612
+ dispatch,
7613
+ now: this.deps.now,
7614
+ sleepUntil: this.deps.sleepUntil,
7615
+ shouldStop: this.deps.shouldStop,
7616
+ stallSnapshot: this.deps.stallSnapshot,
7617
+ rssMb: this.deps.rssMb,
7618
+ ...opts.withSystemGate ? {
7619
+ extraFailReasons: (step) => {
7620
+ const s = this.deps.sampleSystem();
7621
+ system.set(step.cameras, s);
7622
+ const out = [];
7623
+ if (s.cpuPct > 85) out.push(`cpu ${Math.round(s.cpuPct)}% > 85%`);
7624
+ if (s.ramFreePct < 15) out.push(`ram free ${Math.round(s.ramFreePct)}% < 15%`);
7625
+ return out;
7626
+ },
7627
+ onMeasureStartHook: () => {
7628
+ this.deps.sampleSystem();
7629
+ }
7630
+ } : {},
7631
+ onStep: (info) => {
7632
+ if (info.phase === "done") return;
7633
+ this.progress({
7634
+ ...opts.label,
7635
+ load: info.cameras,
7636
+ message: `${opts.label.phase === "device" ? "streams" : "sessions"} ${info.cameras} · ${info.phase}`
7637
+ });
7638
+ }
7639
+ }).then((outcome) => ({
7640
+ steps: [...outcome.steps],
7641
+ system,
7642
+ aborted: outcome.aborted
7643
+ }));
7644
+ }
7645
+ /** A cell whose ramp was cut short measured nothing reliable — null. */
7646
+ async measureDevice(deviceKey, modelId) {
7647
+ const steps = rootOnly(modelId);
7648
+ const label = {
7649
+ phase: "device",
7650
+ deviceKey,
7651
+ modelId
7652
+ };
7653
+ this.progress({
7654
+ ...label,
7655
+ load: null,
7656
+ message: "loading the model"
7657
+ });
7658
+ const warmupMs = await this.warmup(steps, deviceKey, "frame");
7659
+ const { steps: ramp, aborted } = await this.ramp({
7660
+ loads: DEVICE_RAMP,
7661
+ steps,
7662
+ deviceKey,
7663
+ plane: "frame",
7664
+ withSystemGate: false,
7665
+ label
7666
+ });
7667
+ if (aborted) return null;
7668
+ const last = ramp.filter((s) => s.verdict.pass).at(-1);
7669
+ const first = ramp[0];
7670
+ const failed = ramp.find((s) => !s.verdict.pass);
7671
+ const single = first?.raw.e2eMsList ?? [];
7672
+ return {
7673
+ phase: "device",
7674
+ deviceKey,
7675
+ modelId,
7676
+ p50Ms: Math.round(percentile(single, .5) * 10) / 10,
7677
+ p95Ms: Math.round(percentile(single, .95) * 10) / 10,
7678
+ p99Ms: Math.round(percentile(single, .99) * 10) / 10,
7679
+ fps: last === void 0 ? 0 : Math.round(last.raw.completed / this.deps.input.measureSec * 10) / 10,
7680
+ kMax: last?.cameras ?? 0,
7681
+ limitedBy: failed === void 0 ? "none" : limitOf(failed.verdict.failReasons),
7682
+ warmupMs: Math.round(warmupMs),
7683
+ ramp: ramp.map((s) => rampStepOf(s, this.deps.input.measureSec, null))
7684
+ };
7685
+ }
7686
+ async measurePipeline(deviceKey, modelId) {
7687
+ const steps = await this.deps.pipelineSteps(modelId);
7688
+ const label = {
7689
+ phase: "pipeline",
7690
+ deviceKey,
7691
+ modelId
7692
+ };
7693
+ this.progress({
7694
+ ...label,
7695
+ load: null,
7696
+ message: "loading the pipeline"
7697
+ });
7698
+ const fired = /* @__PURE__ */ new Set();
7699
+ const warmupMs = await this.warmup(steps, deviceKey, "full");
7700
+ const { steps: ramp, system, aborted } = await this.ramp({
7701
+ loads: PIPELINE_RAMP,
7702
+ steps,
7703
+ deviceKey,
7704
+ plane: "full",
7705
+ withSystemGate: true,
7706
+ onStepsRan: (ran) => {
7707
+ for (const s of ran) fired.add(s);
7708
+ },
7709
+ label
7710
+ });
7711
+ if (aborted) return null;
7712
+ const last = ramp.filter((s) => s.verdict.pass).at(-1);
7713
+ const failed = ramp.find((s) => !s.verdict.pass);
7714
+ return {
7715
+ phase: "pipeline",
7716
+ deviceKey,
7717
+ modelId,
7718
+ steps: this.deps.input.steps,
7719
+ maxSessions: last?.cameras ?? 0,
7720
+ limitedBy: failed === void 0 ? "none" : limitOf(failed.verdict.failReasons),
7721
+ stepsFired: this.deps.input.steps.filter((s) => fired.has(s)),
7722
+ warmupMs: Math.round(warmupMs),
7723
+ ramp: ramp.map((s) => rampStepOf(s, this.deps.input.measureSec, system.get(s.cameras) ?? null))
7724
+ };
7725
+ }
7726
+ /** Run every cell the input asks for; stops between steps when told to. */
7727
+ async run() {
7728
+ const { input } = this.deps;
7729
+ if (input.phases.includes("device")) for (const device of input.devices) for (const modelId of device.modelIds) {
7730
+ if (this.deps.shouldStop()) return;
7731
+ const cell = await this.measureDevice(device.deviceKey, modelId);
7732
+ this.cellsDone += 1;
7733
+ if (cell !== null) this.deps.onCell(cell);
7734
+ }
7735
+ if (input.phases.includes("pipeline")) for (const device of input.devices) {
7736
+ const modelId = device.modelIds[0];
7737
+ if (modelId === void 0 || this.deps.shouldStop()) return;
7738
+ const cell = await this.measurePipeline(device.deviceKey, modelId);
7739
+ this.cellsDone += 1;
7740
+ if (cell !== null) this.deps.onCell(cell);
7741
+ }
7742
+ }
7743
+ };
7744
+ //#endregion
7745
+ //#region src/pipeline-runner/node-bench/node-bench-host.ts
7746
+ /**
7747
+ * The runner side of the node benchmark (D791): one run per node, started
7748
+ * by the orchestrator through `nodePin`, answered at once and polled. Holds
7749
+ * the run's status in RAM (a restarted runner forgets it — the orchestrator
7750
+ * reads that as `runner-lost` and restores the cluster), stops itself at the
7751
+ * deadline whatever the hub does, and deletes the models it downloaded that
7752
+ * were not installed before (unless told to keep them).
7753
+ */
7754
+ /** Node CPU % between two calls (all cores), and free RAM %. */
7755
+ function createSystemSampler() {
7756
+ const totals = () => os.cpus().reduce((acc, c) => {
7757
+ const t = c.times;
7758
+ return {
7759
+ idle: acc.idle + t.idle,
7760
+ total: acc.total + t.user + t.nice + t.sys + t.idle + t.irq
7761
+ };
7762
+ }, {
7763
+ idle: 0,
7764
+ total: 0
7765
+ });
7766
+ let prev = totals();
7767
+ return () => {
7768
+ const now = totals();
7769
+ const total = now.total - prev.total;
7770
+ const idle = now.idle - prev.idle;
7771
+ prev = now;
7772
+ return {
7773
+ cpuPct: total > 0 ? Math.round((1 - idle / total) * 1e3) / 10 : 0,
7774
+ ramFreePct: Math.round(os.freemem() / os.totalmem() * 1e3) / 10
7775
+ };
7776
+ };
7777
+ }
7778
+ /** Runs kept for status reads after they end. */
7779
+ var KEEP_FINISHED = 5;
7780
+ var NodeBenchHost = class {
7781
+ deps;
7782
+ runs = /* @__PURE__ */ new Map();
7783
+ constructor(deps) {
7784
+ this.deps = deps;
7785
+ }
7786
+ now() {
7787
+ return this.deps.now?.() ?? Date.now();
7788
+ }
7789
+ start(raw) {
7790
+ const input = NodeBenchRunnerInputSchema.parse(raw);
7791
+ if (this.runs.get(input.runId) !== void 0) return {
7792
+ accepted: true,
7793
+ runId: input.runId
7794
+ };
7795
+ for (const [id, run] of this.runs) if (run.status.state === "running") throw new Error(`node bench: run ${id} is measuring this node — one at a time`);
7796
+ const state = {
7797
+ stop: false,
7798
+ status: {
7799
+ runId: input.runId,
7800
+ nodeId: this.deps.nodeId,
7801
+ state: "running",
7802
+ progress: null,
7803
+ cells: [],
7804
+ fingerprint: null,
7805
+ clip: null,
7806
+ activeCameraIds: [...this.deps.activeCameraIds()],
7807
+ deletedModels: [],
7808
+ error: null,
7809
+ startedAtMs: this.now(),
7810
+ finishedAtMs: null
7811
+ }
7812
+ };
7813
+ this.runs.set(input.runId, state);
7814
+ this.prune();
7815
+ this.execute(input, state);
7816
+ return {
7817
+ accepted: true,
7818
+ runId: input.runId
7819
+ };
7820
+ }
7821
+ status(runId) {
7822
+ return this.runs.get(runId)?.status ?? null;
7823
+ }
7824
+ abort(runId) {
7825
+ const run = this.runs.get(runId);
7826
+ if (run === void 0 || run.status.state !== "running") return { aborted: false };
7827
+ run.stop = true;
7828
+ return { aborted: true };
7829
+ }
7830
+ prune() {
7831
+ const finished = [...this.runs.values()].filter((r) => r.status.state !== "running").toSorted((a, b) => b.status.startedAtMs - a.status.startedAtMs);
7832
+ for (const old of finished.slice(KEEP_FINISHED)) this.runs.delete(old.status.runId);
7833
+ }
7834
+ patch(state, patch) {
7835
+ state.status = {
7836
+ ...state.status,
7837
+ ...patch
7838
+ };
7839
+ }
7840
+ noteActiveCameras(state) {
7841
+ const seen = new Set([...state.status.activeCameraIds, ...this.deps.activeCameraIds()]);
7842
+ if (seen.size !== state.status.activeCameraIds.length) this.patch(state, { activeCameraIds: [...seen].toSorted((a, b) => a - b) });
7843
+ }
7844
+ async execute(input, state) {
7845
+ const shouldStop = () => state.stop || this.now() >= input.deadlineAtMs;
7846
+ const sampler = this.deps.startStallSampler();
7847
+ const installedBefore = await this.deps.installedDetectorModels().catch(() => null);
7848
+ try {
7849
+ this.patch(state, { progress: this.preparing(input, "decoding the bench frames") });
7850
+ const deviceKeys = input.devices.map((d) => d.deviceKey);
7851
+ const [frameSet, fingerprint] = await Promise.all([this.deps.loadFrames(), this.deps.fingerprint(deviceKeys).catch(() => null)]);
7852
+ this.patch(state, {
7853
+ clip: frameSet.clip,
7854
+ fingerprint
7855
+ });
7856
+ this.deps.log("info", "node bench start", {
7857
+ runId: input.runId,
7858
+ clip: frameSet.clip.id,
7859
+ frames: frameSet.frames.length,
7860
+ devices: deviceKeys,
7861
+ phases: input.phases
7862
+ });
7863
+ await new NodeBenchDriver({
7864
+ input,
7865
+ frames: frameSet.frames,
7866
+ dispatch: this.deps.dispatch,
7867
+ pipelineSteps: (modelId) => this.deps.pipelineSteps(modelId, input.steps),
7868
+ now: () => this.now(),
7869
+ sleepUntil: this.deps.sleepUntil ?? ((t) => new Promise((resolve) => {
7870
+ setTimeout(resolve, Math.max(0, t - this.now())).unref?.();
7871
+ })),
7872
+ shouldStop,
7873
+ stallSnapshot: sampler.snapshot,
7874
+ rssMb: () => Math.round(process.memoryUsage().rss / 1e6),
7875
+ sampleSystem: this.deps.sampleSystem ?? createSystemSampler(),
7876
+ onProgress: (progress) => {
7877
+ this.noteActiveCameras(state);
7878
+ this.patch(state, { progress });
7879
+ },
7880
+ onCell: (cell) => {
7881
+ this.patch(state, { cells: [...state.status.cells, cell] });
7882
+ this.deps.log("info", "node bench cell", {
7883
+ runId: input.runId,
7884
+ phase: cell.phase,
7885
+ deviceKey: cell.deviceKey,
7886
+ modelId: cell.modelId,
7887
+ ...cell.phase === "device" ? {
7888
+ kMax: cell.kMax,
7889
+ fps: cell.fps,
7890
+ p95Ms: cell.p95Ms
7891
+ } : {
7892
+ maxSessions: cell.maxSessions,
7893
+ stepsFired: cell.stepsFired.join(",")
7894
+ },
7895
+ limitedBy: cell.limitedBy
7896
+ });
7897
+ }
7898
+ }).run();
7899
+ this.patch(state, { state: shouldStop() ? "aborted" : "done" });
7900
+ } catch (err) {
7901
+ const error = err instanceof Error ? err.message : String(err);
7902
+ this.deps.log("warn", "node bench failed", {
7903
+ runId: input.runId,
7904
+ error
7905
+ });
7906
+ this.patch(state, {
7907
+ state: "failed",
7908
+ error
7909
+ });
7910
+ } finally {
7911
+ sampler.stop();
7912
+ await this.cleanupModels(input, state, installedBefore);
7913
+ this.patch(state, {
7914
+ finishedAtMs: this.now(),
7915
+ progress: null
7916
+ });
7917
+ }
7918
+ }
7919
+ preparing(input, message) {
7920
+ return {
7921
+ phase: "preparing",
7922
+ deviceKey: null,
7923
+ modelId: null,
7924
+ load: null,
7925
+ cellsDone: 0,
7926
+ cellsTotal: (input.phases.includes("device") ? input.devices.reduce((n, d) => n + d.modelIds.length, 0) : 0) + (input.phases.includes("pipeline") ? input.devices.length : 0),
7927
+ message
7928
+ };
7929
+ }
7930
+ /** Delete the candidate detectors the run fetched that were not installed before. */
7931
+ async cleanupModels(input, state, before) {
7932
+ if (input.keepDownloadedModels || before === null) return;
7933
+ const after = await this.deps.installedDetectorModels().catch(() => null);
7934
+ if (after === null) return;
7935
+ const candidates = new Set(input.devices.flatMap((d) => d.modelIds));
7936
+ const fetched = [...after].filter((key) => candidates.has(key.split("/")[0] ?? "") && !before.has(key));
7937
+ const deleted = [];
7938
+ for (const key of fetched) try {
7939
+ await this.deps.deleteDetectorModel(key);
7940
+ deleted.push(key);
7941
+ } catch (err) {
7942
+ this.deps.log("warn", "node bench: could not delete a downloaded model", {
7943
+ model: key,
7944
+ error: err instanceof Error ? err.message : String(err)
7945
+ });
7946
+ }
7947
+ if (deleted.length > 0) this.patch(state, { deletedModels: deleted });
7948
+ }
7949
+ };
7950
+ //#endregion
7951
+ //#region src/pipeline-runner/node-bench/node-bench-wiring.ts
7952
+ /**
7953
+ * Binds the node benchmark host (D791) to the runner's in-process executor:
7954
+ * every call is pinned to THIS node, so the frame never leaves the process
7955
+ * group that owns it (D9) and the numbers are this node's.
7956
+ */
7957
+ var DETECTOR_ADDON = "object-detection";
7958
+ /** A stall this long fails a ramp step (the frame interval at 4 fps). */
7959
+ var BENCH_STALL_MS = 250;
7960
+ function isModelFormat(value) {
7961
+ return MODEL_FORMATS.includes(value);
7962
+ }
7963
+ function createNodeBenchHost(deps) {
7964
+ const api = () => {
7965
+ const a = deps.ctx().api;
7966
+ if (!a) throw new Error("node bench: ctx.api unavailable");
7967
+ return a;
7968
+ };
7969
+ const pin = () => nodePin(deps.nodeId);
7970
+ const log = deps.ctx().logger;
7971
+ const decodeJpeg = async (jpeg, maxWidth) => {
7972
+ const { data, info } = await (await getSharp())(Buffer.from(jpeg)).resize({
7973
+ width: maxWidth,
7974
+ withoutEnlargement: true
7975
+ }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
7976
+ return {
7977
+ data: new Uint8Array(data),
7978
+ width: info.width,
7979
+ height: info.height
7980
+ };
7981
+ };
7982
+ return new NodeBenchHost({
7983
+ nodeId: deps.nodeId,
7984
+ loadFrames: () => loadBenchFrames({
7985
+ dataDir: deps.ctx().dataDir,
7986
+ ensureFfmpeg: () => deps.ctx().deps.ensureFfmpeg(),
7987
+ fetch: globalThis.fetch,
7988
+ referenceImages: async () => {
7989
+ const list = await api().pipelineExecutor.listReferenceImages.query(void 0, pin());
7990
+ const out = [];
7991
+ for (const entry of list) {
7992
+ const img = await api().pipelineExecutor.getReferenceImage.query({ filename: entry.filename }, pin());
7993
+ if (img) out.push(new Uint8Array(Buffer.from(img.base64, "base64")));
7994
+ }
7995
+ return out;
7996
+ },
7997
+ decodeJpeg,
7998
+ log: (message, meta) => log.info(message, meta ? { meta } : void 0)
7999
+ }),
8000
+ dispatch: async ({ steps, deviceKey, plane, frame }) => {
8001
+ const result = await api().pipelineExecutor.runPipeline.mutate({
8002
+ steps: [...steps],
8003
+ frame: {
8004
+ data: frame.data,
8005
+ format: "rgb",
8006
+ width: frame.width,
8007
+ height: frame.height,
8008
+ timestamp: Date.now()
8009
+ },
8010
+ plane,
8011
+ deviceKey
8012
+ }, pin());
8013
+ return {
8014
+ shed: result.shed === true,
8015
+ detections: result.detections.length,
8016
+ inferMs: result.debug?.totalInferenceMs ?? 0,
8017
+ predictMs: result.debug?.predictMs ?? 0,
8018
+ stepsRan: [...new Set((result.debug?.stepTimings ?? []).filter((t) => t.ms > 0).map((t) => t.source))]
8019
+ };
8020
+ },
8021
+ pipelineSteps: async (rootModelId, stepIds) => {
8022
+ return buildBenchStepTree(await api().pipelineExecutor.getSchema.query(void 0, pin()), rootModelId, stepIds);
8023
+ },
8024
+ fingerprint: async (deviceKeys) => {
8025
+ const pools = {};
8026
+ for (const deviceKey of deviceKeys) try {
8027
+ const t = await api().pipelineExecutor.getEffectiveTuning.query({ deviceKey }, pin());
8028
+ pools[deviceKey] = {
8029
+ runtime: t.runtime,
8030
+ numWorkers: t.numWorkers,
8031
+ concurrency: t.concurrency,
8032
+ batchMode: t.batchMode
8033
+ };
8034
+ } catch {}
8035
+ return {
8036
+ pipelineVersion: deps.pipelineVersion,
8037
+ cpuModel: os.cpus()[0]?.model?.trim() ?? null,
8038
+ ramGb: Math.round(os.totalmem() / 1e9),
8039
+ os: `${os.platform()}-${os.arch()}`,
8040
+ pools
8041
+ };
8042
+ },
8043
+ installedDetectorModels: async () => {
8044
+ const models = await api().pipelineExecutor.getAddonModels.query({ addonId: DETECTOR_ADDON }, pin());
8045
+ const out = /* @__PURE__ */ new Set();
8046
+ for (const m of models) for (const [format, status] of Object.entries(m.formats)) if (status?.downloaded === true) out.add(`${m.id}/${format}`);
8047
+ return out;
8048
+ },
8049
+ deleteDetectorModel: async (key) => {
8050
+ const [modelId = "", format = ""] = key.split("/");
8051
+ if (!isModelFormat(format)) return;
8052
+ await api().pipelineExecutor.deleteModel.mutate({
8053
+ addonId: DETECTOR_ADDON,
8054
+ modelId,
8055
+ format
8056
+ }, pin());
8057
+ },
8058
+ activeCameraIds: deps.activeCameraIds,
8059
+ startStallSampler: () => startStallSampler({ reportThresholdMs: BENCH_STALL_MS }),
8060
+ log: (level, message, meta) => log[level](message, meta ? { meta } : void 0)
8061
+ });
8062
+ }
7355
8063
  /**
7356
8064
  * Normalised boxes for the subjects worth a tile, highest confidence first,
7357
8065
  * capped.
@@ -8357,7 +9065,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
8357
9065
  budgetSource: decodeLimits.budgetSource,
8358
9066
  maxWorkers: decodeLimits.maxWorkers,
8359
9067
  maxWorkersSource: decodeLimits.maxWorkersSource,
8360
- totalMemMb: Math.round(os.totalmem() / 1048576)
9068
+ totalMemMb: Math.round(os$1.totalmem() / 1048576)
8361
9069
  } });
8362
9070
  this.liveWorkers = new LiveDecodeWorkerAdmission({
8363
9071
  limit: decodeLimits.maxWorkers,
@@ -10002,6 +10710,25 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10002
10710
  * `executionFormat` reports — so the servability check and the dispatch
10003
10711
  * cannot disagree about which format was verified.
10004
10712
  */
10713
+ nodeBenchHost = null;
10714
+ nodeBench() {
10715
+ this.nodeBenchHost ??= createNodeBenchHost({
10716
+ nodeId: this.nodeId,
10717
+ ctx: () => this.ctx,
10718
+ activeCameraIds: () => (this.runner?.getAllCameraMetrics() ?? []).filter((m) => m.phase !== "idle").map((m) => m.deviceId),
10719
+ pipelineVersion: null
10720
+ });
10721
+ return this.nodeBenchHost;
10722
+ }
10723
+ async startNodeBench(input) {
10724
+ return this.nodeBench().start(input);
10725
+ }
10726
+ async getNodeBenchStatus(input) {
10727
+ return this.nodeBench().status(input.runId);
10728
+ }
10729
+ async abortNodeBench(input) {
10730
+ return this.nodeBench().abort(input.runId);
10731
+ }
10005
10732
  async runStatelessStep(input) {
10006
10733
  const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
10007
10734
  return runStatelessStep({
@@ -10218,7 +10945,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10218
10945
  defaultBudgetMb: DEFAULT_CONFIG.nodeDecodeBudgetMb,
10219
10946
  defaultMaxWorkers: DEFAULT_CONFIG.maxLiveDecodeWorkers,
10220
10947
  node: this.nodeLimits?.current() ?? {},
10221
- totalMemMb: Math.round(os.totalmem() / 1048576)
10948
+ totalMemMb: Math.round(os$1.totalmem() / 1048576)
10222
10949
  });
10223
10950
  }
10224
10951
  /** Resize the admission to the resolved limits; one info line per change. */
@@ -10235,7 +10962,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
10235
10962
  budgetSource: limits.budgetSource,
10236
10963
  maxWorkers: limits.maxWorkers,
10237
10964
  maxWorkersSource: limits.maxWorkersSource,
10238
- totalMemMb: Math.round(os.totalmem() / 1048576),
10965
+ totalMemMb: Math.round(os$1.totalmem() / 1048576),
10239
10966
  decodeCeilingInForce: this.liveWorkers?.ceiling
10240
10967
  } });
10241
10968
  }