@camstack/addon-pipeline 1.2.345 → 1.2.346

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-CrEsRBe_.js → custom-model-catalog-B0QdF7dJ.js} +2 -2
  4. package/dist/{custom-model-catalog-C7zLb7ze.mjs → custom-model-catalog-C4LhTrew.mjs} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +8 -8
  7. package/dist/{dist-cT7hhXh9.mjs → dist-Dj26w1od.mjs} +2044 -60
  8. package/dist/{dist-_I6JkuJ8.js → dist-qBCVRsyf.js} +2073 -59
  9. package/dist/{model-catalogs-BrKLOMBe.js → model-catalogs-C6tbm7vF.js} +1 -1
  10. package/dist/{model-catalogs-CPAmiLRa.mjs → model-catalogs-CsxkEiiz.mjs} +1 -1
  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-Cc9bQ8ZM.mjs} +1 -1
  14. package/dist/{node-6zL5ddry.js → node-DmCpPH58.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-DZcnBnb1.mjs → process-memory-BrI4evjl.mjs} +1 -1
  18. package/dist/{process-memory-CstCtC3A.js → process-memory-BxIflT01.js} +1 -1
  19. package/dist/recorder/index.js +25 -3
  20. package/dist/recorder/index.mjs +25 -3
  21. package/dist/{segment-demux-js-qzo-pQK7.mjs → segment-demux-js-CmuAuckE.mjs} +1 -1
  22. package/dist/{segment-demux-js-DPtDBph6.js → segment-demux-js-DCLzkQTf.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-BX73joE9.mjs} +2 -2
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B38GLERV.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-pieGo_tC.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-DKWabjpO.mjs} +2 -2
  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
@@ -3,15 +3,19 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-_I6JkuJ8.js");
7
- const require_node = require("../node-6zL5ddry.js");
8
- const require_custom_model_catalog = require("../custom-model-catalog-CrEsRBe_.js");
6
+ const require_dist = require("../dist-qBCVRsyf.js");
7
+ const require_node = require("../node-DmCpPH58.js");
8
+ const require_custom_model_catalog = require("../custom-model-catalog-B0QdF7dJ.js");
9
9
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
10
10
  const require_lazy_sharp = require("../lazy-sharp-BuwKlQLR.js");
11
11
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
12
12
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
13
13
  let node_os = require("node:os");
14
14
  node_os = require_chunk.__toESM(node_os);
15
+ let node_crypto = require("node:crypto");
16
+ let node_fs = require("node:fs");
17
+ let node_path = require("node:path");
18
+ node_path = require_chunk.__toESM(node_path);
15
19
  let node_child_process = require("node:child_process");
16
20
  let sharp = require("sharp");
17
21
  sharp = require_chunk.__toESM(sharp);
@@ -3181,6 +3185,7 @@ async function executeCapacityRamp(opts) {
3181
3185
  onMeasureStart: () => {
3182
3186
  stallStart = opts.stallSnapshot();
3183
3187
  rssStart = opts.rssMb();
3188
+ opts.onMeasureStartHook?.();
3184
3189
  opts.onStep?.({
3185
3190
  stepIndex,
3186
3191
  cameras,
@@ -3189,15 +3194,25 @@ async function executeCapacityRamp(opts) {
3189
3194
  }
3190
3195
  });
3191
3196
  const stall = diffStallWindow(stallStart, opts.stallSnapshot());
3192
- const verdict = evaluateCapacityStep(raw, stall, opts.slo);
3193
- const step = {
3197
+ const evaluated = evaluateCapacityStep(raw, stall, opts.slo);
3198
+ const base = {
3194
3199
  cameras,
3195
3200
  raw,
3196
3201
  stall,
3197
- verdict,
3202
+ verdict: evaluated,
3198
3203
  rssStartMb: rssStart,
3199
3204
  rssEndMb: opts.rssMb()
3200
3205
  };
3206
+ const extra = opts.extraFailReasons?.(base) ?? [];
3207
+ const verdict = extra.length === 0 ? evaluated : {
3208
+ ...evaluated,
3209
+ pass: false,
3210
+ failReasons: [...evaluated.failReasons, ...extra]
3211
+ };
3212
+ const step = {
3213
+ ...base,
3214
+ verdict
3215
+ };
3201
3216
  steps.push(step);
3202
3217
  firstClass += raw.firstClassShed;
3203
3218
  fallback += raw.fallbackShed;
@@ -7360,6 +7375,699 @@ async function runStatelessStep(deps, input) {
7360
7375
  details
7361
7376
  };
7362
7377
  }
7378
+ //#endregion
7379
+ //#region src/pipeline-runner/node-bench/bench-frames.ts
7380
+ /**
7381
+ * The frames a node benchmark runs on (D791), decoded ONCE on the node into
7382
+ * RAM at the detection size — never shipped across a process per call (D9).
7383
+ *
7384
+ * Source, in order:
7385
+ * 1. the public bench clip (`camstack-bench-v1`, CC-licensed composite with
7386
+ * a synthetic plate — `docs/benchmarks/bench-clip.md`), downloaded once
7387
+ * into the runner's data dir and accepted ONLY when its sha256 matches
7388
+ * the pinned revision; decoded with the node's pinned ffmpeg;
7389
+ * 2. otherwise the executor's reference images (always on the node). The
7390
+ * run says which (`clip.id`), so a result never claims the clip it did
7391
+ * not use.
7392
+ */
7393
+ /**
7394
+ * The published clip, pinned to the exact build uploaded to the HF dataset
7395
+ * repo (`scripts/bench-clip/build-bench-clip.sh` prints the hash). A download
7396
+ * that fails (not uploaded yet) or whose bytes differ falls back to the
7397
+ * reference images, and the run says so. `sha256: null` disables the clip.
7398
+ */
7399
+ var BENCH_CLIP = {
7400
+ id: "camstack-bench-v1",
7401
+ revision: "v1",
7402
+ fileName: "camstack-bench-v1-h264-1080p.mp4",
7403
+ url: "https://huggingface.co/datasets/camstack/camstack-bench-clips/resolve/main/camstack-bench-v1/camstack-bench-v1-h264-1080p.mp4",
7404
+ sha256: "677eea6ccb5711dafe70ac7259aa152b8fad3e5704da1a8a8c66e6b1717f1434"
7405
+ };
7406
+ async function sha256Of(file) {
7407
+ const hash = (0, node_crypto.createHash)("sha256");
7408
+ for await (const chunk of (0, node_fs.createReadStream)(file)) hash.update(chunk);
7409
+ return hash.digest("hex");
7410
+ }
7411
+ /** The verified clip on disk, downloading it when absent or wrong. Null when unavailable. */
7412
+ async function ensureBenchClip(deps) {
7413
+ const clip = deps.clip ?? BENCH_CLIP;
7414
+ if (clip.sha256 === null) return null;
7415
+ const dir = node_path.default.join(deps.dataDir, "bench");
7416
+ const file = node_path.default.join(dir, clip.fileName);
7417
+ try {
7418
+ if (await sha256Of(file) === clip.sha256) return file;
7419
+ } catch {}
7420
+ await node_fs.promises.mkdir(dir, { recursive: true });
7421
+ const staged = `${file}.downloading`;
7422
+ const res = await deps.fetch(clip.url);
7423
+ if (!res.ok || res.body === null) throw new Error(`bench clip download: HTTP ${res.status}`);
7424
+ await node_fs.promises.writeFile(staged, new Uint8Array(await res.arrayBuffer()));
7425
+ const got = await sha256Of(staged);
7426
+ if (got !== clip.sha256) {
7427
+ await node_fs.promises.rm(staged, { force: true });
7428
+ throw new Error(`bench clip download: sha256 ${got} is not the pinned ${clip.sha256}`);
7429
+ }
7430
+ await node_fs.promises.rename(staged, file);
7431
+ return file;
7432
+ }
7433
+ /** Decode `count` evenly spread frames of `file` to raw RGB with the node's ffmpeg. */
7434
+ async function decodeClipFrames(ffmpeg, file, count) {
7435
+ const frameBytes = 640 * 360 * 3;
7436
+ const args = [
7437
+ "-v",
7438
+ "error",
7439
+ "-i",
7440
+ file,
7441
+ "-vf",
7442
+ `fps=1,scale=640:360`,
7443
+ "-frames:v",
7444
+ String(count),
7445
+ "-pix_fmt",
7446
+ "rgb24",
7447
+ "-f",
7448
+ "rawvideo",
7449
+ "pipe:1"
7450
+ ];
7451
+ const chunks = [];
7452
+ const stderr = [];
7453
+ const code = await new Promise((resolve, reject) => {
7454
+ const child = (0, node_child_process.spawn)(ffmpeg, args, { stdio: [
7455
+ "ignore",
7456
+ "pipe",
7457
+ "pipe"
7458
+ ] });
7459
+ child.stdout.on("data", (c) => chunks.push(c));
7460
+ child.stderr.on("data", (c) => stderr.push(c));
7461
+ child.on("error", reject);
7462
+ child.on("close", resolve);
7463
+ });
7464
+ if (code !== 0) throw new Error(`bench clip decode failed (${code}): ${Buffer.concat(stderr).toString().slice(0, 300)}`);
7465
+ const all = Buffer.concat(chunks);
7466
+ const frames = [];
7467
+ for (let off = 0; off + frameBytes <= all.length; off += frameBytes) frames.push({
7468
+ data: new Uint8Array(all.subarray(off, off + frameBytes)),
7469
+ width: 640,
7470
+ height: 360
7471
+ });
7472
+ return frames;
7473
+ }
7474
+ /** The clip's frames, else the reference images'. Throws only when neither yields a frame. */
7475
+ async function loadBenchFrames(deps) {
7476
+ const clip = deps.clip ?? BENCH_CLIP;
7477
+ try {
7478
+ const file = await ensureBenchClip(deps);
7479
+ if (file !== null) {
7480
+ const frames = await decodeClipFrames(await deps.ensureFfmpeg(), file, 120);
7481
+ if (frames.length > 0) return {
7482
+ frames,
7483
+ clip: {
7484
+ id: clip.id,
7485
+ revision: clip.revision,
7486
+ frames: frames.length,
7487
+ width: 640,
7488
+ height: 360
7489
+ }
7490
+ };
7491
+ } else deps.log("bench clip not published yet — measuring on the reference images");
7492
+ } catch (err) {
7493
+ deps.log("bench clip unavailable — measuring on the reference images", { error: err instanceof Error ? err.message : String(err) });
7494
+ }
7495
+ const frames = [];
7496
+ for (const jpeg of await deps.referenceImages()) try {
7497
+ frames.push(await deps.decodeJpeg(jpeg, 640));
7498
+ } catch {}
7499
+ if (frames.length === 0) throw new Error("node bench: neither the bench clip nor a reference image could be decoded");
7500
+ return {
7501
+ frames,
7502
+ clip: {
7503
+ id: "reference-images",
7504
+ revision: null,
7505
+ frames: frames.length,
7506
+ width: frames[0]?.width ?? 0,
7507
+ height: frames[0]?.height ?? 0
7508
+ }
7509
+ };
7510
+ }
7511
+ var WARMUP_CALLS = 3;
7512
+ var DETECTOR_STEP = "object-detection";
7513
+ function rootOnly(modelId) {
7514
+ return [{
7515
+ addonId: DETECTOR_STEP,
7516
+ modelId,
7517
+ enabled: true,
7518
+ children: []
7519
+ }];
7520
+ }
7521
+ /** Which limit a failing step hit first, from its reasons. */
7522
+ function limitOf(reasons) {
7523
+ const has = (prefix) => reasons.some((r) => r.startsWith(prefix));
7524
+ if (has("errors") || has("unresolved")) return "errors";
7525
+ if (has("cpu")) return "cpu";
7526
+ if (has("ram")) return "ram";
7527
+ if (has("event-loop")) return "stall";
7528
+ if (has("e2e p95")) return "p95";
7529
+ if (has("dropRatio")) return "drops";
7530
+ return "none";
7531
+ }
7532
+ function rampStepOf(step, measureSec, system) {
7533
+ return {
7534
+ load: step.cameras,
7535
+ pass: step.verdict.pass,
7536
+ p95Ms: Math.round(step.verdict.e2eMs.p95 * 10) / 10,
7537
+ dropRatio: Math.round(step.verdict.dropRatio * 1e4) / 1e4,
7538
+ fps: Math.round(step.raw.completed / Math.max(measureSec, 1e-9) * 10) / 10,
7539
+ cpuPct: system?.cpuPct ?? null,
7540
+ ramFreePct: system?.ramFreePct ?? null,
7541
+ failReasons: step.verdict.failReasons
7542
+ };
7543
+ }
7544
+ var NodeBenchDriver = class {
7545
+ deps;
7546
+ frameIndex = 0;
7547
+ cellsDone = 0;
7548
+ cellsTotal;
7549
+ constructor(deps) {
7550
+ this.deps = deps;
7551
+ const { input } = deps;
7552
+ const deviceCells = input.phases.includes("device") ? input.devices.reduce((n, d) => n + d.modelIds.length, 0) : 0;
7553
+ const pipelineCells = input.phases.includes("pipeline") ? input.devices.length : 0;
7554
+ this.cellsTotal = deviceCells + pipelineCells;
7555
+ }
7556
+ nextFrame() {
7557
+ const frames = this.deps.frames;
7558
+ const frame = frames[this.frameIndex % frames.length];
7559
+ this.frameIndex += 1;
7560
+ if (frame === void 0) throw new Error("node bench: no frames");
7561
+ return frame;
7562
+ }
7563
+ progress(p) {
7564
+ this.deps.onProgress({
7565
+ ...p,
7566
+ cellsDone: this.cellsDone,
7567
+ cellsTotal: this.cellsTotal
7568
+ });
7569
+ }
7570
+ async warmup(steps, deviceKey, plane) {
7571
+ const t0 = this.deps.now();
7572
+ let firstMs = 0;
7573
+ for (let i = 0; i < WARMUP_CALLS; i++) {
7574
+ const started = this.deps.now();
7575
+ await this.deps.dispatch({
7576
+ steps,
7577
+ deviceKey,
7578
+ plane,
7579
+ frame: this.nextFrame()
7580
+ });
7581
+ if (i === 0) firstMs = this.deps.now() - started;
7582
+ }
7583
+ return Math.max(firstMs, 0) || this.deps.now() - t0;
7584
+ }
7585
+ ramp(opts) {
7586
+ const { input } = this.deps;
7587
+ const frameIntervalMs = 1e3 / input.fpsPerStream;
7588
+ const system = /* @__PURE__ */ new Map();
7589
+ const dispatch = async () => {
7590
+ const r = await this.deps.dispatch({
7591
+ steps: opts.steps,
7592
+ deviceKey: opts.deviceKey,
7593
+ plane: opts.plane,
7594
+ frame: this.nextFrame()
7595
+ });
7596
+ opts.onStepsRan?.(r.stepsRan);
7597
+ if (r.shed) return {
7598
+ kind: "shed",
7599
+ shedSignal: "first-class"
7600
+ };
7601
+ return {
7602
+ kind: "ok",
7603
+ inferMs: r.inferMs,
7604
+ predictMs: r.predictMs,
7605
+ detections: r.detections
7606
+ };
7607
+ };
7608
+ return executeCapacityRamp({
7609
+ ramp: opts.loads,
7610
+ fpsPerCamera: input.fpsPerStream,
7611
+ settleMs: input.settleSec * 1e3,
7612
+ measureMs: input.measureSec * 1e3,
7613
+ drainTimeoutMs: Math.max(5e3, frameIntervalMs * 20),
7614
+ slo: {
7615
+ dropRatioMax: .01,
7616
+ e2eP95MsMax: frameIntervalMs,
7617
+ stallsMax: 0
7618
+ },
7619
+ stopOnFirstFail: true,
7620
+ dispatch,
7621
+ now: this.deps.now,
7622
+ sleepUntil: this.deps.sleepUntil,
7623
+ shouldStop: this.deps.shouldStop,
7624
+ stallSnapshot: this.deps.stallSnapshot,
7625
+ rssMb: this.deps.rssMb,
7626
+ ...opts.withSystemGate ? {
7627
+ extraFailReasons: (step) => {
7628
+ const s = this.deps.sampleSystem();
7629
+ system.set(step.cameras, s);
7630
+ const out = [];
7631
+ if (s.cpuPct > 85) out.push(`cpu ${Math.round(s.cpuPct)}% > 85%`);
7632
+ if (s.ramFreePct < 15) out.push(`ram free ${Math.round(s.ramFreePct)}% < 15%`);
7633
+ return out;
7634
+ },
7635
+ onMeasureStartHook: () => {
7636
+ this.deps.sampleSystem();
7637
+ }
7638
+ } : {},
7639
+ onStep: (info) => {
7640
+ if (info.phase === "done") return;
7641
+ this.progress({
7642
+ ...opts.label,
7643
+ load: info.cameras,
7644
+ message: `${opts.label.phase === "device" ? "streams" : "sessions"} ${info.cameras} · ${info.phase}`
7645
+ });
7646
+ }
7647
+ }).then((outcome) => ({
7648
+ steps: [...outcome.steps],
7649
+ system,
7650
+ aborted: outcome.aborted
7651
+ }));
7652
+ }
7653
+ /** A cell whose ramp was cut short measured nothing reliable — null. */
7654
+ async measureDevice(deviceKey, modelId) {
7655
+ const steps = rootOnly(modelId);
7656
+ const label = {
7657
+ phase: "device",
7658
+ deviceKey,
7659
+ modelId
7660
+ };
7661
+ this.progress({
7662
+ ...label,
7663
+ load: null,
7664
+ message: "loading the model"
7665
+ });
7666
+ const warmupMs = await this.warmup(steps, deviceKey, "frame");
7667
+ const { steps: ramp, aborted } = await this.ramp({
7668
+ loads: require_dist.DEVICE_RAMP,
7669
+ steps,
7670
+ deviceKey,
7671
+ plane: "frame",
7672
+ withSystemGate: false,
7673
+ label
7674
+ });
7675
+ if (aborted) return null;
7676
+ const last = ramp.filter((s) => s.verdict.pass).at(-1);
7677
+ const first = ramp[0];
7678
+ const failed = ramp.find((s) => !s.verdict.pass);
7679
+ const single = first?.raw.e2eMsList ?? [];
7680
+ return {
7681
+ phase: "device",
7682
+ deviceKey,
7683
+ modelId,
7684
+ p50Ms: Math.round(percentile(single, .5) * 10) / 10,
7685
+ p95Ms: Math.round(percentile(single, .95) * 10) / 10,
7686
+ p99Ms: Math.round(percentile(single, .99) * 10) / 10,
7687
+ fps: last === void 0 ? 0 : Math.round(last.raw.completed / this.deps.input.measureSec * 10) / 10,
7688
+ kMax: last?.cameras ?? 0,
7689
+ limitedBy: failed === void 0 ? "none" : limitOf(failed.verdict.failReasons),
7690
+ warmupMs: Math.round(warmupMs),
7691
+ ramp: ramp.map((s) => rampStepOf(s, this.deps.input.measureSec, null))
7692
+ };
7693
+ }
7694
+ async measurePipeline(deviceKey, modelId) {
7695
+ const steps = await this.deps.pipelineSteps(modelId);
7696
+ const label = {
7697
+ phase: "pipeline",
7698
+ deviceKey,
7699
+ modelId
7700
+ };
7701
+ this.progress({
7702
+ ...label,
7703
+ load: null,
7704
+ message: "loading the pipeline"
7705
+ });
7706
+ const fired = /* @__PURE__ */ new Set();
7707
+ const warmupMs = await this.warmup(steps, deviceKey, "full");
7708
+ const { steps: ramp, system, aborted } = await this.ramp({
7709
+ loads: require_dist.PIPELINE_RAMP,
7710
+ steps,
7711
+ deviceKey,
7712
+ plane: "full",
7713
+ withSystemGate: true,
7714
+ onStepsRan: (ran) => {
7715
+ for (const s of ran) fired.add(s);
7716
+ },
7717
+ label
7718
+ });
7719
+ if (aborted) return null;
7720
+ const last = ramp.filter((s) => s.verdict.pass).at(-1);
7721
+ const failed = ramp.find((s) => !s.verdict.pass);
7722
+ return {
7723
+ phase: "pipeline",
7724
+ deviceKey,
7725
+ modelId,
7726
+ steps: this.deps.input.steps,
7727
+ maxSessions: last?.cameras ?? 0,
7728
+ limitedBy: failed === void 0 ? "none" : limitOf(failed.verdict.failReasons),
7729
+ stepsFired: this.deps.input.steps.filter((s) => fired.has(s)),
7730
+ warmupMs: Math.round(warmupMs),
7731
+ ramp: ramp.map((s) => rampStepOf(s, this.deps.input.measureSec, system.get(s.cameras) ?? null))
7732
+ };
7733
+ }
7734
+ /** Run every cell the input asks for; stops between steps when told to. */
7735
+ async run() {
7736
+ const { input } = this.deps;
7737
+ if (input.phases.includes("device")) for (const device of input.devices) for (const modelId of device.modelIds) {
7738
+ if (this.deps.shouldStop()) return;
7739
+ const cell = await this.measureDevice(device.deviceKey, modelId);
7740
+ this.cellsDone += 1;
7741
+ if (cell !== null) this.deps.onCell(cell);
7742
+ }
7743
+ if (input.phases.includes("pipeline")) for (const device of input.devices) {
7744
+ const modelId = device.modelIds[0];
7745
+ if (modelId === void 0 || this.deps.shouldStop()) return;
7746
+ const cell = await this.measurePipeline(device.deviceKey, modelId);
7747
+ this.cellsDone += 1;
7748
+ if (cell !== null) this.deps.onCell(cell);
7749
+ }
7750
+ }
7751
+ };
7752
+ //#endregion
7753
+ //#region src/pipeline-runner/node-bench/node-bench-host.ts
7754
+ /**
7755
+ * The runner side of the node benchmark (D791): one run per node, started
7756
+ * by the orchestrator through `nodePin`, answered at once and polled. Holds
7757
+ * the run's status in RAM (a restarted runner forgets it — the orchestrator
7758
+ * reads that as `runner-lost` and restores the cluster), stops itself at the
7759
+ * deadline whatever the hub does, and deletes the models it downloaded that
7760
+ * were not installed before (unless told to keep them).
7761
+ */
7762
+ /** Node CPU % between two calls (all cores), and free RAM %. */
7763
+ function createSystemSampler() {
7764
+ const totals = () => node_os.default.cpus().reduce((acc, c) => {
7765
+ const t = c.times;
7766
+ return {
7767
+ idle: acc.idle + t.idle,
7768
+ total: acc.total + t.user + t.nice + t.sys + t.idle + t.irq
7769
+ };
7770
+ }, {
7771
+ idle: 0,
7772
+ total: 0
7773
+ });
7774
+ let prev = totals();
7775
+ return () => {
7776
+ const now = totals();
7777
+ const total = now.total - prev.total;
7778
+ const idle = now.idle - prev.idle;
7779
+ prev = now;
7780
+ return {
7781
+ cpuPct: total > 0 ? Math.round((1 - idle / total) * 1e3) / 10 : 0,
7782
+ ramFreePct: Math.round(node_os.default.freemem() / node_os.default.totalmem() * 1e3) / 10
7783
+ };
7784
+ };
7785
+ }
7786
+ /** Runs kept for status reads after they end. */
7787
+ var KEEP_FINISHED = 5;
7788
+ var NodeBenchHost = class {
7789
+ deps;
7790
+ runs = /* @__PURE__ */ new Map();
7791
+ constructor(deps) {
7792
+ this.deps = deps;
7793
+ }
7794
+ now() {
7795
+ return this.deps.now?.() ?? Date.now();
7796
+ }
7797
+ start(raw) {
7798
+ const input = require_dist.NodeBenchRunnerInputSchema.parse(raw);
7799
+ if (this.runs.get(input.runId) !== void 0) return {
7800
+ accepted: true,
7801
+ runId: input.runId
7802
+ };
7803
+ 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`);
7804
+ const state = {
7805
+ stop: false,
7806
+ status: {
7807
+ runId: input.runId,
7808
+ nodeId: this.deps.nodeId,
7809
+ state: "running",
7810
+ progress: null,
7811
+ cells: [],
7812
+ fingerprint: null,
7813
+ clip: null,
7814
+ activeCameraIds: [...this.deps.activeCameraIds()],
7815
+ deletedModels: [],
7816
+ error: null,
7817
+ startedAtMs: this.now(),
7818
+ finishedAtMs: null
7819
+ }
7820
+ };
7821
+ this.runs.set(input.runId, state);
7822
+ this.prune();
7823
+ this.execute(input, state);
7824
+ return {
7825
+ accepted: true,
7826
+ runId: input.runId
7827
+ };
7828
+ }
7829
+ status(runId) {
7830
+ return this.runs.get(runId)?.status ?? null;
7831
+ }
7832
+ abort(runId) {
7833
+ const run = this.runs.get(runId);
7834
+ if (run === void 0 || run.status.state !== "running") return { aborted: false };
7835
+ run.stop = true;
7836
+ return { aborted: true };
7837
+ }
7838
+ prune() {
7839
+ const finished = [...this.runs.values()].filter((r) => r.status.state !== "running").toSorted((a, b) => b.status.startedAtMs - a.status.startedAtMs);
7840
+ for (const old of finished.slice(KEEP_FINISHED)) this.runs.delete(old.status.runId);
7841
+ }
7842
+ patch(state, patch) {
7843
+ state.status = {
7844
+ ...state.status,
7845
+ ...patch
7846
+ };
7847
+ }
7848
+ noteActiveCameras(state) {
7849
+ const seen = new Set([...state.status.activeCameraIds, ...this.deps.activeCameraIds()]);
7850
+ if (seen.size !== state.status.activeCameraIds.length) this.patch(state, { activeCameraIds: [...seen].toSorted((a, b) => a - b) });
7851
+ }
7852
+ async execute(input, state) {
7853
+ const shouldStop = () => state.stop || this.now() >= input.deadlineAtMs;
7854
+ const sampler = this.deps.startStallSampler();
7855
+ const installedBefore = await this.deps.installedDetectorModels().catch(() => null);
7856
+ try {
7857
+ this.patch(state, { progress: this.preparing(input, "decoding the bench frames") });
7858
+ const deviceKeys = input.devices.map((d) => d.deviceKey);
7859
+ const [frameSet, fingerprint] = await Promise.all([this.deps.loadFrames(), this.deps.fingerprint(deviceKeys).catch(() => null)]);
7860
+ this.patch(state, {
7861
+ clip: frameSet.clip,
7862
+ fingerprint
7863
+ });
7864
+ this.deps.log("info", "node bench start", {
7865
+ runId: input.runId,
7866
+ clip: frameSet.clip.id,
7867
+ frames: frameSet.frames.length,
7868
+ devices: deviceKeys,
7869
+ phases: input.phases
7870
+ });
7871
+ await new NodeBenchDriver({
7872
+ input,
7873
+ frames: frameSet.frames,
7874
+ dispatch: this.deps.dispatch,
7875
+ pipelineSteps: (modelId) => this.deps.pipelineSteps(modelId, input.steps),
7876
+ now: () => this.now(),
7877
+ sleepUntil: this.deps.sleepUntil ?? ((t) => new Promise((resolve) => {
7878
+ setTimeout(resolve, Math.max(0, t - this.now())).unref?.();
7879
+ })),
7880
+ shouldStop,
7881
+ stallSnapshot: sampler.snapshot,
7882
+ rssMb: () => Math.round(process.memoryUsage().rss / 1e6),
7883
+ sampleSystem: this.deps.sampleSystem ?? createSystemSampler(),
7884
+ onProgress: (progress) => {
7885
+ this.noteActiveCameras(state);
7886
+ this.patch(state, { progress });
7887
+ },
7888
+ onCell: (cell) => {
7889
+ this.patch(state, { cells: [...state.status.cells, cell] });
7890
+ this.deps.log("info", "node bench cell", {
7891
+ runId: input.runId,
7892
+ phase: cell.phase,
7893
+ deviceKey: cell.deviceKey,
7894
+ modelId: cell.modelId,
7895
+ ...cell.phase === "device" ? {
7896
+ kMax: cell.kMax,
7897
+ fps: cell.fps,
7898
+ p95Ms: cell.p95Ms
7899
+ } : {
7900
+ maxSessions: cell.maxSessions,
7901
+ stepsFired: cell.stepsFired.join(",")
7902
+ },
7903
+ limitedBy: cell.limitedBy
7904
+ });
7905
+ }
7906
+ }).run();
7907
+ this.patch(state, { state: shouldStop() ? "aborted" : "done" });
7908
+ } catch (err) {
7909
+ const error = err instanceof Error ? err.message : String(err);
7910
+ this.deps.log("warn", "node bench failed", {
7911
+ runId: input.runId,
7912
+ error
7913
+ });
7914
+ this.patch(state, {
7915
+ state: "failed",
7916
+ error
7917
+ });
7918
+ } finally {
7919
+ sampler.stop();
7920
+ await this.cleanupModels(input, state, installedBefore);
7921
+ this.patch(state, {
7922
+ finishedAtMs: this.now(),
7923
+ progress: null
7924
+ });
7925
+ }
7926
+ }
7927
+ preparing(input, message) {
7928
+ return {
7929
+ phase: "preparing",
7930
+ deviceKey: null,
7931
+ modelId: null,
7932
+ load: null,
7933
+ cellsDone: 0,
7934
+ cellsTotal: (input.phases.includes("device") ? input.devices.reduce((n, d) => n + d.modelIds.length, 0) : 0) + (input.phases.includes("pipeline") ? input.devices.length : 0),
7935
+ message
7936
+ };
7937
+ }
7938
+ /** Delete the candidate detectors the run fetched that were not installed before. */
7939
+ async cleanupModels(input, state, before) {
7940
+ if (input.keepDownloadedModels || before === null) return;
7941
+ const after = await this.deps.installedDetectorModels().catch(() => null);
7942
+ if (after === null) return;
7943
+ const candidates = new Set(input.devices.flatMap((d) => d.modelIds));
7944
+ const fetched = [...after].filter((key) => candidates.has(key.split("/")[0] ?? "") && !before.has(key));
7945
+ const deleted = [];
7946
+ for (const key of fetched) try {
7947
+ await this.deps.deleteDetectorModel(key);
7948
+ deleted.push(key);
7949
+ } catch (err) {
7950
+ this.deps.log("warn", "node bench: could not delete a downloaded model", {
7951
+ model: key,
7952
+ error: err instanceof Error ? err.message : String(err)
7953
+ });
7954
+ }
7955
+ if (deleted.length > 0) this.patch(state, { deletedModels: deleted });
7956
+ }
7957
+ };
7958
+ //#endregion
7959
+ //#region src/pipeline-runner/node-bench/node-bench-wiring.ts
7960
+ /**
7961
+ * Binds the node benchmark host (D791) to the runner's in-process executor:
7962
+ * every call is pinned to THIS node, so the frame never leaves the process
7963
+ * group that owns it (D9) and the numbers are this node's.
7964
+ */
7965
+ var DETECTOR_ADDON = "object-detection";
7966
+ /** A stall this long fails a ramp step (the frame interval at 4 fps). */
7967
+ var BENCH_STALL_MS = 250;
7968
+ function isModelFormat(value) {
7969
+ return require_dist.MODEL_FORMATS.includes(value);
7970
+ }
7971
+ function createNodeBenchHost(deps) {
7972
+ const api = () => {
7973
+ const a = deps.ctx().api;
7974
+ if (!a) throw new Error("node bench: ctx.api unavailable");
7975
+ return a;
7976
+ };
7977
+ const pin = () => require_dist.nodePin(deps.nodeId);
7978
+ const log = deps.ctx().logger;
7979
+ const decodeJpeg = async (jpeg, maxWidth) => {
7980
+ const { data, info } = await (await require_lazy_sharp.getSharp())(Buffer.from(jpeg)).resize({
7981
+ width: maxWidth,
7982
+ withoutEnlargement: true
7983
+ }).removeAlpha().raw().toBuffer({ resolveWithObject: true });
7984
+ return {
7985
+ data: new Uint8Array(data),
7986
+ width: info.width,
7987
+ height: info.height
7988
+ };
7989
+ };
7990
+ return new NodeBenchHost({
7991
+ nodeId: deps.nodeId,
7992
+ loadFrames: () => loadBenchFrames({
7993
+ dataDir: deps.ctx().dataDir,
7994
+ ensureFfmpeg: () => deps.ctx().deps.ensureFfmpeg(),
7995
+ fetch: globalThis.fetch,
7996
+ referenceImages: async () => {
7997
+ const list = await api().pipelineExecutor.listReferenceImages.query(void 0, pin());
7998
+ const out = [];
7999
+ for (const entry of list) {
8000
+ const img = await api().pipelineExecutor.getReferenceImage.query({ filename: entry.filename }, pin());
8001
+ if (img) out.push(new Uint8Array(Buffer.from(img.base64, "base64")));
8002
+ }
8003
+ return out;
8004
+ },
8005
+ decodeJpeg,
8006
+ log: (message, meta) => log.info(message, meta ? { meta } : void 0)
8007
+ }),
8008
+ dispatch: async ({ steps, deviceKey, plane, frame }) => {
8009
+ const result = await api().pipelineExecutor.runPipeline.mutate({
8010
+ steps: [...steps],
8011
+ frame: {
8012
+ data: frame.data,
8013
+ format: "rgb",
8014
+ width: frame.width,
8015
+ height: frame.height,
8016
+ timestamp: Date.now()
8017
+ },
8018
+ plane,
8019
+ deviceKey
8020
+ }, pin());
8021
+ return {
8022
+ shed: result.shed === true,
8023
+ detections: result.detections.length,
8024
+ inferMs: result.debug?.totalInferenceMs ?? 0,
8025
+ predictMs: result.debug?.predictMs ?? 0,
8026
+ stepsRan: [...new Set((result.debug?.stepTimings ?? []).filter((t) => t.ms > 0).map((t) => t.source))]
8027
+ };
8028
+ },
8029
+ pipelineSteps: async (rootModelId, stepIds) => {
8030
+ return require_dist.buildBenchStepTree(await api().pipelineExecutor.getSchema.query(void 0, pin()), rootModelId, stepIds);
8031
+ },
8032
+ fingerprint: async (deviceKeys) => {
8033
+ const pools = {};
8034
+ for (const deviceKey of deviceKeys) try {
8035
+ const t = await api().pipelineExecutor.getEffectiveTuning.query({ deviceKey }, pin());
8036
+ pools[deviceKey] = {
8037
+ runtime: t.runtime,
8038
+ numWorkers: t.numWorkers,
8039
+ concurrency: t.concurrency,
8040
+ batchMode: t.batchMode
8041
+ };
8042
+ } catch {}
8043
+ return {
8044
+ pipelineVersion: deps.pipelineVersion,
8045
+ cpuModel: node_os.default.cpus()[0]?.model?.trim() ?? null,
8046
+ ramGb: Math.round(node_os.default.totalmem() / 1e9),
8047
+ os: `${node_os.default.platform()}-${node_os.default.arch()}`,
8048
+ pools
8049
+ };
8050
+ },
8051
+ installedDetectorModels: async () => {
8052
+ const models = await api().pipelineExecutor.getAddonModels.query({ addonId: DETECTOR_ADDON }, pin());
8053
+ const out = /* @__PURE__ */ new Set();
8054
+ for (const m of models) for (const [format, status] of Object.entries(m.formats)) if (status?.downloaded === true) out.add(`${m.id}/${format}`);
8055
+ return out;
8056
+ },
8057
+ deleteDetectorModel: async (key) => {
8058
+ const [modelId = "", format = ""] = key.split("/");
8059
+ if (!isModelFormat(format)) return;
8060
+ await api().pipelineExecutor.deleteModel.mutate({
8061
+ addonId: DETECTOR_ADDON,
8062
+ modelId,
8063
+ format
8064
+ }, pin());
8065
+ },
8066
+ activeCameraIds: deps.activeCameraIds,
8067
+ startStallSampler: () => startStallSampler({ reportThresholdMs: BENCH_STALL_MS }),
8068
+ log: (level, message, meta) => log[level](message, meta ? { meta } : void 0)
8069
+ });
8070
+ }
7363
8071
  /**
7364
8072
  * Normalised boxes for the subjects worth a tile, highest confidence first,
7365
8073
  * capped.
@@ -10010,6 +10718,25 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
10010
10718
  * `executionFormat` reports — so the servability check and the dispatch
10011
10719
  * cannot disagree about which format was verified.
10012
10720
  */
10721
+ nodeBenchHost = null;
10722
+ nodeBench() {
10723
+ this.nodeBenchHost ??= createNodeBenchHost({
10724
+ nodeId: this.nodeId,
10725
+ ctx: () => this.ctx,
10726
+ activeCameraIds: () => (this.runner?.getAllCameraMetrics() ?? []).filter((m) => m.phase !== "idle").map((m) => m.deviceId),
10727
+ pipelineVersion: null
10728
+ });
10729
+ return this.nodeBenchHost;
10730
+ }
10731
+ async startNodeBench(input) {
10732
+ return this.nodeBench().start(input);
10733
+ }
10734
+ async getNodeBenchStatus(input) {
10735
+ return this.nodeBench().status(input.runId);
10736
+ }
10737
+ async abortNodeBench(input) {
10738
+ return this.nodeBench().abort(input.runId);
10739
+ }
10013
10740
  async runStatelessStep(input) {
10014
10741
  const convention = await this.cropConvention.refresh(this.ctxIfReady?.api);
10015
10742
  return runStatelessStep({