@camstack/addon-pipeline 1.2.188 → 1.2.189

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.
@@ -301,6 +301,16 @@ var DEFAULT_NODE_DECODE_BUDGET_MB = 8192;
301
301
  * `native-lease-config.ts` on why "unset" includes a knob sitting at its
302
302
  * default); anything absent falls back to what the worker ships with.
303
303
  */
304
+ /**
305
+ * How many frames of one camera the runner may have in inference at once:
306
+ * the worker's hold minus one, never below one. The hold evicts oldest-first
307
+ * when a newer marked frame lands, so the (hold)th outstanding ref is the one
308
+ * that makes the first unreadable. See `runner.ts` `maxInflightPerCamera`.
309
+ */
310
+ function maxInflightPerCamera(override) {
311
+ const holdFrames = override.holdFrames ?? require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
312
+ return Math.max(1, holdFrames - 1);
313
+ }
304
314
  function decodeWorkerFootprintMb(override) {
305
315
  const budgetMb = override.budgetMb ?? require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
306
316
  const holdFrames = override.holdFrames ?? require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
@@ -4520,6 +4530,7 @@ var PipelineRunner = class {
4520
4530
  detectionQueue,
4521
4531
  inferenceTimes: [],
4522
4532
  shedFrames: 0,
4533
+ inflight: 0,
4523
4534
  processedCount: 0,
4524
4535
  startTime: Date.now(),
4525
4536
  phase: initialPhase,
@@ -4626,8 +4637,23 @@ var PipelineRunner = class {
4626
4637
  * time so the pump can copy the pixels while the frame is still reserved.
4627
4638
  */
4628
4639
  detectionBackpressure(deviceId) {
4629
- if (!this.cameras.has(deviceId)) return true;
4630
- return this.semaphore.available <= 0;
4640
+ const state = this.cameras.get(deviceId);
4641
+ if (!state) return true;
4642
+ return this.semaphore.available <= 0 || state.inflight >= this.maxInflightPerCamera();
4643
+ }
4644
+ /** The per-camera in-flight cap in force (config, else the shipped hold minus one). */
4645
+ maxInflightPerCamera() {
4646
+ return Math.max(1, this.config.maxInflightPerCamera ?? require_dist.DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames - 1);
4647
+ }
4648
+ /**
4649
+ * Follow a resized native-frame hold. Running inferences are untouched; the
4650
+ * next pick is measured against the new cap.
4651
+ */
4652
+ setMaxInflightPerCamera(cap) {
4653
+ this.config = {
4654
+ ...this.config,
4655
+ maxInflightPerCamera: cap
4656
+ };
4631
4657
  }
4632
4658
  enqueueReservedDetectionFrame(deviceId, frame, handle) {
4633
4659
  const state = this.cameras.get(deviceId);
@@ -4912,6 +4938,7 @@ var PipelineRunner = class {
4912
4938
  async processWithSemaphore(deviceId, entry, frameInput, state, streamType) {
4913
4939
  const pickedAt = Date.now();
4914
4940
  const { frame, handle } = entry;
4941
+ state.inflight += 1;
4915
4942
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
4916
4943
  const rawRelease = await this.semaphore.acquire();
4917
4944
  let released = false;
@@ -4953,6 +4980,7 @@ var PipelineRunner = class {
4953
4980
  } catch {
4954
4981
  frameReleaseReason = "error";
4955
4982
  } finally {
4983
+ state.inflight -= 1;
4956
4984
  release();
4957
4985
  try {
4958
4986
  if (frameReleaseReason === "success" && handle && isContinuousRetainEnabled()) await this.config.hydrateRetainedFrame?.(handle);
@@ -5052,7 +5080,7 @@ var PipelineRunner = class {
5052
5080
  } catch {}
5053
5081
  }
5054
5082
  pickNextDetectionFrame() {
5055
- for (const [deviceId, state] of this.cameras) if (state.registration.detectionMode === "always-on" && state.detectionQueue.size > 0) return {
5083
+ for (const [deviceId, state] of this.cameras) if (state.registration.detectionMode === "always-on" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) return {
5056
5084
  deviceId,
5057
5085
  entry: state.detectionQueue.dequeue(),
5058
5086
  state
@@ -5065,7 +5093,7 @@ var PipelineRunner = class {
5065
5093
  if (!deviceId) continue;
5066
5094
  const state = this.cameras.get(deviceId);
5067
5095
  if (!state) continue;
5068
- if (state.phase === "active" && state.detectionQueue.size > 0) {
5096
+ if (state.phase === "active" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
5069
5097
  this.defaultRoundRobinIndex = (idx + 1) % this.defaultRoundRobinKeys.length;
5070
5098
  const entry = state.detectionQueue.dequeue();
5071
5099
  if (!entry) continue;
@@ -5795,6 +5823,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
5795
5823
  this.runner = new PipelineRunner({
5796
5824
  maxQueueDepth: this.config.maxQueueDepth,
5797
5825
  maxConcurrentInferences: this.config.maxConcurrentInferences,
5826
+ maxInflightPerCamera: maxInflightPerCamera(this.nativeLease.current()),
5798
5827
  targetLoadPercent: this.config.targetLoadPercent,
5799
5828
  minThrottledFps: this.config.minThrottledFps,
5800
5829
  processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
@@ -7045,6 +7074,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends require_dist.BaseAdd
7045
7074
  async refreshNativeLeaseSettings() {
7046
7075
  await this.nativeLease.refresh(this.ctxIfReady?.api);
7047
7076
  this.liveWorkers?.resizeFootprint(decodeWorkerFootprintMb(this.nativeLease.current()));
7077
+ this.runner?.setMaxInflightPerCamera(maxInflightPerCamera(this.nativeLease.current()));
7048
7078
  await this.nodeLimits?.refresh(this.ctxIfReady?.api);
7049
7079
  this.applyNodeDecodeLimits("lease-refresh");
7050
7080
  }
@@ -295,6 +295,16 @@ var DEFAULT_NODE_DECODE_BUDGET_MB = 8192;
295
295
  * `native-lease-config.ts` on why "unset" includes a knob sitting at its
296
296
  * default); anything absent falls back to what the worker ships with.
297
297
  */
298
+ /**
299
+ * How many frames of one camera the runner may have in inference at once:
300
+ * the worker's hold minus one, never below one. The hold evicts oldest-first
301
+ * when a newer marked frame lands, so the (hold)th outstanding ref is the one
302
+ * that makes the first unreadable. See `runner.ts` `maxInflightPerCamera`.
303
+ */
304
+ function maxInflightPerCamera(override) {
305
+ const holdFrames = override.holdFrames ?? DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
306
+ return Math.max(1, holdFrames - 1);
307
+ }
298
308
  function decodeWorkerFootprintMb(override) {
299
309
  const budgetMb = override.budgetMb ?? DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb;
300
310
  const holdFrames = override.holdFrames ?? DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames;
@@ -4513,6 +4523,7 @@ var PipelineRunner = class {
4513
4523
  detectionQueue,
4514
4524
  inferenceTimes: [],
4515
4525
  shedFrames: 0,
4526
+ inflight: 0,
4516
4527
  processedCount: 0,
4517
4528
  startTime: Date.now(),
4518
4529
  phase: initialPhase,
@@ -4619,8 +4630,23 @@ var PipelineRunner = class {
4619
4630
  * time so the pump can copy the pixels while the frame is still reserved.
4620
4631
  */
4621
4632
  detectionBackpressure(deviceId) {
4622
- if (!this.cameras.has(deviceId)) return true;
4623
- return this.semaphore.available <= 0;
4633
+ const state = this.cameras.get(deviceId);
4634
+ if (!state) return true;
4635
+ return this.semaphore.available <= 0 || state.inflight >= this.maxInflightPerCamera();
4636
+ }
4637
+ /** The per-camera in-flight cap in force (config, else the shipped hold minus one). */
4638
+ maxInflightPerCamera() {
4639
+ return Math.max(1, this.config.maxInflightPerCamera ?? DEFAULT_NATIVE_LEASE_SETTINGS.holdFrames - 1);
4640
+ }
4641
+ /**
4642
+ * Follow a resized native-frame hold. Running inferences are untouched; the
4643
+ * next pick is measured against the new cap.
4644
+ */
4645
+ setMaxInflightPerCamera(cap) {
4646
+ this.config = {
4647
+ ...this.config,
4648
+ maxInflightPerCamera: cap
4649
+ };
4624
4650
  }
4625
4651
  enqueueReservedDetectionFrame(deviceId, frame, handle) {
4626
4652
  const state = this.cameras.get(deviceId);
@@ -4905,6 +4931,7 @@ var PipelineRunner = class {
4905
4931
  async processWithSemaphore(deviceId, entry, frameInput, state, streamType) {
4906
4932
  const pickedAt = Date.now();
4907
4933
  const { frame, handle } = entry;
4934
+ state.inflight += 1;
4908
4935
  const enqueuedAt = frame._enqueuedAt ?? pickedAt;
4909
4936
  const rawRelease = await this.semaphore.acquire();
4910
4937
  let released = false;
@@ -4946,6 +4973,7 @@ var PipelineRunner = class {
4946
4973
  } catch {
4947
4974
  frameReleaseReason = "error";
4948
4975
  } finally {
4976
+ state.inflight -= 1;
4949
4977
  release();
4950
4978
  try {
4951
4979
  if (frameReleaseReason === "success" && handle && isContinuousRetainEnabled()) await this.config.hydrateRetainedFrame?.(handle);
@@ -5045,7 +5073,7 @@ var PipelineRunner = class {
5045
5073
  } catch {}
5046
5074
  }
5047
5075
  pickNextDetectionFrame() {
5048
- for (const [deviceId, state] of this.cameras) if (state.registration.detectionMode === "always-on" && state.detectionQueue.size > 0) return {
5076
+ for (const [deviceId, state] of this.cameras) if (state.registration.detectionMode === "always-on" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) return {
5049
5077
  deviceId,
5050
5078
  entry: state.detectionQueue.dequeue(),
5051
5079
  state
@@ -5058,7 +5086,7 @@ var PipelineRunner = class {
5058
5086
  if (!deviceId) continue;
5059
5087
  const state = this.cameras.get(deviceId);
5060
5088
  if (!state) continue;
5061
- if (state.phase === "active" && state.detectionQueue.size > 0) {
5089
+ if (state.phase === "active" && state.detectionQueue.size > 0 && state.inflight < this.maxInflightPerCamera()) {
5062
5090
  this.defaultRoundRobinIndex = (idx + 1) % this.defaultRoundRobinKeys.length;
5063
5091
  const entry = state.detectionQueue.dequeue();
5064
5092
  if (!entry) continue;
@@ -5788,6 +5816,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
5788
5816
  this.runner = new PipelineRunner({
5789
5817
  maxQueueDepth: this.config.maxQueueDepth,
5790
5818
  maxConcurrentInferences: this.config.maxConcurrentInferences,
5819
+ maxInflightPerCamera: maxInflightPerCamera(this.nativeLease.current()),
5791
5820
  targetLoadPercent: this.config.targetLoadPercent,
5792
5821
  minThrottledFps: this.config.minThrottledFps,
5793
5822
  processFrame: (deviceId, frame, handle) => this.runInference(deviceId, frame, handle),
@@ -7038,6 +7067,7 @@ var PipelineRunnerAddon = class PipelineRunnerAddon extends BaseAddon {
7038
7067
  async refreshNativeLeaseSettings() {
7039
7068
  await this.nativeLease.refresh(this.ctxIfReady?.api);
7040
7069
  this.liveWorkers?.resizeFootprint(decodeWorkerFootprintMb(this.nativeLease.current()));
7070
+ this.runner?.setMaxInflightPerCamera(maxInflightPerCamera(this.nativeLease.current()));
7041
7071
  await this.nodeLimits?.refresh(this.ctxIfReady?.api);
7042
7072
  this.applyNodeDecodeLimits("lease-refresh");
7043
7073
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.188",
3
+ "version": "1.2.189",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",