@camstack/addon-pipeline 1.2.365 → 1.2.366

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.
@@ -3085,6 +3085,199 @@ var RtpPacketizer = class {
3085
3085
  }
3086
3086
  };
3087
3087
  //#endregion
3088
+ //#region src/stream-broker/stream-broker/broker-mem-probe.ts
3089
+ /** Cadence of the `[broker-mem]` line — aligned with the runner's `[mem]`. */
3090
+ var BROKER_MEM_INTERVAL_MS = 5 * 6e4;
3091
+ var copiedPackets = 0;
3092
+ var copiedBytes = 0;
3093
+ /** O(1) per packet: called by `copyOwned`. No allocation. */
3094
+ function recordCopyOwned(bytes) {
3095
+ copiedPackets += 1;
3096
+ copiedBytes += bytes;
3097
+ }
3098
+ function readCopyOwnedTotals() {
3099
+ return {
3100
+ packets: copiedPackets,
3101
+ bytes: copiedBytes
3102
+ };
3103
+ }
3104
+ /** `RssAnon:` / `RssFile:` out of `/proc/self/status` text; null when absent. */
3105
+ function parseProcStatus(text) {
3106
+ const anon = /^RssAnon:\s+(\d+)\s*kB/m.exec(text);
3107
+ const file = /^RssFile:\s+(\d+)\s*kB/m.exec(text);
3108
+ if (anon === null || file === null) return null;
3109
+ return {
3110
+ rssAnonKb: Number(anon[1]),
3111
+ rssFileKb: Number(file[1])
3112
+ };
3113
+ }
3114
+ /**
3115
+ * Summarise `/proc/self/maps`. Cheaper than `smaps_rollup`'s kernel page walk:
3116
+ * the file is one line per mapping and needs no per-page accounting. The
3117
+ * `[heap]` figure is the brk extent (virtual), which is the number glibc's
3118
+ * main arena grows when fragmented.
3119
+ *
3120
+ * Line shape: `start-end perms offset dev inode [path]` — an anonymous mapping
3121
+ * has no sixth field.
3122
+ */
3123
+ function parseProcMaps(text) {
3124
+ let heapBytes = 0;
3125
+ let anonMappings = 0;
3126
+ let anonBytes = 0;
3127
+ let from = 0;
3128
+ while (from < text.length) {
3129
+ let to = text.indexOf("\n", from);
3130
+ if (to === -1) to = text.length;
3131
+ const line = text.slice(from, to);
3132
+ from = to + 1;
3133
+ const dash = line.indexOf("-");
3134
+ const space = line.indexOf(" ");
3135
+ if (dash <= 0 || space <= dash) continue;
3136
+ const size = Number.parseInt(line.slice(dash + 1, space), 16) - Number.parseInt(line.slice(0, dash), 16);
3137
+ if (!Number.isFinite(size)) continue;
3138
+ const path = line.split(/\s+/, 6)[5];
3139
+ if (path === void 0 || path === "") {
3140
+ anonMappings += 1;
3141
+ anonBytes += size;
3142
+ } else if (path === "[heap]") heapBytes += size;
3143
+ }
3144
+ return {
3145
+ heapBytes,
3146
+ anonMappings,
3147
+ anonBytes
3148
+ };
3149
+ }
3150
+ /** Opened/closed consumers since the last reading; `drain()` resets. */
3151
+ var ConsumerChurn = class {
3152
+ opened = 0;
3153
+ closed = 0;
3154
+ openedByKind = /* @__PURE__ */ new Map();
3155
+ closedByKind = /* @__PURE__ */ new Map();
3156
+ recordOpen(kind) {
3157
+ this.opened += 1;
3158
+ this.openedByKind.set(kind, (this.openedByKind.get(kind) ?? 0) + 1);
3159
+ }
3160
+ recordClose(kind) {
3161
+ this.closed += 1;
3162
+ this.closedByKind.set(kind, (this.closedByKind.get(kind) ?? 0) + 1);
3163
+ }
3164
+ drain() {
3165
+ const reading = {
3166
+ opened: this.opened,
3167
+ closed: this.closed,
3168
+ openedByKind: Object.fromEntries(this.openedByKind),
3169
+ closedByKind: Object.fromEntries(this.closedByKind)
3170
+ };
3171
+ this.opened = 0;
3172
+ this.closed = 0;
3173
+ this.openedByKind = /* @__PURE__ */ new Map();
3174
+ this.closedByKind = /* @__PURE__ */ new Map();
3175
+ return reading;
3176
+ }
3177
+ };
3178
+ /** The one churn counter of this process (the broker runner). */
3179
+ var consumerChurn = new ConsumerChurn();
3180
+ /**
3181
+ * Sliding-window limiter for the attach/detach lines. `admit(now)` answers
3182
+ * whether this line may be written; the number of lines swallowed since the
3183
+ * last admitted one is returned with it so the next line can say so (a branch
3184
+ * that drops work names it).
3185
+ */
3186
+ function createLineLimiter(maxPerWindow, windowMs) {
3187
+ let windowStart = 0;
3188
+ let inWindow = 0;
3189
+ let suppressed = 0;
3190
+ return (now) => {
3191
+ if (now - windowStart >= windowMs) {
3192
+ windowStart = now;
3193
+ inWindow = 0;
3194
+ }
3195
+ if (inWindow >= maxPerWindow) {
3196
+ suppressed += 1;
3197
+ return {
3198
+ admit: false,
3199
+ suppressedBefore: 0
3200
+ };
3201
+ }
3202
+ inWindow += 1;
3203
+ const suppressedBefore = suppressed;
3204
+ suppressed = 0;
3205
+ return {
3206
+ admit: true,
3207
+ suppressedBefore
3208
+ };
3209
+ };
3210
+ }
3211
+ var consumerLineLimiter = createLineLimiter(40, 6e4);
3212
+ /**
3213
+ * One consumer attached to / detached from a broker: counted always (the
3214
+ * `[broker-mem]` line reports opened/closed since the last line), written at
3215
+ * info under a rate limit so a step in RSS can be matched to an event. The
3216
+ * line after a suppressed burst says how many were swallowed.
3217
+ */
3218
+ function reportConsumerEvent(logger, event, kind, deviceId, total, now = Date.now()) {
3219
+ if (event === "attach") consumerChurn.recordOpen(kind);
3220
+ else consumerChurn.recordClose(kind);
3221
+ if (logger === void 0 || logger === null) return;
3222
+ const verdict = consumerLineLimiter(now);
3223
+ if (!verdict.admit) return;
3224
+ const suppressed = verdict.suppressedBefore > 0 ? ` suppressedBefore=${verdict.suppressedBefore}` : "";
3225
+ logger.info(`consumer ${event} kind=${kind} total=${total}${suppressed}`, { ...deviceId !== void 0 ? { tags: { deviceId } } : {} });
3226
+ }
3227
+ var MB = 1024 * 1024;
3228
+ function ringBytesOf(s) {
3229
+ return s.preBufferBytes + s.clipBytes + s.rtpRingBytes + s.keyframeCacheBytes;
3230
+ }
3231
+ function mb(bytes) {
3232
+ return (bytes / MB).toFixed(1);
3233
+ }
3234
+ function kindList(counts) {
3235
+ const keys = Object.keys(counts).sort();
3236
+ return keys.length === 0 ? "none" : keys.map((k) => `${k}:${counts[k]}`).join(",");
3237
+ }
3238
+ /** One greppable `key=value` line; no per-device breakdown beyond the top N. */
3239
+ function formatBrokerMem(input) {
3240
+ const { samples, proc, churn, copied } = input;
3241
+ const consumers = {};
3242
+ let rtsp = 0;
3243
+ let audio = 0;
3244
+ let pipe = 0;
3245
+ let ingesting = 0;
3246
+ let planes = 0;
3247
+ let pre = 0;
3248
+ let clip = 0;
3249
+ let rtp = 0;
3250
+ let kf = 0;
3251
+ let qSum = 0;
3252
+ let qMax = 0;
3253
+ for (const s of samples) {
3254
+ if (s.ingesting) ingesting += 1;
3255
+ if (s.annexBPlaneActive) planes += 1;
3256
+ rtsp += s.rtspSessions;
3257
+ audio += s.audioSubscribers;
3258
+ pipe += s.pipeClients;
3259
+ pre += s.preBufferBytes;
3260
+ clip += s.clipBytes;
3261
+ rtp += s.rtpRingBytes;
3262
+ kf += s.keyframeCacheBytes;
3263
+ qSum += s.writeQueueSumBytes;
3264
+ if (s.writeQueueMaxBytes > qMax) qMax = s.writeQueueMaxBytes;
3265
+ for (const [kind, n] of Object.entries(s.encodedByKind)) consumers[kind] = (consumers[kind] ?? 0) + n;
3266
+ }
3267
+ const top = [...samples].filter((s) => s.deviceId !== void 0).sort((a, b) => ringBytesOf(b) - ringBytesOf(a)).slice(0, 5).map((s) => `${s.deviceId}:${mb(ringBytesOf(s))}`).join(",");
3268
+ const parts = ["[broker-mem]"];
3269
+ if (proc.status !== null) parts.push(`rssAnonMb=${(proc.status.rssAnonKb / 1024).toFixed(0)}`, `rssFileMb=${(proc.status.rssFileKb / 1024).toFixed(0)}`);
3270
+ if (proc.maps !== null) parts.push(`heapMapMb=${mb(proc.maps.heapBytes)}`, `anonMaps=${proc.maps.anonMappings}`, `anonMapsMb=${mb(proc.maps.anonBytes)}`);
3271
+ parts.push(`brokers=${samples.length}`, `ingesting=${ingesting}`, `annexBPlanes=${planes}`, `rtsp=${rtsp}`, `webrtc=${input.webrtcSessions}`, `encoded=${kindList(consumers)}`, `audioSubs=${audio}`, `pipeClients=${pipe}`, `egress=${input.egressTranscodes}`, `children=${input.childProcesses}`, `opened=${churn.opened}`, `closed=${churn.closed}`, `openedKinds=${kindList(churn.openedByKind)}`, `closedKinds=${kindList(churn.closedByKind)}`, `preBufferMb=${mb(pre)}`, `clipMb=${mb(clip)}`, `rtpRingMb=${mb(rtp)}`, `keyframeMb=${mb(kf)}`, `writeQueueSumMb=${mb(qSum)}`, `writeQueueMaxMb=${mb(qMax)}`, `copiedPackets=${copied.packets}`, `copiedMb=${mb(copied.bytes)}`, `topRingMb=${top === "" ? "none" : top}`);
3272
+ return parts.join(" ");
3273
+ }
3274
+ /** Count entries named `name` in `process.getActiveResourcesInfo()`. */
3275
+ function countActiveResources(resources, name) {
3276
+ let n = 0;
3277
+ for (const r of resources) if (r === name) n += 1;
3278
+ return n;
3279
+ }
3280
+ //#endregion
3088
3281
  //#region src/stream-broker/stream-broker/owned-buffer.ts
3089
3282
  /**
3090
3283
  * Buffers that enter a LONG-LIVED ring own their backing store.
@@ -3144,6 +3337,7 @@ function detachFromPool(buf) {
3144
3337
  function copyOwned(src, start, end) {
3145
3338
  const owned = Buffer.allocUnsafeSlow(end - start);
3146
3339
  src.copy(owned, 0, start, end);
3340
+ recordCopyOwned(end - start);
3147
3341
  return owned;
3148
3342
  }
3149
3343
  //#endregion
@@ -3756,6 +3950,26 @@ var RtspRestreamer = class {
3756
3950
  for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
3757
3951
  return n;
3758
3952
  }
3953
+ /**
3954
+ * What this restreamer holds, for the `[broker-mem]` probe: live sessions,
3955
+ * their socket write queues (sum and max) and the keyframe caches. One pass
3956
+ * over the sessions, no allocation beyond the result.
3957
+ */
3958
+ getMemProbe() {
3959
+ let sum = 0;
3960
+ let max = 0;
3961
+ for (const s of this.sessions.values()) {
3962
+ const q = s.getWriteQueueBytes();
3963
+ sum += q;
3964
+ if (q > max) max = q;
3965
+ }
3966
+ return {
3967
+ sessions: this.sessions.size,
3968
+ writeQueueSumBytes: sum,
3969
+ writeQueueMaxBytes: max,
3970
+ keyframeCacheBytes: this.lastKeyframeNalBytes + this.lastKeyframeRtpBytes
3971
+ };
3972
+ }
3759
3973
  /** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
3760
3974
  listSessionInfos() {
3761
3975
  return [...this.sessions.values()].map((s) => s.getInfo());
@@ -3767,6 +3981,7 @@ var RtspRestreamer = class {
3767
3981
  streamName: this.streamName,
3768
3982
  total: this.sessions.size
3769
3983
  } });
3984
+ reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
3770
3985
  this._onSessionCountChanged?.();
3771
3986
  this.reconcileSentryAudio();
3772
3987
  if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
@@ -4231,6 +4446,7 @@ var RtspRestreamer = class {
4231
4446
  streamName: this.streamName,
4232
4447
  total: this.sessions.size
4233
4448
  } });
4449
+ reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
4234
4450
  this._onSessionCountChanged?.();
4235
4451
  this.reconcileSentryAudio();
4236
4452
  }
@@ -4500,6 +4716,10 @@ var RtspSession = class {
4500
4716
  this.onClose?.();
4501
4717
  });
4502
4718
  }
4719
+ /** Bytes the socket has accepted but not yet flushed (corked bytes included). */
4720
+ getWriteQueueBytes() {
4721
+ return this.socket.writableLength;
4722
+ }
4503
4723
  getSessionId() {
4504
4724
  return this.sessionId;
4505
4725
  }
@@ -6453,6 +6673,10 @@ var EgressTranscodeManager = class {
6453
6673
  /** In-flight starts, keyed by sharing key — concurrent acquires share one. */
6454
6674
  pending = /* @__PURE__ */ new Map();
6455
6675
  releaseGraceMs;
6676
+ /** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
6677
+ activeCount() {
6678
+ return this.entries.size;
6679
+ }
6456
6680
  constructor(opts) {
6457
6681
  this.opts = opts;
6458
6682
  this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
@@ -12816,6 +13040,8 @@ var StreamBroker = class StreamBroker {
12816
13040
  * timeout was chosen against.
12817
13041
  */
12818
13042
  static RTP_ACTIVITY_SWEEP_MS = 1e3;
13043
+ /** A source counts as ingesting when its last video packet is younger than this. */
13044
+ static INGESTING_WITHIN_MS = 1e4;
12819
13045
  manualStop = false;
12820
13046
  stopping = false;
12821
13047
  pipeServer;
@@ -13090,6 +13316,33 @@ var StreamBroker = class StreamBroker {
13090
13316
  return this.lastPacketAt;
13091
13317
  }
13092
13318
  /**
13319
+ * What this broker holds, for the `[broker-mem]` probe (`broker-mem-probe.ts`).
13320
+ * Ring sizes are the rings' own counters (backing-store bytes), so this is
13321
+ * O(sessions + subscribers) and allocates only the result.
13322
+ */
13323
+ getMemSample(now) {
13324
+ const encodedByKind = {};
13325
+ for (const entry of this.encodedCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
13326
+ for (const entry of this.rtpVideoCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
13327
+ const rtsp = this.rtspRestreamer.getMemProbe();
13328
+ return {
13329
+ deviceId: this.numericDeviceId,
13330
+ brokerId: this.deviceId,
13331
+ ingesting: this.lastPacketAt > 0 && now - this.lastPacketAt < StreamBroker.INGESTING_WITHIN_MS,
13332
+ preBufferBytes: this.preBuffer.getBufferedBytes(),
13333
+ clipBytes: this.clipBuffer?.getBufferedBytes() ?? 0,
13334
+ rtpRingBytes: this.rtpRingBytes,
13335
+ keyframeCacheBytes: rtsp.keyframeCacheBytes,
13336
+ rtspSessions: rtsp.sessions,
13337
+ writeQueueSumBytes: rtsp.writeQueueSumBytes,
13338
+ writeQueueMaxBytes: rtsp.writeQueueMaxBytes,
13339
+ encodedByKind,
13340
+ audioSubscribers: this.audioChunkPlane.subscriberCount,
13341
+ pipeClients: this.pipeServer.getClientCount(),
13342
+ annexBPlaneActive: this.annexBPlaneActive
13343
+ };
13344
+ }
13345
+ /**
13093
13346
  * Whether a LAZY publisher is being built right now.
13094
13347
  *
13095
13348
  * A consumer deciding whether it is worth waiting for this source needs to
@@ -14052,6 +14305,7 @@ var StreamBroker = class StreamBroker {
14052
14305
  packetsDelivered: 0
14053
14306
  };
14054
14307
  this.encodedCallbacks.set(callback, entry);
14308
+ reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14055
14309
  if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
14056
14310
  total: this.encodedCallbacks.size,
14057
14311
  subscriberId: entry.id,
@@ -14062,6 +14316,7 @@ var StreamBroker = class StreamBroker {
14062
14316
  this.checkDemand();
14063
14317
  return () => {
14064
14318
  this.encodedCallbacks.delete(callback);
14319
+ reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14065
14320
  if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
14066
14321
  total: this.encodedCallbacks.size,
14067
14322
  subscriberId: entry.id
@@ -14156,6 +14411,7 @@ var StreamBroker = class StreamBroker {
14156
14411
  packetsDelivered: 0
14157
14412
  };
14158
14413
  this.rtpVideoCallbacks.set(callback, entry);
14414
+ reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14159
14415
  if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
14160
14416
  total: this.rtpVideoCallbacks.size,
14161
14417
  subscriberId: entry.id,
@@ -14165,6 +14421,7 @@ var StreamBroker = class StreamBroker {
14165
14421
  this.checkDemand();
14166
14422
  return () => {
14167
14423
  this.rtpVideoCallbacks.delete(callback);
14424
+ reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14168
14425
  if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
14169
14426
  total: this.rtpVideoCallbacks.size,
14170
14427
  subscriberId: entry.id
@@ -14565,6 +14822,7 @@ var StreamBroker = class StreamBroker {
14565
14822
  tag,
14566
14823
  accept: options?.accept
14567
14824
  });
14825
+ reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
14568
14826
  this.checkDemand();
14569
14827
  return result;
14570
14828
  }
@@ -14575,7 +14833,10 @@ var StreamBroker = class StreamBroker {
14575
14833
  /** Release an audio-chunk subscription. Returns `true` when it existed. */
14576
14834
  unsubscribeAudioChunks(subscriptionId) {
14577
14835
  const released = this.audioChunkPlane.unsubscribe(subscriptionId);
14578
- if (released) this.checkDemand();
14836
+ if (released) {
14837
+ reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
14838
+ this.checkDemand();
14839
+ }
14579
14840
  return released;
14580
14841
  }
14581
14842
  getStats() {
@@ -19402,6 +19663,10 @@ var StreamBrokerManager = class StreamBrokerManager {
19402
19663
  return { killed: broker.killClient(input.channel, input.handle) };
19403
19664
  }
19404
19665
  /** Raw broker instances — internal use only. */
19666
+ /** Live egress transcodes (ffmpeg children) — the `[broker-mem]` probe. */
19667
+ getEgressTranscodeCount() {
19668
+ return this.egressTranscodeManager?.activeCount() ?? 0;
19669
+ }
19405
19670
  getBrokerInstances() {
19406
19671
  return [...this.brokers.values()];
19407
19672
  }
@@ -35274,6 +35539,7 @@ var BrokerWebrtcServer = class {
35274
35539
  }
35275
35540
  };
35276
35541
  this.sessions.set(sessionId, entry);
35542
+ reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
35277
35543
  session.onClosed = () => this.cleanupSession(sessionId);
35278
35544
  const locateRecorded = this.locateRecordedSegment;
35279
35545
  const readRecorded = this.readRecordedSegmentBytes;
@@ -36491,6 +36757,7 @@ var BrokerWebrtcServer = class {
36491
36757
  const entry = this.sessions.get(sessionId);
36492
36758
  if (!entry) return;
36493
36759
  this.sessions.delete(sessionId);
36760
+ reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
36494
36761
  entry.pendingSwitch?.cancel();
36495
36762
  entry.pendingSwitch = void 0;
36496
36763
  let brokerStillServed = false;
@@ -37212,6 +37479,45 @@ function dialAnswerFromError(err) {
37212
37479
  };
37213
37480
  }
37214
37481
  //#endregion
37482
+ //#region src/stream-broker/broker-mem-report.ts
37483
+ /** Linux only: elsewhere (or on a read error) the native view is simply absent. */
37484
+ async function readProc() {
37485
+ if (process.platform !== "linux") return {
37486
+ status: null,
37487
+ maps: null
37488
+ };
37489
+ const [status, maps] = await Promise.all([(0, node_fs_promises.readFile)("/proc/self/status", "utf8").then(parseProcStatus, () => null), (0, node_fs_promises.readFile)("/proc/self/maps", "utf8").then(parseProcMaps, () => null)]);
37490
+ return {
37491
+ status,
37492
+ maps
37493
+ };
37494
+ }
37495
+ async function emitOnce(deps) {
37496
+ const now = Date.now();
37497
+ const proc = await readProc();
37498
+ const resources = process.getActiveResourcesInfo();
37499
+ const line = formatBrokerMem({
37500
+ samples: deps.manager.getBrokerInstances().map((b) => b.getMemSample(now)),
37501
+ webrtcSessions: deps.webrtcSessions(),
37502
+ egressTranscodes: deps.manager.getEgressTranscodeCount(),
37503
+ childProcesses: countActiveResources(resources, "ProcessWrap"),
37504
+ proc,
37505
+ churn: consumerChurn.drain(),
37506
+ copied: readCopyOwnedTotals()
37507
+ });
37508
+ deps.logger.info(line);
37509
+ }
37510
+ /** Start the five-minute `[broker-mem]` heartbeat; returns its stop function. */
37511
+ function startBrokerMemReport(deps) {
37512
+ const timer = setInterval(() => {
37513
+ emitOnce(deps).catch((err) => {
37514
+ deps.logger.warn("broker-mem probe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
37515
+ });
37516
+ }, BROKER_MEM_INTERVAL_MS);
37517
+ timer.unref();
37518
+ return () => clearInterval(timer);
37519
+ }
37520
+ //#endregion
37215
37521
  //#region src/stream-broker/event-loop-stall-monitor.ts
37216
37522
  var DEFAULT_INTERVAL_MS = 100;
37217
37523
  var DEFAULT_THRESHOLD_MS = 250;
@@ -37386,6 +37692,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
37386
37692
  * leaks those sockets and wedges the next runner's werift ICE agent
37387
37693
  * (DTLS never connects until a full hub restart). */
37388
37694
  webrtcServer = null;
37695
+ /** Stops the five-minute `[broker-mem]` attribution heartbeat. */
37696
+ stopBrokerMemReport = null;
37389
37697
  /** Names this process's event-loop stalls — see onInitialize. */
37390
37698
  loopStallMonitor = null;
37391
37699
  /** The `log-channels` provider for this process. Stopped on shutdown so a
@@ -37743,6 +38051,11 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
37743
38051
  this.webrtcServer = webrtcServer;
37744
38052
  const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
37745
38053
  this.metricsSnapshotTimer = setInterval(() => this.emitBrokerMetricsSnapshot(), BROKER_METRICS_SNAPSHOT_INTERVAL_MS);
38054
+ this.stopBrokerMemReport = startBrokerMemReport({
38055
+ logger: this.ctx.logger,
38056
+ manager: this.brokerManager,
38057
+ webrtcSessions: () => webrtcServer.getSessionCount()
38058
+ });
37746
38059
  const widgetsProvider = { listWidgets: async () => [{
37747
38060
  tab: "device-tab",
37748
38061
  label: "Stream Brokers",
@@ -37847,6 +38160,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
37847
38160
  async onShutdown() {
37848
38161
  this.logChannels?.stop();
37849
38162
  this.logChannels = null;
38163
+ this.stopBrokerMemReport?.();
38164
+ this.stopBrokerMemReport = null;
37850
38165
  this.loopStallMonitor?.stop();
37851
38166
  this.loopStallMonitor = null;
37852
38167
  this.flushProbeSnapshots(Date.now(), true);
@@ -3081,6 +3081,199 @@ var RtpPacketizer = class {
3081
3081
  }
3082
3082
  };
3083
3083
  //#endregion
3084
+ //#region src/stream-broker/stream-broker/broker-mem-probe.ts
3085
+ /** Cadence of the `[broker-mem]` line — aligned with the runner's `[mem]`. */
3086
+ var BROKER_MEM_INTERVAL_MS = 5 * 6e4;
3087
+ var copiedPackets = 0;
3088
+ var copiedBytes = 0;
3089
+ /** O(1) per packet: called by `copyOwned`. No allocation. */
3090
+ function recordCopyOwned(bytes) {
3091
+ copiedPackets += 1;
3092
+ copiedBytes += bytes;
3093
+ }
3094
+ function readCopyOwnedTotals() {
3095
+ return {
3096
+ packets: copiedPackets,
3097
+ bytes: copiedBytes
3098
+ };
3099
+ }
3100
+ /** `RssAnon:` / `RssFile:` out of `/proc/self/status` text; null when absent. */
3101
+ function parseProcStatus(text) {
3102
+ const anon = /^RssAnon:\s+(\d+)\s*kB/m.exec(text);
3103
+ const file = /^RssFile:\s+(\d+)\s*kB/m.exec(text);
3104
+ if (anon === null || file === null) return null;
3105
+ return {
3106
+ rssAnonKb: Number(anon[1]),
3107
+ rssFileKb: Number(file[1])
3108
+ };
3109
+ }
3110
+ /**
3111
+ * Summarise `/proc/self/maps`. Cheaper than `smaps_rollup`'s kernel page walk:
3112
+ * the file is one line per mapping and needs no per-page accounting. The
3113
+ * `[heap]` figure is the brk extent (virtual), which is the number glibc's
3114
+ * main arena grows when fragmented.
3115
+ *
3116
+ * Line shape: `start-end perms offset dev inode [path]` — an anonymous mapping
3117
+ * has no sixth field.
3118
+ */
3119
+ function parseProcMaps(text) {
3120
+ let heapBytes = 0;
3121
+ let anonMappings = 0;
3122
+ let anonBytes = 0;
3123
+ let from = 0;
3124
+ while (from < text.length) {
3125
+ let to = text.indexOf("\n", from);
3126
+ if (to === -1) to = text.length;
3127
+ const line = text.slice(from, to);
3128
+ from = to + 1;
3129
+ const dash = line.indexOf("-");
3130
+ const space = line.indexOf(" ");
3131
+ if (dash <= 0 || space <= dash) continue;
3132
+ const size = Number.parseInt(line.slice(dash + 1, space), 16) - Number.parseInt(line.slice(0, dash), 16);
3133
+ if (!Number.isFinite(size)) continue;
3134
+ const path = line.split(/\s+/, 6)[5];
3135
+ if (path === void 0 || path === "") {
3136
+ anonMappings += 1;
3137
+ anonBytes += size;
3138
+ } else if (path === "[heap]") heapBytes += size;
3139
+ }
3140
+ return {
3141
+ heapBytes,
3142
+ anonMappings,
3143
+ anonBytes
3144
+ };
3145
+ }
3146
+ /** Opened/closed consumers since the last reading; `drain()` resets. */
3147
+ var ConsumerChurn = class {
3148
+ opened = 0;
3149
+ closed = 0;
3150
+ openedByKind = /* @__PURE__ */ new Map();
3151
+ closedByKind = /* @__PURE__ */ new Map();
3152
+ recordOpen(kind) {
3153
+ this.opened += 1;
3154
+ this.openedByKind.set(kind, (this.openedByKind.get(kind) ?? 0) + 1);
3155
+ }
3156
+ recordClose(kind) {
3157
+ this.closed += 1;
3158
+ this.closedByKind.set(kind, (this.closedByKind.get(kind) ?? 0) + 1);
3159
+ }
3160
+ drain() {
3161
+ const reading = {
3162
+ opened: this.opened,
3163
+ closed: this.closed,
3164
+ openedByKind: Object.fromEntries(this.openedByKind),
3165
+ closedByKind: Object.fromEntries(this.closedByKind)
3166
+ };
3167
+ this.opened = 0;
3168
+ this.closed = 0;
3169
+ this.openedByKind = /* @__PURE__ */ new Map();
3170
+ this.closedByKind = /* @__PURE__ */ new Map();
3171
+ return reading;
3172
+ }
3173
+ };
3174
+ /** The one churn counter of this process (the broker runner). */
3175
+ var consumerChurn = new ConsumerChurn();
3176
+ /**
3177
+ * Sliding-window limiter for the attach/detach lines. `admit(now)` answers
3178
+ * whether this line may be written; the number of lines swallowed since the
3179
+ * last admitted one is returned with it so the next line can say so (a branch
3180
+ * that drops work names it).
3181
+ */
3182
+ function createLineLimiter(maxPerWindow, windowMs) {
3183
+ let windowStart = 0;
3184
+ let inWindow = 0;
3185
+ let suppressed = 0;
3186
+ return (now) => {
3187
+ if (now - windowStart >= windowMs) {
3188
+ windowStart = now;
3189
+ inWindow = 0;
3190
+ }
3191
+ if (inWindow >= maxPerWindow) {
3192
+ suppressed += 1;
3193
+ return {
3194
+ admit: false,
3195
+ suppressedBefore: 0
3196
+ };
3197
+ }
3198
+ inWindow += 1;
3199
+ const suppressedBefore = suppressed;
3200
+ suppressed = 0;
3201
+ return {
3202
+ admit: true,
3203
+ suppressedBefore
3204
+ };
3205
+ };
3206
+ }
3207
+ var consumerLineLimiter = createLineLimiter(40, 6e4);
3208
+ /**
3209
+ * One consumer attached to / detached from a broker: counted always (the
3210
+ * `[broker-mem]` line reports opened/closed since the last line), written at
3211
+ * info under a rate limit so a step in RSS can be matched to an event. The
3212
+ * line after a suppressed burst says how many were swallowed.
3213
+ */
3214
+ function reportConsumerEvent(logger, event, kind, deviceId, total, now = Date.now()) {
3215
+ if (event === "attach") consumerChurn.recordOpen(kind);
3216
+ else consumerChurn.recordClose(kind);
3217
+ if (logger === void 0 || logger === null) return;
3218
+ const verdict = consumerLineLimiter(now);
3219
+ if (!verdict.admit) return;
3220
+ const suppressed = verdict.suppressedBefore > 0 ? ` suppressedBefore=${verdict.suppressedBefore}` : "";
3221
+ logger.info(`consumer ${event} kind=${kind} total=${total}${suppressed}`, { ...deviceId !== void 0 ? { tags: { deviceId } } : {} });
3222
+ }
3223
+ var MB = 1024 * 1024;
3224
+ function ringBytesOf(s) {
3225
+ return s.preBufferBytes + s.clipBytes + s.rtpRingBytes + s.keyframeCacheBytes;
3226
+ }
3227
+ function mb(bytes) {
3228
+ return (bytes / MB).toFixed(1);
3229
+ }
3230
+ function kindList(counts) {
3231
+ const keys = Object.keys(counts).sort();
3232
+ return keys.length === 0 ? "none" : keys.map((k) => `${k}:${counts[k]}`).join(",");
3233
+ }
3234
+ /** One greppable `key=value` line; no per-device breakdown beyond the top N. */
3235
+ function formatBrokerMem(input) {
3236
+ const { samples, proc, churn, copied } = input;
3237
+ const consumers = {};
3238
+ let rtsp = 0;
3239
+ let audio = 0;
3240
+ let pipe = 0;
3241
+ let ingesting = 0;
3242
+ let planes = 0;
3243
+ let pre = 0;
3244
+ let clip = 0;
3245
+ let rtp = 0;
3246
+ let kf = 0;
3247
+ let qSum = 0;
3248
+ let qMax = 0;
3249
+ for (const s of samples) {
3250
+ if (s.ingesting) ingesting += 1;
3251
+ if (s.annexBPlaneActive) planes += 1;
3252
+ rtsp += s.rtspSessions;
3253
+ audio += s.audioSubscribers;
3254
+ pipe += s.pipeClients;
3255
+ pre += s.preBufferBytes;
3256
+ clip += s.clipBytes;
3257
+ rtp += s.rtpRingBytes;
3258
+ kf += s.keyframeCacheBytes;
3259
+ qSum += s.writeQueueSumBytes;
3260
+ if (s.writeQueueMaxBytes > qMax) qMax = s.writeQueueMaxBytes;
3261
+ for (const [kind, n] of Object.entries(s.encodedByKind)) consumers[kind] = (consumers[kind] ?? 0) + n;
3262
+ }
3263
+ const top = [...samples].filter((s) => s.deviceId !== void 0).sort((a, b) => ringBytesOf(b) - ringBytesOf(a)).slice(0, 5).map((s) => `${s.deviceId}:${mb(ringBytesOf(s))}`).join(",");
3264
+ const parts = ["[broker-mem]"];
3265
+ if (proc.status !== null) parts.push(`rssAnonMb=${(proc.status.rssAnonKb / 1024).toFixed(0)}`, `rssFileMb=${(proc.status.rssFileKb / 1024).toFixed(0)}`);
3266
+ if (proc.maps !== null) parts.push(`heapMapMb=${mb(proc.maps.heapBytes)}`, `anonMaps=${proc.maps.anonMappings}`, `anonMapsMb=${mb(proc.maps.anonBytes)}`);
3267
+ parts.push(`brokers=${samples.length}`, `ingesting=${ingesting}`, `annexBPlanes=${planes}`, `rtsp=${rtsp}`, `webrtc=${input.webrtcSessions}`, `encoded=${kindList(consumers)}`, `audioSubs=${audio}`, `pipeClients=${pipe}`, `egress=${input.egressTranscodes}`, `children=${input.childProcesses}`, `opened=${churn.opened}`, `closed=${churn.closed}`, `openedKinds=${kindList(churn.openedByKind)}`, `closedKinds=${kindList(churn.closedByKind)}`, `preBufferMb=${mb(pre)}`, `clipMb=${mb(clip)}`, `rtpRingMb=${mb(rtp)}`, `keyframeMb=${mb(kf)}`, `writeQueueSumMb=${mb(qSum)}`, `writeQueueMaxMb=${mb(qMax)}`, `copiedPackets=${copied.packets}`, `copiedMb=${mb(copied.bytes)}`, `topRingMb=${top === "" ? "none" : top}`);
3268
+ return parts.join(" ");
3269
+ }
3270
+ /** Count entries named `name` in `process.getActiveResourcesInfo()`. */
3271
+ function countActiveResources(resources, name) {
3272
+ let n = 0;
3273
+ for (const r of resources) if (r === name) n += 1;
3274
+ return n;
3275
+ }
3276
+ //#endregion
3084
3277
  //#region src/stream-broker/stream-broker/owned-buffer.ts
3085
3278
  /**
3086
3279
  * Buffers that enter a LONG-LIVED ring own their backing store.
@@ -3140,6 +3333,7 @@ function detachFromPool(buf) {
3140
3333
  function copyOwned(src, start, end) {
3141
3334
  const owned = Buffer.allocUnsafeSlow(end - start);
3142
3335
  src.copy(owned, 0, start, end);
3336
+ recordCopyOwned(end - start);
3143
3337
  return owned;
3144
3338
  }
3145
3339
  //#endregion
@@ -3752,6 +3946,26 @@ var RtspRestreamer = class {
3752
3946
  for (const session of this.sessions.values()) if (session.isPrestreamIntent()) n += 1;
3753
3947
  return n;
3754
3948
  }
3949
+ /**
3950
+ * What this restreamer holds, for the `[broker-mem]` probe: live sessions,
3951
+ * their socket write queues (sum and max) and the keyframe caches. One pass
3952
+ * over the sessions, no allocation beyond the result.
3953
+ */
3954
+ getMemProbe() {
3955
+ let sum = 0;
3956
+ let max = 0;
3957
+ for (const s of this.sessions.values()) {
3958
+ const q = s.getWriteQueueBytes();
3959
+ sum += q;
3960
+ if (q > max) max = q;
3961
+ }
3962
+ return {
3963
+ sessions: this.sessions.size,
3964
+ writeQueueSumBytes: sum,
3965
+ writeQueueMaxBytes: max,
3966
+ keyframeCacheBytes: this.lastKeyframeNalBytes + this.lastKeyframeRtpBytes
3967
+ };
3968
+ }
3755
3969
  /** Per-session diagnostic snapshots consumed by `StreamBroker.listClients`. */
3756
3970
  listSessionInfos() {
3757
3971
  return [...this.sessions.values()].map((s) => s.getInfo());
@@ -3763,6 +3977,7 @@ var RtspRestreamer = class {
3763
3977
  streamName: this.streamName,
3764
3978
  total: this.sessions.size
3765
3979
  } });
3980
+ reportConsumerEvent(this.logger, "attach", "rtsp", this.deviceId, this.sessions.size);
3766
3981
  this._onSessionCountChanged?.();
3767
3982
  this.reconcileSentryAudio();
3768
3983
  if (this._rtpPassthroughMode) setTimeout(() => this.servePendingSessionsRtp(), 0);
@@ -4227,6 +4442,7 @@ var RtspRestreamer = class {
4227
4442
  streamName: this.streamName,
4228
4443
  total: this.sessions.size
4229
4444
  } });
4445
+ reportConsumerEvent(this.logger, "detach", "rtsp", this.deviceId, this.sessions.size);
4230
4446
  this._onSessionCountChanged?.();
4231
4447
  this.reconcileSentryAudio();
4232
4448
  }
@@ -4496,6 +4712,10 @@ var RtspSession = class {
4496
4712
  this.onClose?.();
4497
4713
  });
4498
4714
  }
4715
+ /** Bytes the socket has accepted but not yet flushed (corked bytes included). */
4716
+ getWriteQueueBytes() {
4717
+ return this.socket.writableLength;
4718
+ }
4499
4719
  getSessionId() {
4500
4720
  return this.sessionId;
4501
4721
  }
@@ -6449,6 +6669,10 @@ var EgressTranscodeManager = class {
6449
6669
  /** In-flight starts, keyed by sharing key — concurrent acquires share one. */
6450
6670
  pending = /* @__PURE__ */ new Map();
6451
6671
  releaseGraceMs;
6672
+ /** Live egress transcodes (ffmpeg children), for the `[broker-mem]` probe. */
6673
+ activeCount() {
6674
+ return this.entries.size;
6675
+ }
6452
6676
  constructor(opts) {
6453
6677
  this.opts = opts;
6454
6678
  this.releaseGraceMs = opts.releaseGraceMs ?? DEFAULT_RELEASE_GRACE_MS;
@@ -12812,6 +13036,8 @@ var StreamBroker = class StreamBroker {
12812
13036
  * timeout was chosen against.
12813
13037
  */
12814
13038
  static RTP_ACTIVITY_SWEEP_MS = 1e3;
13039
+ /** A source counts as ingesting when its last video packet is younger than this. */
13040
+ static INGESTING_WITHIN_MS = 1e4;
12815
13041
  manualStop = false;
12816
13042
  stopping = false;
12817
13043
  pipeServer;
@@ -13086,6 +13312,33 @@ var StreamBroker = class StreamBroker {
13086
13312
  return this.lastPacketAt;
13087
13313
  }
13088
13314
  /**
13315
+ * What this broker holds, for the `[broker-mem]` probe (`broker-mem-probe.ts`).
13316
+ * Ring sizes are the rings' own counters (backing-store bytes), so this is
13317
+ * O(sessions + subscribers) and allocates only the result.
13318
+ */
13319
+ getMemSample(now) {
13320
+ const encodedByKind = {};
13321
+ for (const entry of this.encodedCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
13322
+ for (const entry of this.rtpVideoCallbacks.values()) encodedByKind[entry.attribution.kind] = (encodedByKind[entry.attribution.kind] ?? 0) + 1;
13323
+ const rtsp = this.rtspRestreamer.getMemProbe();
13324
+ return {
13325
+ deviceId: this.numericDeviceId,
13326
+ brokerId: this.deviceId,
13327
+ ingesting: this.lastPacketAt > 0 && now - this.lastPacketAt < StreamBroker.INGESTING_WITHIN_MS,
13328
+ preBufferBytes: this.preBuffer.getBufferedBytes(),
13329
+ clipBytes: this.clipBuffer?.getBufferedBytes() ?? 0,
13330
+ rtpRingBytes: this.rtpRingBytes,
13331
+ keyframeCacheBytes: rtsp.keyframeCacheBytes,
13332
+ rtspSessions: rtsp.sessions,
13333
+ writeQueueSumBytes: rtsp.writeQueueSumBytes,
13334
+ writeQueueMaxBytes: rtsp.writeQueueMaxBytes,
13335
+ encodedByKind,
13336
+ audioSubscribers: this.audioChunkPlane.subscriberCount,
13337
+ pipeClients: this.pipeServer.getClientCount(),
13338
+ annexBPlaneActive: this.annexBPlaneActive
13339
+ };
13340
+ }
13341
+ /**
13089
13342
  * Whether a LAZY publisher is being built right now.
13090
13343
  *
13091
13344
  * A consumer deciding whether it is worth waiting for this source needs to
@@ -14048,6 +14301,7 @@ var StreamBroker = class StreamBroker {
14048
14301
  packetsDelivered: 0
14049
14302
  };
14050
14303
  this.encodedCallbacks.set(callback, entry);
14304
+ reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14051
14305
  if (this.streamingDebug) this.logger?.info("encoded subscriber added", { meta: {
14052
14306
  total: this.encodedCallbacks.size,
14053
14307
  subscriberId: entry.id,
@@ -14058,6 +14312,7 @@ var StreamBroker = class StreamBroker {
14058
14312
  this.checkDemand();
14059
14313
  return () => {
14060
14314
  this.encodedCallbacks.delete(callback);
14315
+ reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14061
14316
  if (this.streamingDebug) this.logger?.info("encoded subscriber removed", { meta: {
14062
14317
  total: this.encodedCallbacks.size,
14063
14318
  subscriberId: entry.id
@@ -14152,6 +14407,7 @@ var StreamBroker = class StreamBroker {
14152
14407
  packetsDelivered: 0
14153
14408
  };
14154
14409
  this.rtpVideoCallbacks.set(callback, entry);
14410
+ reportConsumerEvent(this.logger, "attach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14155
14411
  if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber added", { meta: {
14156
14412
  total: this.rtpVideoCallbacks.size,
14157
14413
  subscriberId: entry.id,
@@ -14161,6 +14417,7 @@ var StreamBroker = class StreamBroker {
14161
14417
  this.checkDemand();
14162
14418
  return () => {
14163
14419
  this.rtpVideoCallbacks.delete(callback);
14420
+ reportConsumerEvent(this.logger, "detach", attribution.kind, this.numericDeviceId, this.encodedCallbacks.size + this.rtpVideoCallbacks.size);
14164
14421
  if (this.streamingDebug || CH_RTP.on) this.logger?.info("rtp video subscriber removed", { meta: {
14165
14422
  total: this.rtpVideoCallbacks.size,
14166
14423
  subscriberId: entry.id
@@ -14561,6 +14818,7 @@ var StreamBroker = class StreamBroker {
14561
14818
  tag,
14562
14819
  accept: options?.accept
14563
14820
  });
14821
+ reportConsumerEvent(this.logger, "attach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
14564
14822
  this.checkDemand();
14565
14823
  return result;
14566
14824
  }
@@ -14571,7 +14829,10 @@ var StreamBroker = class StreamBroker {
14571
14829
  /** Release an audio-chunk subscription. Returns `true` when it existed. */
14572
14830
  unsubscribeAudioChunks(subscriptionId) {
14573
14831
  const released = this.audioChunkPlane.unsubscribe(subscriptionId);
14574
- if (released) this.checkDemand();
14832
+ if (released) {
14833
+ reportConsumerEvent(this.logger, "detach", "audio-chunks", this.numericDeviceId, this.audioChunkPlane.subscriberCount);
14834
+ this.checkDemand();
14835
+ }
14575
14836
  return released;
14576
14837
  }
14577
14838
  getStats() {
@@ -19398,6 +19659,10 @@ var StreamBrokerManager = class StreamBrokerManager {
19398
19659
  return { killed: broker.killClient(input.channel, input.handle) };
19399
19660
  }
19400
19661
  /** Raw broker instances — internal use only. */
19662
+ /** Live egress transcodes (ffmpeg children) — the `[broker-mem]` probe. */
19663
+ getEgressTranscodeCount() {
19664
+ return this.egressTranscodeManager?.activeCount() ?? 0;
19665
+ }
19401
19666
  getBrokerInstances() {
19402
19667
  return [...this.brokers.values()];
19403
19668
  }
@@ -35270,6 +35535,7 @@ var BrokerWebrtcServer = class {
35270
35535
  }
35271
35536
  };
35272
35537
  this.sessions.set(sessionId, entry);
35538
+ reportConsumerEvent(this.logger, "attach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
35273
35539
  session.onClosed = () => this.cleanupSession(sessionId);
35274
35540
  const locateRecorded = this.locateRecordedSegment;
35275
35541
  const readRecorded = this.readRecordedSegmentBytes;
@@ -36487,6 +36753,7 @@ var BrokerWebrtcServer = class {
36487
36753
  const entry = this.sessions.get(sessionId);
36488
36754
  if (!entry) return;
36489
36755
  this.sessions.delete(sessionId);
36756
+ reportConsumerEvent(this.logger, "detach", `webrtc:${entry.consumerKind}`, entry.rebind?.deviceId, this.sessions.size);
36490
36757
  entry.pendingSwitch?.cancel();
36491
36758
  entry.pendingSwitch = void 0;
36492
36759
  let brokerStillServed = false;
@@ -37208,6 +37475,45 @@ function dialAnswerFromError(err) {
37208
37475
  };
37209
37476
  }
37210
37477
  //#endregion
37478
+ //#region src/stream-broker/broker-mem-report.ts
37479
+ /** Linux only: elsewhere (or on a read error) the native view is simply absent. */
37480
+ async function readProc() {
37481
+ if (process.platform !== "linux") return {
37482
+ status: null,
37483
+ maps: null
37484
+ };
37485
+ const [status, maps] = await Promise.all([readFile("/proc/self/status", "utf8").then(parseProcStatus, () => null), readFile("/proc/self/maps", "utf8").then(parseProcMaps, () => null)]);
37486
+ return {
37487
+ status,
37488
+ maps
37489
+ };
37490
+ }
37491
+ async function emitOnce(deps) {
37492
+ const now = Date.now();
37493
+ const proc = await readProc();
37494
+ const resources = process.getActiveResourcesInfo();
37495
+ const line = formatBrokerMem({
37496
+ samples: deps.manager.getBrokerInstances().map((b) => b.getMemSample(now)),
37497
+ webrtcSessions: deps.webrtcSessions(),
37498
+ egressTranscodes: deps.manager.getEgressTranscodeCount(),
37499
+ childProcesses: countActiveResources(resources, "ProcessWrap"),
37500
+ proc,
37501
+ churn: consumerChurn.drain(),
37502
+ copied: readCopyOwnedTotals()
37503
+ });
37504
+ deps.logger.info(line);
37505
+ }
37506
+ /** Start the five-minute `[broker-mem]` heartbeat; returns its stop function. */
37507
+ function startBrokerMemReport(deps) {
37508
+ const timer = setInterval(() => {
37509
+ emitOnce(deps).catch((err) => {
37510
+ deps.logger.warn("broker-mem probe failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
37511
+ });
37512
+ }, BROKER_MEM_INTERVAL_MS);
37513
+ timer.unref();
37514
+ return () => clearInterval(timer);
37515
+ }
37516
+ //#endregion
37211
37517
  //#region src/stream-broker/event-loop-stall-monitor.ts
37212
37518
  var DEFAULT_INTERVAL_MS = 100;
37213
37519
  var DEFAULT_THRESHOLD_MS = 250;
@@ -37382,6 +37688,8 @@ var StreamBrokerAddon = class extends BaseAddon {
37382
37688
  * leaks those sockets and wedges the next runner's werift ICE agent
37383
37689
  * (DTLS never connects until a full hub restart). */
37384
37690
  webrtcServer = null;
37691
+ /** Stops the five-minute `[broker-mem]` attribution heartbeat. */
37692
+ stopBrokerMemReport = null;
37385
37693
  /** Names this process's event-loop stalls — see onInitialize. */
37386
37694
  loopStallMonitor = null;
37387
37695
  /** The `log-channels` provider for this process. Stopped on shutdown so a
@@ -37739,6 +38047,11 @@ var StreamBrokerAddon = class extends BaseAddon {
37739
38047
  this.webrtcServer = webrtcServer;
37740
38048
  const webrtcSessionProvider = new WebrtcSessionProvider(webrtcServer, this.brokerManager);
37741
38049
  this.metricsSnapshotTimer = setInterval(() => this.emitBrokerMetricsSnapshot(), BROKER_METRICS_SNAPSHOT_INTERVAL_MS);
38050
+ this.stopBrokerMemReport = startBrokerMemReport({
38051
+ logger: this.ctx.logger,
38052
+ manager: this.brokerManager,
38053
+ webrtcSessions: () => webrtcServer.getSessionCount()
38054
+ });
37742
38055
  const widgetsProvider = { listWidgets: async () => [{
37743
38056
  tab: "device-tab",
37744
38057
  label: "Stream Brokers",
@@ -37843,6 +38156,8 @@ var StreamBrokerAddon = class extends BaseAddon {
37843
38156
  async onShutdown() {
37844
38157
  this.logChannels?.stop();
37845
38158
  this.logChannels = null;
38159
+ this.stopBrokerMemReport?.();
38160
+ this.stopBrokerMemReport = null;
37846
38161
  this.loopStallMonitor?.stop();
37847
38162
  this.loopStallMonitor = null;
37848
38163
  this.flushProbeSnapshots(Date.now(), true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.365",
3
+ "version": "1.2.366",
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",