@streamr/dht 103.6.0-rc.0 → 103.8.0-rc.3

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.
@@ -261,6 +261,68 @@ class SendFailed extends Err {
261
261
  constructor(message, originalError) { super(ErrorCode.SEND_FAILED, message, originalError); }
262
262
  }
263
263
 
264
+ let enabled = true;
265
+ function setGapDiagnosticsEnabled(val) {
266
+ enabled = val;
267
+ globalThis.__dhtGapDiagEnabled = val;
268
+ }
269
+ function isGapDiagnosticsEnabled() {
270
+ return enabled;
271
+ }
272
+ const SUMMARY_INTERVAL_MS = 2000;
273
+ const accumulators = new Map();
274
+ function logGapDiagnosticSampled(layer, opts = {}) {
275
+ if (!enabled && !globalThis.__dhtGapDiagEnabled)
276
+ return;
277
+ const now = performance.now();
278
+ const threshold = opts.outlierThresholdMs ?? 30;
279
+ let acc = accumulators.get(layer);
280
+ if (acc === undefined) {
281
+ acc = { count: 0, sumDeltaMs: 0, maxDeltaMs: 0, outlierCount: 0, lastReportMs: now, lastEventMs: now };
282
+ accumulators.set(layer, acc);
283
+ return;
284
+ }
285
+ const deltaMs = now - acc.lastEventMs;
286
+ acc.lastEventMs = now;
287
+ acc.count++;
288
+ if (deltaMs > acc.maxDeltaMs)
289
+ acc.maxDeltaMs = deltaMs;
290
+ acc.sumDeltaMs += deltaMs;
291
+ if (deltaMs > threshold)
292
+ acc.outlierCount++;
293
+ if (deltaMs > threshold) {
294
+ const payload = {
295
+ layer,
296
+ timestampMs: now,
297
+ deltaMs: +deltaMs.toFixed(2),
298
+ detail: opts.detail,
299
+ };
300
+ // eslint-disable-next-line no-console
301
+ console.log('[gap-diagnostics]', JSON.stringify(payload));
302
+ }
303
+ if (now - acc.lastReportMs >= SUMMARY_INTERVAL_MS) {
304
+ const summaryDetail = {
305
+ count: acc.count,
306
+ meanDeltaMs: acc.count > 0 ? +(acc.sumDeltaMs / acc.count).toFixed(2) : 0,
307
+ maxDeltaMs: +acc.maxDeltaMs.toFixed(2),
308
+ outlierCount: acc.outlierCount,
309
+ periodMs: +(now - acc.lastReportMs).toFixed(1),
310
+ };
311
+ const summary = {
312
+ layer: `${layer}.summary`,
313
+ timestampMs: now,
314
+ detail: summaryDetail,
315
+ };
316
+ // eslint-disable-next-line no-console
317
+ console.log('[gap-diagnostics]', JSON.stringify(summary));
318
+ acc.count = 0;
319
+ acc.sumDeltaMs = 0;
320
+ acc.maxDeltaMs = 0;
321
+ acc.outlierCount = 0;
322
+ acc.lastReportMs = now;
323
+ }
324
+ }
325
+
264
326
  // @generated message type with reflection information, may provide speed optimized methods
265
327
  class Empty$Type extends runtime.MessageType {
266
328
  constructor() {
@@ -2305,6 +2367,7 @@ class ConnectionManager extends eventemitter3.EventEmitter {
2305
2367
  if ((this.state === ConnectionManagerState.STOPPED || this.state === ConnectionManagerState.STOPPING) && !opts.sendIfStopped) {
2306
2368
  return;
2307
2369
  }
2370
+ logGapDiagnosticSampled('dht.connMgr.send');
2308
2371
  const peerDescriptor = message.targetDescriptor;
2309
2372
  if (this.isConnectionToSelf(peerDescriptor)) {
2310
2373
  throw new CannotConnectToSelf('Cannot send to self');
@@ -2380,6 +2443,7 @@ class ConnectionManager extends eventemitter3.EventEmitter {
2380
2443
  this.rpcCommunicator?.handleMessageFromPeer(message);
2381
2444
  }
2382
2445
  else {
2446
+ logGapDiagnosticSampled('dht.connMgr.emitMessage');
2383
2447
  logger$A.trace('emit "message" ' + toNodeId(message.sourceDescriptor)
2384
2448
  + ' ' + message.serviceId + ' ' + message.messageId);
2385
2449
  this.emit('message', message);
@@ -2389,6 +2453,7 @@ class ConnectionManager extends eventemitter3.EventEmitter {
2389
2453
  if (this.state === ConnectionManagerState.STOPPED) {
2390
2454
  return;
2391
2455
  }
2456
+ logGapDiagnosticSampled('dht.connMgr.onData');
2392
2457
  this.metrics.receiveBytesPerSecond.record(data.byteLength);
2393
2458
  this.metrics.receiveMessagesPerSecond.record(1);
2394
2459
  let message;
@@ -2822,7 +2887,7 @@ const parseVersion = (version) => {
2822
2887
  }
2823
2888
  };
2824
2889
 
2825
- var version = "103.6.0-rc.0";
2890
+ var version = "103.8.0-rc.3";
2826
2891
 
2827
2892
  const logger$y = new utils.Logger('Handshaker');
2828
2893
  // Optimally the Outgoing and Incoming Handshakers could be their own separate classes
@@ -3280,6 +3345,9 @@ class DirectWebrtcConnection extends eventemitter3.EventEmitter {
3280
3345
  }
3281
3346
  send(data) {
3282
3347
  if (this.lastState === 'connected') {
3348
+ logGapDiagnosticSampled('dht.dc.send', {
3349
+ detail: { bufferedAmount: this.dataChannel.bufferedAmount, queueLen: this.messageQueue.length }
3350
+ });
3283
3351
  if (this.dataChannel.bufferedAmount > this.bufferThresholdHigh) {
3284
3352
  this.messageQueue.push(data);
3285
3353
  }
@@ -3308,6 +3376,7 @@ class DirectWebrtcConnection extends eventemitter3.EventEmitter {
3308
3376
  };
3309
3377
  dataChannel.onmessage = (msg) => {
3310
3378
  logger$v.trace('dc.onmessage');
3379
+ logGapDiagnosticSampled('dht.dc.onmessage');
3311
3380
  this.emit('data', new Uint8Array(msg.data));
3312
3381
  };
3313
3382
  dataChannel.onbufferedamountlow = () => {
@@ -3520,6 +3589,69 @@ function installWebrtcBridge(worker) {
3520
3589
  * onbufferedamountlow) fire in the worker's event loop. The main thread
3521
3590
  * is never involved in the data path.
3522
3591
  */
3592
+ // ── agent log: layer0-vs-layer2 contention probe ────────────────────
3593
+ // Every DHT connection's datachannel `onmessage` runs in THIS one worker, and
3594
+ // `emit('data')` synchronously drives the downstream routing / RPC dispatch.
3595
+ // So timing each emit captures the per-message synchronous processing cost —
3596
+ // and aggregating across ALL connections tells us how much of the worker's
3597
+ // event loop is consumed servicing GLOBAL-DHT layer0 traffic (routing for the
3598
+ // hundreds of nodes we forward for) vs our handful of media messages. If a
3599
+ // media arrival gap coincides with a window of high non-media processing, that
3600
+ // is layer0 starving layer2. Emitted in the same `[gap-diagnostics]` line
3601
+ // shape the analyzer already parses.
3602
+ let dcWinStart = performance.now();
3603
+ let dcWinCount = 0;
3604
+ let dcWinBusyMs = 0;
3605
+ let dcWinMaxMs = 0;
3606
+ function recordDcProcessing(procMs) {
3607
+ if (!isGapDiagnosticsEnabled() && !globalThis.__dhtGapDiagEnabled) {
3608
+ return;
3609
+ }
3610
+ dcWinCount++;
3611
+ dcWinBusyMs += procMs;
3612
+ if (procMs > dcWinMaxMs)
3613
+ dcWinMaxMs = procMs;
3614
+ // Individual event for a single message whose inline processing blocked the
3615
+ // loop unusually long (one expensive routing/RPC dispatch).
3616
+ if (procMs > 15) {
3617
+ try {
3618
+ console.log('[gap-diagnostics]', JSON.stringify({
3619
+ layer: 'dht.dc.processing.slow',
3620
+ timestampMs: performance.now(),
3621
+ deltaMs: +procMs.toFixed(2),
3622
+ }));
3623
+ }
3624
+ catch {
3625
+ /* ignore */
3626
+ }
3627
+ }
3628
+ const now = performance.now();
3629
+ const winElapsed = now - dcWinStart;
3630
+ if (winElapsed >= 1000) {
3631
+ try {
3632
+ console.log('[gap-diagnostics]', JSON.stringify({
3633
+ layer: 'dht.dc.processing.window',
3634
+ timestampMs: now,
3635
+ detail: {
3636
+ windowMs: +winElapsed.toFixed(0),
3637
+ count: dcWinCount,
3638
+ msgPerSec: +((dcWinCount / winElapsed) * 1000).toFixed(0),
3639
+ busyMs: +dcWinBusyMs.toFixed(1),
3640
+ occupancyPct: +((dcWinBusyMs / winElapsed) *
3641
+ 100).toFixed(1),
3642
+ maxMs: +dcWinMaxMs.toFixed(1),
3643
+ },
3644
+ }));
3645
+ }
3646
+ catch {
3647
+ /* ignore */
3648
+ }
3649
+ dcWinStart = now;
3650
+ dcWinCount = 0;
3651
+ dcWinBusyMs = 0;
3652
+ dcWinMaxMs = 0;
3653
+ }
3654
+ }
3523
3655
  // ── Module-level bridge client (initialized once per worker) ────────
3524
3656
  let resolveBridgeProxy;
3525
3657
  const bridgeProxyPromise = new Promise((resolve) => {
@@ -3562,6 +3694,19 @@ class WorkerWebrtcConnection extends eventemitter3.EventEmitter {
3562
3694
  earlyTimeout;
3563
3695
  messageQueue = [];
3564
3696
  startPromise;
3697
+ // agent log: PER-CONNECTION datachannel receive cadence. The global
3698
+ // `dht.dc.onmessage` accumulator can't isolate one stream; this tracks
3699
+ // inter-message arrival on THIS connection so we can pick the connection
3700
+ // carrying the composite media (highest count/bytes) and compare its
3701
+ // datachannel-level gaps against messageArrival (post-routing) and
3702
+ // videoFrameArrival (post-decrypt) — i.e. localize WHERE the gap is born.
3703
+ lastRecvMs;
3704
+ recvWinStart = 0;
3705
+ recvCount = 0;
3706
+ recvSumDelta = 0;
3707
+ recvMaxDelta = 0;
3708
+ recvBytes = 0;
3709
+ recvMaxBytes = 0;
3565
3710
  constructor(params) {
3566
3711
  super();
3567
3712
  this.connectionId = createRandomConnectionId();
@@ -3602,7 +3747,7 @@ class WorkerWebrtcConnection extends eventemitter3.EventEmitter {
3602
3747
  if (state === DisconnectedState.CLOSED ||
3603
3748
  state === DisconnectedState.DISCONNECTED ||
3604
3749
  state === DisconnectedState.FAILED) {
3605
- this.doClose(false);
3750
+ this.doClose(false, `pcState=${state}`);
3606
3751
  }
3607
3752
  },
3608
3753
  onDataChannel: (channel) => {
@@ -3655,6 +3800,9 @@ class WorkerWebrtcConnection extends eventemitter3.EventEmitter {
3655
3800
  }
3656
3801
  send(data) {
3657
3802
  if (this.connected && this.dataChannel) {
3803
+ logGapDiagnosticSampled('dht.dc.send', {
3804
+ detail: { bufferedAmount: this.dataChannel.bufferedAmount, queueLen: this.messageQueue.length }
3805
+ });
3658
3806
  if (this.dataChannel.bufferedAmount > this.bufferThresholdHigh) {
3659
3807
  this.messageQueue.push(data);
3660
3808
  }
@@ -3674,6 +3822,62 @@ class WorkerWebrtcConnection extends eventemitter3.EventEmitter {
3674
3822
  }
3675
3823
  }
3676
3824
  // ── DataChannel handling (runs entirely in the worker) ──────
3825
+ recordRecv(bytes) {
3826
+ if (!isGapDiagnosticsEnabled() && !globalThis.__dhtGapDiagEnabled) {
3827
+ return;
3828
+ }
3829
+ const now = performance.now();
3830
+ const conn = String(this.connectionId).slice(0, 8);
3831
+ if (this.lastRecvMs !== undefined) {
3832
+ const delta = now - this.lastRecvMs;
3833
+ this.recvSumDelta += delta;
3834
+ if (delta > this.recvMaxDelta)
3835
+ this.recvMaxDelta = delta;
3836
+ if (delta > 60) {
3837
+ try {
3838
+ console.log('[gap-diagnostics]', JSON.stringify({
3839
+ layer: 'dht.dc.recvGap',
3840
+ timestampMs: now,
3841
+ deltaMs: +delta.toFixed(1),
3842
+ detail: { conn, bytes },
3843
+ }));
3844
+ }
3845
+ catch (_e) { /* ignore */ }
3846
+ }
3847
+ }
3848
+ this.lastRecvMs = now;
3849
+ this.recvCount++;
3850
+ this.recvBytes += bytes;
3851
+ if (bytes > this.recvMaxBytes)
3852
+ this.recvMaxBytes = bytes;
3853
+ if (this.recvWinStart === 0)
3854
+ this.recvWinStart = now;
3855
+ const elapsed = now - this.recvWinStart;
3856
+ if (elapsed >= 1000) {
3857
+ try {
3858
+ console.log('[gap-diagnostics]', JSON.stringify({
3859
+ layer: 'dht.dc.recv',
3860
+ timestampMs: now,
3861
+ detail: {
3862
+ conn,
3863
+ count: this.recvCount,
3864
+ perSec: Math.round((this.recvCount / elapsed) * 1000),
3865
+ meanMs: +(this.recvSumDelta / Math.max(1, this.recvCount)).toFixed(1),
3866
+ maxMs: +this.recvMaxDelta.toFixed(1),
3867
+ bytesPerSec: Math.round((this.recvBytes / elapsed) * 1000),
3868
+ maxBytes: this.recvMaxBytes,
3869
+ },
3870
+ }));
3871
+ }
3872
+ catch (_e) { /* ignore */ }
3873
+ this.recvWinStart = now;
3874
+ this.recvCount = 0;
3875
+ this.recvSumDelta = 0;
3876
+ this.recvMaxDelta = 0;
3877
+ this.recvBytes = 0;
3878
+ this.recvMaxBytes = 0;
3879
+ }
3880
+ }
3677
3881
  setupDataChannel(dataChannel) {
3678
3882
  this.dataChannel = dataChannel;
3679
3883
  this.dataChannel.binaryType = 'arraybuffer';
@@ -3684,14 +3888,18 @@ class WorkerWebrtcConnection extends eventemitter3.EventEmitter {
3684
3888
  };
3685
3889
  dataChannel.onclose = () => {
3686
3890
  logger$u.trace('dc.onClosed (worker)');
3687
- this.doClose(false);
3891
+ this.doClose(false, 'dataChannel.onclose');
3688
3892
  };
3689
3893
  dataChannel.onerror = (err) => {
3690
3894
  logger$u.warn('Data channel error (worker)', { err });
3691
3895
  };
3692
3896
  dataChannel.onmessage = (msg) => {
3693
3897
  logger$u.trace('dc.onmessage (worker)');
3898
+ logGapDiagnosticSampled('dht.dc.onmessage');
3899
+ this.recordRecv(msg.data instanceof ArrayBuffer ? msg.data.byteLength : 0);
3900
+ const t0 = performance.now();
3694
3901
  this.emit('data', new Uint8Array(msg.data));
3902
+ recordDcProcessing(performance.now() - t0);
3695
3903
  };
3696
3904
  dataChannel.onbufferedamountlow = () => {
3697
3905
  logger$u.trace('dc.onBufferedAmountLow (worker)');
@@ -5960,7 +6168,7 @@ class PeerDiscovery {
5960
6168
  logger$d.debug(`Ring join on ${this.options.serviceId} timed out`);
5961
6169
  }
5962
6170
  finally {
5963
- sessions.forEach((session) => this.ongoingDiscoverySessions.delete(session.id));
6171
+ sessions.forEach((session) => this.ongoingRingDiscoverySessions.delete(session.id));
5964
6172
  }
5965
6173
  }
5966
6174
  async rejoinDht(entryPoint, contactedPeers = new Set(), distantJoinContactPeers = new Set()) {
@@ -8078,7 +8286,9 @@ exports.createOutgoingHandshaker = createOutgoingHandshaker;
8078
8286
  exports.getRandomRegion = getRandomRegion;
8079
8287
  exports.getRegionDelayMatrix = getRegionDelayMatrix;
8080
8288
  exports.installWebrtcBridge = installWebrtcBridge;
8289
+ exports.logGapDiagnosticSampled = logGapDiagnosticSampled;
8081
8290
  exports.randomDhtAddress = randomDhtAddress;
8291
+ exports.setGapDiagnosticsEnabled = setGapDiagnosticsEnabled;
8082
8292
  exports.toDhtAddress = toDhtAddress;
8083
8293
  exports.toDhtAddressRaw = toDhtAddressRaw;
8084
8294
  exports.toNodeId = toNodeId;