@camstack/addon-pipeline 1.2.37 → 1.2.39

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 (90) hide show
  1. package/dist/audio-analyzer/index.js +1 -1
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/detection-pipeline/index.js +145 -175
  4. package/dist/detection-pipeline/index.mjs +120 -150
  5. package/dist/{dist-BvFLZyhW.js → dist-BtAvtroH.js} +260 -108
  6. package/dist/{dist-Du7_SNiR.mjs → dist-COgGdFMv.mjs} +260 -108
  7. package/dist/{step-definitions-C8tk9fPJ.mjs → event-loop-stall-monitor-naFYvcOO.mjs} +139 -2
  8. package/dist/{step-definitions-i8O-hxyI.js → event-loop-stall-monitor-shfwi1_b.js} +150 -1
  9. package/dist/lazy-sharp-B-mb8uWx.mjs +20 -0
  10. package/dist/lazy-sharp-CcYuXtsa.js +25 -0
  11. package/dist/motion-wasm/index.js +3 -3
  12. package/dist/motion-wasm/index.mjs +3 -3
  13. package/dist/pipeline-runner/index.js +3316 -2428
  14. package/dist/pipeline-runner/index.mjs +3315 -2428
  15. package/dist/recorder/index.js +102 -664
  16. package/dist/recorder/index.mjs +102 -658
  17. package/dist/stream-broker/_stub.js +2 -2
  18. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C5m4hC2K.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CjrKd4xX.mjs} +3 -3
  19. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B59GNAJq.mjs +26 -0
  20. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-83qNzMSL.mjs +26 -0
  21. package/dist/stream-broker/{hostInit-WXFrgppm.mjs → hostInit-BFH17kSk.mjs} +3 -3
  22. package/dist/stream-broker/index.js +717 -211
  23. package/dist/stream-broker/index.mjs +717 -211
  24. package/dist/stream-broker/remoteEntry.js +1 -1
  25. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Dx-Vrpyt.js → MaskShapeCanvas-DI4BY7W2-yeSHfpUy.js} +1 -1
  26. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-9D-BWwQP.js → MotionZonesSettings-NcxxQN8r-Df4WxM6N.js} +1 -1
  27. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-HvAetfA4.js → PrivacyMaskSettings-APgPLF7p-COHSiTGq.js} +1 -1
  28. package/embed-dist/assets/index-4fJzjj4H.css +2 -0
  29. package/embed-dist/assets/index-Ct9M2v5n.js +112 -0
  30. package/embed-dist/index.html +2 -2
  31. package/package.json +2 -2
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-ByHi7ZAe.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CscpvIRR.mjs +0 -26
  34. package/embed-dist/assets/index-9Ob2YrIf.js +0 -343
  35. package/embed-dist/assets/index-CAe6Dg-X.css +0 -2
  36. package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
  37. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  38. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  39. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
  43. package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
  44. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  45. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  49. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  50. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  51. package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
  52. package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
  55. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  56. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  57. package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
  58. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  59. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  60. package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
  61. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  62. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  63. package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
  64. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  65. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  66. package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
  67. package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
  70. package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
  71. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  72. package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
  73. package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
  75. package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
  76. package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
  77. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  78. package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
  79. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
  80. package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
  81. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  82. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  83. package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
  84. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  85. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  86. package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
  87. package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
  88. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  89. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  90. package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
@@ -1,14 +1,14 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-Du7_SNiR.mjs";
3
- import { a as getStep, i as getDefaultModelForFormatFromDef, n as ALL_STEPS, o as getStepDefinition, r as getDefaultModelForFormat, s as resolveModelForFormat, t as ALL_PIPELINE_STEPS } from "../step-definitions-C8tk9fPJ.mjs";
2
+ import { B as errMsg, E as evaluateZoneRules, F as runtimeDevices$1, G as createEvent, K as hydrateSchema, R as supportedRuntimes$1, T as enumerateInferenceDevices, V as BaseAddon, X as nodePin, Z as parseJsonUnknown, ct as object, dt as union, et as sleep, ft as EventCategory, h as YAMNET_TO_MACRO, j as pipelineExecutorCapability, nt as array, s as DEVICE_BACKEND_TO_FORMAT, t as APPLE_SA_TO_MACRO, ut as string, w as detectionPipelineCapability, x as defaultDeviceFor$1 } from "../dist-COgGdFMv.mjs";
3
+ import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-naFYvcOO.mjs";
4
+ import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
4
5
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
5
6
  import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-Cp9f4dk6-CwUb5v3Y.mjs";
6
- import * as os from "node:os";
7
- import { spawn } from "node:child_process";
8
7
  import sharp from "sharp";
8
+ import { spawn } from "node:child_process";
9
+ import * as os from "node:os";
9
10
  import * as fs from "node:fs";
10
11
  import * as path$1 from "node:path";
11
- import { PerformanceObserver } from "node:perf_hooks";
12
12
  //#region src/detection-pipeline/runtimes.ts
13
13
  var KNOWN_PLATFORMS = [
14
14
  "darwin",
@@ -2238,6 +2238,89 @@ function normalizeOutput(output) {
2238
2238
  };
2239
2239
  }
2240
2240
  //#endregion
2241
+ //#region src/detection-pipeline/pipeline/drop-sampler.ts
2242
+ /**
2243
+ * drop-sampler — keep the record of a dropped detection without the flood.
2244
+ *
2245
+ * ## Why
2246
+ *
2247
+ * The full-frame phantom guard logs every detection it removes, on purpose: a
2248
+ * branch that drops work silently reads as "never happened", and this repo has
2249
+ * paid for that twice. Its own comment set the bound honestly — *"Bounded by
2250
+ * how rare full-frame phantoms are, which is the point: if this floods, that is
2251
+ * itself the finding."*
2252
+ *
2253
+ * It flooded. Camera 615 emits a persistent full-frame `vehicle` phantom
2254
+ * (score ~0.62, areaRatio 0.949) and the guard was writing **dozens of INFO
2255
+ * lines a second** on 2026-08-02. So the finding stands — that camera has a
2256
+ * standing phantom worth investigating — but the log volume is now costing more
2257
+ * than the record is worth, and it drowns everything else in the same stream.
2258
+ *
2259
+ * ## The shape
2260
+ *
2261
+ * FIRST occurrence of each `(deviceId, macroClass)` logs in full: the example
2262
+ * is what makes the finding actionable — the bbox, the score, the ratios.
2263
+ * After that the pair is counted and a SUMMARY is emitted per interval, so the
2264
+ * question "how often does this fire, and on what?" is still answerable — from
2265
+ * a counter rather than from N identical lines.
2266
+ *
2267
+ * Deliberately per `(device, class)` and not global: a phantom on one camera
2268
+ * must not mask a new one appearing on another, which a single global counter
2269
+ * would do.
2270
+ */
2271
+ /** How often an active drop pattern re-reports, with its count. */
2272
+ var DROP_SUMMARY_INTERVAL_MS = 6e4;
2273
+ /**
2274
+ * Decides, per `(deviceId, class)`, whether this drop is worth a line.
2275
+ *
2276
+ * Pure-ish and clock-injected so the interval logic is testable without
2277
+ * waiting a minute.
2278
+ */
2279
+ var DropSampler = class {
2280
+ seen = /* @__PURE__ */ new Map();
2281
+ now;
2282
+ intervalMs;
2283
+ constructor(intervalMs = DROP_SUMMARY_INTERVAL_MS, now = () => Date.now()) {
2284
+ this.intervalMs = intervalMs;
2285
+ this.now = now;
2286
+ }
2287
+ /**
2288
+ * Record one drop. Returns what to log.
2289
+ *
2290
+ * The first sighting of a pair yields the example. Afterwards the count
2291
+ * accumulates and a summary is released once per interval — carrying the
2292
+ * number suppressed, so nothing about the RATE is lost even though the lines
2293
+ * are.
2294
+ */
2295
+ record(deviceId, macroClass) {
2296
+ const key = `${deviceId}:${macroClass}`;
2297
+ const at = this.now();
2298
+ const prev = this.seen.get(key);
2299
+ if (prev === void 0) {
2300
+ this.seen.set(key, {
2301
+ count: 0,
2302
+ lastReportedAt: at
2303
+ });
2304
+ return {
2305
+ logExample: true,
2306
+ summaryCount: null
2307
+ };
2308
+ }
2309
+ prev.count += 1;
2310
+ if (at - prev.lastReportedAt < this.intervalMs) return {
2311
+ logExample: false,
2312
+ summaryCount: null
2313
+ };
2314
+ const n = prev.count;
2315
+ prev.count = 0;
2316
+ prev.lastReportedAt = at;
2317
+ return {
2318
+ logExample: false,
2319
+ summaryCount: n
2320
+ };
2321
+ }
2322
+ };
2323
+ //#endregion
2241
2324
  //#region src/detection-pipeline/pipeline/execution-trace.ts
2242
2325
  var traceCounter = 0;
2243
2326
  var ExecutionTraceBuilder = class {
@@ -3294,6 +3377,10 @@ function buildFrameResult(input) {
3294
3377
  height: input.height,
3295
3378
  detections,
3296
3379
  ...input.discarded !== void 0 && input.discarded.length > 0 ? { discarded: input.discarded } : {},
3380
+ ...input.shed !== void 0 ? {
3381
+ shed: true,
3382
+ shedReason: input.shed.reason
3383
+ } : {},
3297
3384
  debug: frameDebug
3298
3385
  };
3299
3386
  }
@@ -3416,6 +3503,9 @@ function reprojectCropZoneDetections(output, cropOrigin) {
3416
3503
  }
3417
3504
  var PipelineExecutor = class {
3418
3505
  opts;
3506
+ /** Bounds the full-frame-guard drop log: first example per (device,
3507
+ * class), then a counted summary per minute. See `drop-sampler.ts`. */
3508
+ dropSampler = new DropSampler();
3419
3509
  constructor(opts) {
3420
3510
  this.opts = opts;
3421
3511
  }
@@ -3487,11 +3577,15 @@ var PipelineExecutor = class {
3487
3577
  details
3488
3578
  };
3489
3579
  let discarded;
3580
+ const rootShed = {
3581
+ count: 0,
3582
+ reasons: []
3583
+ };
3490
3584
  for (const rootStep of tree.roots) {
3491
3585
  if (debug) console.log(`[executor] rootStep=${rootStep.stepId} settings=${JSON.stringify(rootStep.settings ?? {})}`);
3492
3586
  const cropPrep = rootStep.definition.extractMode === "crop-zone" && cropZoneBbox !== void 0 ? await this.prepareCropZoneInput(cropZoneBbox, fullFrameJpegProvider, imageWidth, imageHeight, deviceId) : null;
3493
3587
  const rootStart = Date.now();
3494
- const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg);
3588
+ const rawRootOutput = await this.executeStep(rootStep, cropPrep?.input ?? rootInput, cropPrep?.width ?? imageWidth, cropPrep?.height ?? imageHeight, "full-frame", void 0, void 0, traceBuilder, stepTimings, poolAgg, rootShed);
3495
3589
  const rootMs = Date.now() - rootStart;
3496
3590
  if (rawRootOutput === null) continue;
3497
3591
  const rootOutput = cropPrep && rawRootOutput.kind === "detections" ? reprojectCropZoneDetections(rawRootOutput, cropPrep.origin) : rawRootOutput;
@@ -3557,7 +3651,17 @@ var PipelineExecutor = class {
3557
3651
  const fullFrameBar = rootStep.settings?.["fullFrameGuardMaxConfidence"];
3558
3652
  const fullFrameHardArea = rootStep.settings?.["fullFrameGuardHardAreaRatio"];
3559
3653
  if (isFullFramePhantomDetection(mutable.bbox, imageWidth, imageHeight, mutable.score, typeof fullFrameBar === "number" ? fullFrameBar : void 0, typeof fullFrameHardArea === "number" ? fullFrameHardArea : void 0)) {
3560
- this.opts.logger?.info("full-frame phantom guard: detection dropped", {
3654
+ const sample = this.dropSampler.record(deviceId, String(mutable.macroClass));
3655
+ if (sample.summaryCount !== null) this.opts.logger?.info("full-frame phantom guard: still dropping", {
3656
+ tags: { deviceId },
3657
+ meta: {
3658
+ macroClass: mutable.macroClass,
3659
+ suppressed: sample.summaryCount,
3660
+ windowMs: DROP_SUMMARY_INTERVAL_MS,
3661
+ hint: "a standing full-frame phantom — check the camera framing / model, not the guard"
3662
+ }
3663
+ });
3664
+ if (sample.logExample) this.opts.logger?.info("full-frame phantom guard: detection dropped", {
3561
3665
  tags: { deviceId },
3562
3666
  meta: {
3563
3667
  macroClass: mutable.macroClass,
@@ -3619,6 +3723,7 @@ var PipelineExecutor = class {
3619
3723
  firstLevel,
3620
3724
  details,
3621
3725
  discarded,
3726
+ ...rootShed.count > 0 ? { shed: { reason: rootShed.reasons[0] ?? "pool-overload" } } : {},
3622
3727
  stepTimings,
3623
3728
  totalMs,
3624
3729
  engine: this.opts.engineRuntime,
@@ -3689,7 +3794,7 @@ var PipelineExecutor = class {
3689
3794
  return null;
3690
3795
  }
3691
3796
  }
3692
- async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg) {
3797
+ async executeStep(step, input, inputWidth, inputHeight, inputType, _parentBbox, parentClass, traceBuilder, stepTimings, poolAgg, shedAgg) {
3693
3798
  const preprocessStart = Date.now();
3694
3799
  const preprocessMs = Date.now() - preprocessStart;
3695
3800
  const inferenceStart = Date.now();
@@ -3726,6 +3831,10 @@ var PipelineExecutor = class {
3726
3831
  throw new Error(`Inference failed for step "${step.stepId}": ${errorMsg}`, { cause: err });
3727
3832
  }
3728
3833
  if (engineOutput.dropped === true) {
3834
+ if (shedAgg) {
3835
+ shedAgg.count += 1;
3836
+ shedAgg.reasons.push(engineOutput.shedReason ?? "pool-overload");
3837
+ }
3729
3838
  stepTimings.push({
3730
3839
  source: step.stepId,
3731
3840
  modelId: step.modelId,
@@ -5613,8 +5722,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5613
5722
  jpegProvider = async () => data;
5614
5723
  } else if (frame.format === "rgb" || frame.format === "bgr" || frame.format === "gray") {
5615
5724
  const channels = frame.format === "gray" ? 1 : 3;
5616
- const sharp = (await import("sharp")).default;
5617
- const jpeg = await sharp(data, { raw: {
5725
+ const jpeg = await (await getSharp())(data, { raw: {
5618
5726
  width: frame.width,
5619
5727
  height: frame.height,
5620
5728
  channels
@@ -5628,8 +5736,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5628
5736
  };
5629
5737
  jpegProvider = async () => jpeg;
5630
5738
  } else {
5631
- const sharp = (await import("sharp")).default;
5632
- const encoded = await sharp(data, { raw: {
5739
+ const encoded = await (await getSharp())(data, { raw: {
5633
5740
  width: frame.width,
5634
5741
  height: frame.height,
5635
5742
  channels: 3
@@ -6242,8 +6349,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6242
6349
  /** Parse JPEG/PNG dimensions without decoding the full image */
6243
6350
  async getJpegDimensions(buf) {
6244
6351
  try {
6245
- const sharp = (await import("sharp")).default;
6246
- const { width, height } = await sharp(buf).metadata();
6352
+ const { width, height } = await (await getSharp())(buf).metadata();
6247
6353
  return {
6248
6354
  width: width ?? 640,
6249
6355
  height: height ?? 640
@@ -7198,142 +7304,6 @@ function applyDeviceOverridesToTree(tree, rootStepId, overrides) {
7198
7304
  } : root) };
7199
7305
  }
7200
7306
  //#endregion
7201
- //#region src/detection-pipeline/diagnostics/event-loop-stall-monitor.ts
7202
- /**
7203
- * event-loop-stall-monitor — names the thing that freezes the detection process.
7204
- *
7205
- * ## What this exists to settle
7206
- *
7207
- * On 2026-08-01/02 inference across the whole fleet stalled in bursts: 759
7208
- * calls in 90 minutes crossed the runner's 20s deadline, p50 41s and max 80s,
7209
- * and EVERY one of them eventually resolved. Six explanations were killed with
7210
- * telemetry alone:
7211
- *
7212
- * - not a slow GPU or a wedged pool — the work completes, just enormously late;
7213
- * - not CPU starvation — hosts sat at 8-48% during the freezes;
7214
- * - not swap — `pswpin/pswpout` are flat zero on every host;
7215
- * - not a logging artifact — Loki's timestamp trails the in-process one by
7216
- * 0.1-1.0s at p95, so the gaps are real, not buffered delivery;
7217
- * - not the v1.2.30 release — the stalls predate it by a day;
7218
- * - not only the idle-pool recompile — that is real and hub-side, but the
7219
- * agent stalls with no eviction and no compile at all.
7220
- *
7221
- * What IS established: the 10s stats timer — which touches no inference code —
7222
- * stops firing for 22-88s and resumes at the same instant the abandoned calls
7223
- * settle. The event loop itself freezes. From outside the process, GC and a
7224
- * synchronous block are indistinguishable, and guessing between them is how
7225
- * this investigation would go wrong. So measure it.
7226
- *
7227
- * ## How
7228
- *
7229
- * A 1s interval whose LATENESS is the freeze: a tick that arrives 40s late means
7230
- * the loop was blocked ~39s, whatever blocked it. In the same window the GC
7231
- * observer accumulates pause time. The two numbers together answer the question
7232
- * outright:
7233
- *
7234
- * - `gcMs` ≈ `stallMs` → garbage collection. Look at heap growth.
7235
- * - `gcMs` ≈ 0 → something SYNCHRONOUS blocked the loop, and GC is innocent.
7236
- *
7237
- * Heap and RSS ride along because if it is GC, the next question is always
7238
- * "growing since when?".
7239
- *
7240
- * Deliberately not a histogram (`monitorEventLoopDelay`): that reports a
7241
- * distribution, and the question here is about ONE outlier and what coincided
7242
- * with it.
7243
- */
7244
- /** Tick period. Small enough to bound attribution error, large enough to cost
7245
- * nothing on a process already doing frame work. */
7246
- var STALL_TICK_MS = 1e3;
7247
- /**
7248
- * Report a freeze only past this. The runner's own inference deadline is 20s
7249
- * and the observed freezes run 22-88s, so this is far below the signal while
7250
- * staying clear of ordinary scheduling jitter (a busy frame path routinely
7251
- * overruns a 1s timer by tens of ms).
7252
- */
7253
- var STALL_REPORT_MS = 2e3;
7254
- /**
7255
- * Attribute a freeze. Pure, because this is the ONE judgement the whole module
7256
- * exists to make and it must be testable without freezing a real event loop.
7257
- *
7258
- * `gc` when garbage collection accounts for most of the stall; a
7259
- * `synchronous-block` when it accounts for almost none — that verdict points
7260
- * away from the heap entirely and at whatever ran without yielding.
7261
- */
7262
- function attributeStall(stallMs, gcMs, gcCount, longestGcMs) {
7263
- const share = stallMs > 0 ? gcMs / stallMs : 0;
7264
- return {
7265
- stallMs,
7266
- gcMs,
7267
- gcCount,
7268
- longestGcMs,
7269
- cause: share >= .7 ? "gc" : share <= .2 ? "synchronous-block" : "mixed"
7270
- };
7271
- }
7272
- /**
7273
- * Start watching. Returns a stop function; safe to call twice.
7274
- *
7275
- * Never throws into the caller: a diagnostic that can take the detection
7276
- * process down is worse than no diagnostic. `PerformanceObserver` for `gc` is
7277
- * unavailable in some embedded runtimes, and that must degrade to
7278
- * stall-detection-without-attribution rather than a boot failure.
7279
- */
7280
- function startEventLoopStallMonitor(logger, reportThresholdMs = STALL_REPORT_MS) {
7281
- let gc = {
7282
- totalMs: 0,
7283
- count: 0,
7284
- longestMs: 0
7285
- };
7286
- let observer = null;
7287
- try {
7288
- observer = new PerformanceObserver((list) => {
7289
- for (const entry of list.getEntries()) {
7290
- gc.totalMs += entry.duration;
7291
- gc.count += 1;
7292
- if (entry.duration > gc.longestMs) gc.longestMs = entry.duration;
7293
- }
7294
- });
7295
- observer.observe({ entryTypes: ["gc"] });
7296
- } catch {
7297
- observer = null;
7298
- }
7299
- let expected = Date.now() + STALL_TICK_MS;
7300
- const timer = setInterval(() => {
7301
- const now = Date.now();
7302
- const lateBy = now - expected;
7303
- const window = gc;
7304
- gc = {
7305
- totalMs: 0,
7306
- count: 0,
7307
- longestMs: 0
7308
- };
7309
- expected = now + STALL_TICK_MS;
7310
- if (lateBy < reportThresholdMs) return;
7311
- const verdict = attributeStall(lateBy, Math.round(window.totalMs), window.count, Math.round(window.longestMs));
7312
- const mem = process.memoryUsage();
7313
- logger.warn("event loop stalled", { meta: {
7314
- stallMs: verdict.stallMs,
7315
- cause: verdict.cause,
7316
- gcMs: verdict.gcMs,
7317
- gcCount: verdict.gcCount,
7318
- longestGcMs: verdict.longestGcMs,
7319
- gcObserved: observer !== null,
7320
- heapUsedMb: Math.round(mem.heapUsed / 1e6),
7321
- heapTotalMb: Math.round(mem.heapTotal / 1e6),
7322
- rssMb: Math.round(mem.rss / 1e6),
7323
- externalMb: Math.round(mem.external / 1e6),
7324
- arrayBuffersMb: Math.round(mem.arrayBuffers / 1e6)
7325
- } });
7326
- }, STALL_TICK_MS);
7327
- timer.unref?.();
7328
- let stopped = false;
7329
- return () => {
7330
- if (stopped) return;
7331
- stopped = true;
7332
- clearInterval(timer);
7333
- observer?.disconnect();
7334
- };
7335
- }
7336
- //#endregion
7337
7307
  //#region src/detection-pipeline/python-readiness.ts
7338
7308
  /**
7339
7309
  * Resolve the embedded Python once, turning the null/throw failure modes into