@peerbit/pubsub 5.3.12 → 5.4.0

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 (50) hide show
  1. package/dist/src/index.d.ts +31 -13
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +622 -145
  4. package/dist/src/index.js.map +1 -1
  5. package/package.json +8 -7
  6. package/src/index.ts +914 -216
  7. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +0 -3
  8. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +0 -1
  9. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +0 -90
  10. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +0 -1
  11. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +0 -2
  12. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +0 -1
  13. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +0 -712
  14. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +0 -1
  15. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +0 -2
  16. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +0 -1
  17. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +0 -1591
  18. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +0 -1
  19. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +0 -2
  20. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +0 -1
  21. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +0 -297
  22. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +0 -1
  23. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +0 -73
  24. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +0 -1
  25. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +0 -183
  26. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +0 -1
  27. package/dist/benchmark/fanout-tree-sim-lib.d.ts +0 -303
  28. package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +0 -1
  29. package/dist/benchmark/fanout-tree-sim-lib.js +0 -1752
  30. package/dist/benchmark/fanout-tree-sim-lib.js.map +0 -1
  31. package/dist/benchmark/fanout-tree-sim.d.ts +0 -2
  32. package/dist/benchmark/fanout-tree-sim.d.ts.map +0 -1
  33. package/dist/benchmark/fanout-tree-sim.js +0 -870
  34. package/dist/benchmark/fanout-tree-sim.js.map +0 -1
  35. package/dist/benchmark/index.d.ts +0 -8
  36. package/dist/benchmark/index.d.ts.map +0 -1
  37. package/dist/benchmark/index.js +0 -62
  38. package/dist/benchmark/index.js.map +0 -1
  39. package/dist/benchmark/pubsub-topic-sim-lib.d.ts +0 -86
  40. package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +0 -1
  41. package/dist/benchmark/pubsub-topic-sim-lib.js +0 -680
  42. package/dist/benchmark/pubsub-topic-sim-lib.js.map +0 -1
  43. package/dist/benchmark/pubsub-topic-sim.d.ts +0 -9
  44. package/dist/benchmark/pubsub-topic-sim.d.ts.map +0 -1
  45. package/dist/benchmark/pubsub-topic-sim.js +0 -134
  46. package/dist/benchmark/pubsub-topic-sim.js.map +0 -1
  47. package/dist/benchmark/sim/bench-utils.d.ts +0 -25
  48. package/dist/benchmark/sim/bench-utils.d.ts.map +0 -1
  49. package/dist/benchmark/sim/bench-utils.js +0 -141
  50. package/dist/benchmark/sim/bench-utils.js.map +0 -1
@@ -1,1752 +0,0 @@
1
- import { PreHash, SignatureWithKey } from "@peerbit/crypto";
2
- import { InMemoryNetwork, InMemorySession, } from "@peerbit/libp2p-test-utils/inmemory-libp2p.js";
3
- import { delay } from "@peerbit/time";
4
- import { anySignal } from "any-signal";
5
- import { monitorEventLoopDelay } from "node:perf_hooks";
6
- import { FanoutTree } from "../src/index.js";
7
- import { parentUpgradeRuntimeOptions } from "./fanout-tree-parent-upgrade-preset.js";
8
- import { int, mulberry32, quantile, runWithConcurrency, } from "./sim/bench-utils.js";
9
- class SimFanoutTree extends FanoutTree {
10
- constructor(c, opts) {
11
- super(c, opts);
12
- // Fast/mock signing: keep signer identity semantics but skip crypto work.
13
- this.sign = async () => new SignatureWithKey({
14
- signature: new Uint8Array([0]),
15
- publicKey: this.publicKey,
16
- prehash: PreHash.NONE,
17
- });
18
- }
19
- async verifyAndProcess(message) {
20
- // Skip expensive crypto verify for large sims, but keep session handling behavior
21
- // consistent with the real implementation.
22
- const from = message.header.signatures.publicKeys[0];
23
- if (!this.peers.has(from.hashcode())) {
24
- this.updateSession(from, Number(message.header.session));
25
- }
26
- return true;
27
- }
28
- }
29
- const effectiveMaxChildrenForUpload = (params, ch, maxChildren) => {
30
- const requested = Math.max(0, Math.floor(maxChildren));
31
- const uploadLimitBps = Math.max(0, Math.floor(ch.uploadLimitBps ?? 0));
32
- if (uploadLimitBps <= 0)
33
- return 0;
34
- const msgRate = Math.max(1, Math.floor(params.msgRate));
35
- const msgSize = Math.max(1, Math.floor(params.msgSize));
36
- const uploadOverheadBytes = Math.max(0, Math.floor(ch.uploadOverheadBytes ?? 128));
37
- const perChildBytes = Math.max(1, 1 + msgSize + uploadOverheadBytes);
38
- const perChildBps = Math.max(1, Math.floor(msgRate * perChildBytes));
39
- return Math.max(0, Math.min(requested, Math.floor(uploadLimitBps / perChildBps)));
40
- };
41
- const pickDistinct = (rng, n, k, exclude) => {
42
- if (k <= 0)
43
- return [];
44
- const out = new Set();
45
- while (out.size < k) {
46
- const candidate = int(rng, n);
47
- if (exclude.has(candidate))
48
- continue;
49
- out.add(candidate);
50
- }
51
- return [...out];
52
- };
53
- const parseSimPeerIndex = (peerId) => {
54
- const s = String(peerId?.toString?.() ?? "");
55
- const m = s.match(/sim-(\d+)/);
56
- if (!m)
57
- return 0;
58
- const n = Number(m[1]);
59
- return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
60
- };
61
- export const resolveFanoutTreeSimParams = (input) => {
62
- const nodes = Number(input.nodes ?? 2000);
63
- const bootstraps = Number(input.bootstraps ?? 1);
64
- const subscribersDefault = Math.max(0, nodes - 1 - bootstraps);
65
- const msgRate = Number(input.msgRate ?? 30);
66
- const intervalMsRaw = Number(input.intervalMs ?? 0);
67
- const intervalMs = intervalMsRaw > 0
68
- ? intervalMsRaw
69
- : msgRate > 0
70
- ? Math.floor(1000 / msgRate)
71
- : 0;
72
- const parentUpgradeMaxChildLoadRatio = Number(input.parentUpgradeMaxChildLoadRatio ?? 0.5);
73
- const parentUpgradeRootMaxChildLoadRatio = Number(input.parentUpgradeRootMaxChildLoadRatio ??
74
- Math.min(parentUpgradeMaxChildLoadRatio, 0.4));
75
- const parentUpgradeMode = input.parentUpgradeMode === "probe" || input.parentUpgradeMode === "shadow"
76
- ? input.parentUpgradeMode
77
- : input.parentUpgradeMode === "direct"
78
- ? "direct"
79
- : "shadow";
80
- return {
81
- nodes,
82
- rootIndex: Number(input.rootIndex ?? 0),
83
- bootstraps,
84
- bootstrapMaxPeers: Number(input.bootstrapMaxPeers ?? 0),
85
- subscribers: Number(input.subscribers ?? subscribersDefault),
86
- relayFraction: Number(input.relayFraction ?? 0.25),
87
- messages: Number(input.messages ?? 200),
88
- secondBatchMessages: Number(input.secondBatchMessages ?? 0),
89
- secondBatchSettleMs: Number(input.secondBatchSettleMs ?? 0),
90
- msgRate,
91
- msgSize: Number(input.msgSize ?? 1024),
92
- intervalMs,
93
- settleMs: Number(input.settleMs ?? 2_000),
94
- deadlineMs: Number(input.deadlineMs ?? 0),
95
- maxDataAgeMs: Number(input.maxDataAgeMs ?? 0),
96
- timeoutMs: Number(input.timeoutMs ?? 300_000),
97
- seed: Number(input.seed ?? 1),
98
- topic: String(input.topic ?? "concert"),
99
- rootUploadLimitBps: Number(input.rootUploadLimitBps ?? 20_000_000),
100
- rootMaxChildren: Number(input.rootMaxChildren ?? 64),
101
- relayUploadLimitBps: Number(input.relayUploadLimitBps ?? 10_000_000),
102
- relayMaxChildren: Number(input.relayMaxChildren ?? 32),
103
- allowKick: Boolean(input.allowKick ?? false),
104
- bidPerByte: Number(input.bidPerByte ?? 0),
105
- bidPerByteRelay: Number(input.bidPerByteRelay ?? input.bidPerByte ?? 0),
106
- bidPerByteLeaf: Number(input.bidPerByteLeaf ?? input.bidPerByte ?? 0),
107
- repair: Boolean(input.repair ?? true),
108
- repairWindowMessages: Number(input.repairWindowMessages ?? 1024),
109
- repairMaxBackfillMessages: Number(input.repairMaxBackfillMessages ?? -1),
110
- repairIntervalMs: Number(input.repairIntervalMs ?? 200),
111
- repairMaxPerReq: Number(input.repairMaxPerReq ?? 64),
112
- neighborRepair: Boolean(input.neighborRepair ?? false),
113
- neighborRepairPeers: Number(input.neighborRepairPeers ?? 2),
114
- neighborMeshPeers: Number(input.neighborMeshPeers ?? -1),
115
- neighborAnnounceIntervalMs: Number(input.neighborAnnounceIntervalMs ?? -1),
116
- neighborMeshRefreshIntervalMs: Number(input.neighborMeshRefreshIntervalMs ?? -1),
117
- neighborHaveTtlMs: Number(input.neighborHaveTtlMs ?? -1),
118
- neighborRepairBudgetBps: Number(input.neighborRepairBudgetBps ?? -1),
119
- neighborRepairBurstMs: Number(input.neighborRepairBurstMs ?? -1),
120
- streamRxDelayMs: Number(input.streamRxDelayMs ?? 0),
121
- streamHighWaterMarkBytes: Number(input.streamHighWaterMarkBytes ?? 256 * 1024),
122
- dialDelayMs: Number(input.dialDelayMs ?? 0),
123
- joinConcurrency: Number(input.joinConcurrency ?? 256),
124
- joinReqTimeoutMs: Number(input.joinReqTimeoutMs ?? -1),
125
- candidateShuffleTopK: Number(input.candidateShuffleTopK ?? -1),
126
- candidateScoringMode: input.candidateScoringMode === "ranked-strict" ||
127
- input.candidateScoringMode === "weighted" ||
128
- input.candidateScoringMode === "ranked-shuffle"
129
- ? input.candidateScoringMode
130
- : "ranked-shuffle",
131
- parentUpgradeIntervalMs: Number(input.parentUpgradeIntervalMs ?? 0),
132
- parentUpgradeLeafOnly: Boolean(input.parentUpgradeLeafOnly ?? true),
133
- parentUpgradeMinLevelGain: Number(input.parentUpgradeMinLevelGain ?? 1),
134
- parentUpgradeRootMinLevelGain: Number(input.parentUpgradeRootMinLevelGain ?? 3),
135
- parentUpgradeRootMinSubtreeGain: Number(input.parentUpgradeRootMinSubtreeGain ??
136
- input.parentUpgradeRootMinLevelGain ??
137
- 3),
138
- parentUpgradeNonRootMinLevelGain: Number(input.parentUpgradeNonRootMinLevelGain ?? 2),
139
- parentUpgradeMinFreeSlots: Number(input.parentUpgradeMinFreeSlots ?? 8),
140
- parentUpgradeRootMinFreeSlots: Number(input.parentUpgradeRootMinFreeSlots ??
141
- input.parentUpgradeMinFreeSlots ??
142
- 8),
143
- parentUpgradeMaxChildLoadRatio,
144
- parentUpgradeRootMaxChildLoadRatio,
145
- parentUpgradeCooldownMs: Number(input.parentUpgradeCooldownMs ?? 5_000),
146
- parentUpgradeFailedBackoffMinMs: Number(input.parentUpgradeFailedBackoffMinMs ?? 5_000),
147
- parentUpgradeFailedBackoffMaxMs: Number(input.parentUpgradeFailedBackoffMaxMs ?? 60_000),
148
- parentUpgradeQuietMs: Number(input.parentUpgradeQuietMs ?? 5_000),
149
- parentUpgradeRepairQuietMs: Number(input.parentUpgradeRepairQuietMs ?? input.parentUpgradeQuietMs ?? 5_000),
150
- parentUpgradeMaxPerPeer: Number(input.parentUpgradeMaxPerPeer ?? 2),
151
- parentUpgradeRepairGuard: Boolean(input.parentUpgradeRepairGuard ?? true),
152
- parentUpgradeDataGuard: Boolean(input.parentUpgradeDataGuard ?? true),
153
- parentUpgradeMode,
154
- parentUpgradeVerifyStaleRootCapacity: Boolean(input.parentUpgradeVerifyStaleRootCapacity ??
155
- parentUpgradeMode === "shadow"),
156
- parentUpgradeStaleRootProbeProbability: Number(input.parentUpgradeStaleRootProbeProbability ?? 0.0625),
157
- parentProbeTimeoutMs: Number(input.parentProbeTimeoutMs ?? 500),
158
- parentProbeMaxPerRound: Number(input.parentProbeMaxPerRound ?? 2),
159
- parentProbeMaxLagMessages: Number(input.parentProbeMaxLagMessages ?? 0),
160
- parentProbeRejectCooldownMs: Number(input.parentProbeRejectCooldownMs ?? 10_000),
161
- parentProbeRejectCooldownMaxMs: Number(input.parentProbeRejectCooldownMaxMs ?? 60_000),
162
- parentShadowObserveMs: Number(input.parentShadowObserveMs ?? 2_000),
163
- parentShadowMinObservations: Number(input.parentShadowMinObservations ?? 2),
164
- parentShadowDualPathMs: Number(input.parentShadowDualPathMs ??
165
- (parentUpgradeMode === "shadow" ? 5_000 : 0)),
166
- parentShadowDualPathMinMessages: Number(input.parentShadowDualPathMinMessages ??
167
- (parentUpgradeMode === "shadow" ? 32 : 1)),
168
- bootstrapEnsureIntervalMs: Number(input.bootstrapEnsureIntervalMs ?? -1),
169
- trackerQueryIntervalMs: Number(input.trackerQueryIntervalMs ?? -1),
170
- joinAttemptsPerRound: Number(input.joinAttemptsPerRound ?? -1),
171
- candidateCooldownMs: Number(input.candidateCooldownMs ?? -1),
172
- joinPhases: Boolean(input.joinPhases ?? false),
173
- joinPhaseSettleMs: Number(input.joinPhaseSettleMs ?? 2_000),
174
- lateRootConnectAfterMs: Number(input.lateRootConnectAfterMs ?? -1),
175
- lateRootDuringPublish: Boolean(input.lateRootDuringPublish ?? false),
176
- lateRootMaxChildren: Number(input.lateRootMaxChildren ?? 0),
177
- lateRootConnectFraction: Number(input.lateRootConnectFraction ?? 1),
178
- maxLatencySamples: Number(input.maxLatencySamples ?? 1_000_000),
179
- profile: Boolean(input.profile ?? false),
180
- progress: Boolean(input.progress ?? false),
181
- progressEveryMs: Number(input.progressEveryMs ?? 5_000),
182
- dropDataFrameRate: Number(input.dropDataFrameRate ?? 0),
183
- churnEveryMs: Number(input.churnEveryMs ?? 0),
184
- churnDownMs: Number(input.churnDownMs ?? 0),
185
- churnFraction: Number(input.churnFraction ?? 0),
186
- assertMinJoinedPct: Number(input.assertMinJoinedPct ?? 0),
187
- assertMinDeliveryPct: Number(input.assertMinDeliveryPct ?? 0),
188
- assertMinDeadlineDeliveryPct: Number(input.assertMinDeadlineDeliveryPct ?? 0),
189
- assertMaxUploadFracPct: Number(input.assertMaxUploadFracPct ?? 0),
190
- assertMaxOverheadFactor: Number(input.assertMaxOverheadFactor ?? 0),
191
- assertMaxControlBpp: Number(input.assertMaxControlBpp ?? 0),
192
- assertMaxTrackerBpp: Number(input.assertMaxTrackerBpp ?? 0),
193
- assertMaxTrackerBytes: Number(input.assertMaxTrackerBytes ?? 0),
194
- assertMaxRepairBpp: Number(input.assertMaxRepairBpp ?? 0),
195
- assertAttachP95Ms: Number(input.assertAttachP95Ms ?? 0),
196
- assertMaxTreeLevelP95: Number(input.assertMaxTreeLevelP95 ?? 0),
197
- assertMaxFormationScore: Number(input.assertMaxFormationScore ?? 0),
198
- assertMinReparentUpgradeTotal: Number(input.assertMinReparentUpgradeTotal ?? 0),
199
- assertMinActiveShadowPromoteTotal: Number(input.assertMinActiveShadowPromoteTotal ?? 0),
200
- assertMaxOrphans: Number(input.assertMaxOrphans ?? 0),
201
- assertRecoveryP95Ms: Number(input.assertRecoveryP95Ms ?? 0),
202
- assertMaxReparentsPerMin: Number(input.assertMaxReparentsPerMin ?? 0),
203
- assertMaxOrphanArea: Number(input.assertMaxOrphanArea ?? 0),
204
- };
205
- };
206
- export const formatFanoutTreeSimResult = (r) => {
207
- const p = r.params;
208
- return [
209
- "fanout-tree-sim",
210
- `nodes=${p.nodes} bootstraps=${r.bootstrapCount} bootstrapMaxPeers=${p.bootstrapMaxPeers} subscribers=${r.subscriberCount} relays=${r.relayCount}`,
211
- ...(p.lateRootConnectAfterMs >= 0
212
- ? [
213
- `lateRoot: afterMs=${p.lateRootConnectAfterMs} maxChildren=${p.lateRootMaxChildren} connectFraction=${p.lateRootConnectFraction}`,
214
- ]
215
- : []),
216
- `joined=${r.joinedCount}/${r.subscriberCount} (${r.joinedPct.toFixed(2)}%)`,
217
- `join: ${(r.joinMs / 1000).toFixed(3)}s`,
218
- `attachMs samples=${r.attachSamples} p50=${r.attachP50.toFixed(1)} p95=${r.attachP95.toFixed(1)} p99=${r.attachP99.toFixed(1)} max=${r.attachMax.toFixed(1)}`,
219
- `formationPaths: underlayEdges=${r.formationUnderlayEdges} dist(p95/max)=${r.formationUnderlayDistP95.toFixed(1)}/${r.formationUnderlayDistMax.toFixed(1)} stretch(p95/max)=${r.formationStretchP95.toFixed(2)}/${r.formationStretchMax.toFixed(2)} score=${r.formationScore.toFixed(2)}`,
220
- `formationTree: maxLevel=${r.formationTreeMaxLevel} p95Level=${r.formationTreeLevelP95.toFixed(1)} avgLevel=${r.formationTreeLevelAvg.toFixed(2)} orphans=${r.formationTreeOrphans} rootChildren=${r.formationTreeRootChildren} children(p95/max)=${r.formationTreeChildrenP95.toFixed(1)}/${r.formationTreeChildrenMax}`,
221
- `publish: ${(r.publishMs / 1000).toFixed(3)}s intervalMs=${p.intervalMs}`,
222
- ...(p.secondBatchMessages > 0
223
- ? [
224
- `secondBatch: messages=${p.secondBatchMessages} settleMs=${p.secondBatchSettleMs} delivered=${r.secondBatchDelivered}/${r.secondBatchExpected} (${r.secondBatchDeliveredPct.toFixed(2)}%) deadline=${r.secondBatchDeliveredWithinDeadline}/${r.secondBatchExpected} (${r.secondBatchDeliveredWithinDeadlinePct.toFixed(2)}%) latencyP95=${r.secondBatchLatencyP95.toFixed(1)}`,
225
- ]
226
- : []),
227
- `churn: everyMs=${p.churnEveryMs} downMs=${p.churnDownMs} fraction=${p.churnFraction} events=${r.churnEvents} peers=${r.churnedPeersTotal}`,
228
- ...(r.maintSamples > 0
229
- ? [
230
- `maintenance: maxOrphans=${r.maintMaxOrphans} orphanArea=${r.maintOrphanArea.toFixed(1)}s recoveryMs p50=${r.maintRecoveryP50Ms.toFixed(1)} p95=${r.maintRecoveryP95Ms.toFixed(1)} reparentsPerMin=${r.maintReparentsPerMin.toFixed(2)} flapMax=${r.maintMaxReparentsPerPeer} driftP95(level/children)=${r.maintLevelP95DriftMax.toFixed(1)}/${r.maintChildrenP95DriftMax.toFixed(1)}`,
231
- ]
232
- : []),
233
- `delivered=${r.delivered}/${r.expected} (${r.deliveredPct.toFixed(2)}%) dup=${r.duplicates}`,
234
- p.deadlineMs > 0
235
- ? `deadline=${p.deadlineMs}ms${p.maxDataAgeMs > 0 ? ` maxAgeMs=${p.maxDataAgeMs}` : ""} delivered=${r.deliveredWithinDeadline}/${r.expected} (${r.deliveredWithinDeadlinePct.toFixed(2)}%)`
236
- : `deadline=off${p.maxDataAgeMs > 0 ? ` maxAgeMs=${p.maxDataAgeMs}` : ""}`,
237
- `latencyMs p50=${r.latencyP50.toFixed(1)} p95=${r.latencyP95.toFixed(1)} p99=${r.latencyP99.toFixed(1)} max=${r.latencyMax.toFixed(1)}`,
238
- `drops: forward total=${r.droppedForwardsTotal} max=${r.droppedForwardsMax} node=${r.droppedForwardsMaxNode ?? "-"} stale total=${r.staleForwardsDroppedTotal} max=${r.staleForwardsDroppedMax} node=${r.staleForwardsDroppedMaxNode ?? "-"} write total=${r.dataWriteDropsTotal} max=${r.dataWriteDropsMax} node=${r.dataWriteDropsMaxNode ?? "-"}`,
239
- ...(p.lateRootDuringPublish ||
240
- r.publishActiveReparentUpgradeSkipDataTotal > 0
241
- ? [
242
- `publishActiveParentUpgrade: upgrade=${r.publishActiveReparentUpgradeTotal} dataSkips=${r.publishActiveReparentUpgradeSkipDataTotal} repairSkips=${r.publishActiveReparentUpgradeSkipRepairTotal} quietSkips=${r.publishActiveReparentUpgradeSkipQuietTotal} probes=${r.publishActiveParentProbeReqSentTotal} shadowStart=${r.publishActiveParentShadowStartTotal} shadowPromote=${r.publishActiveParentShadowPromoteTotal}`,
243
- ]
244
- : []),
245
- `reparent: disconnect=${r.reparentDisconnectTotal} stale=${r.reparentStaleTotal} kicked=${r.reparentKickedTotal} upgrade=${r.reparentUpgradeTotal}`,
246
- `reparentUpgradeSkipped: leaf=${r.reparentUpgradeSkipLeafTotal} repair=${r.reparentUpgradeSkipRepairTotal} data=${r.reparentUpgradeSkipDataTotal} cooldown=${r.reparentUpgradeSkipCooldownTotal} quiet=${r.reparentUpgradeSkipQuietTotal} budget=${r.reparentUpgradeSkipBudgetTotal} candidateLevel=${r.reparentUpgradeSkipCandidateLevelTotal} candidateSlots=${r.reparentUpgradeSkipCandidateSlotsTotal} candidatePressure=${r.reparentUpgradeSkipCandidatePressureTotal} rootPressure=${r.reparentUpgradeSkipRootPressureTotal}`,
247
- `parentProbe: req=${r.parentProbeReqSentTotal}/${r.parentProbeReqReceivedTotal} reply=${r.parentProbeReplySentTotal}/${r.parentProbeReplyReceivedTotal} skipped noReply=${r.reparentUpgradeSkipProbeNoReplyTotal} notRooted=${r.reparentUpgradeSkipProbeNotRootedTotal} repair=${r.reparentUpgradeSkipProbeRepairTotal} lag=${r.reparentUpgradeSkipProbeLagTotal} overloaded=${r.reparentUpgradeSkipProbeOverloadedTotal} cooldown=${r.reparentUpgradeSkipProbeCooldownTotal}`,
248
- `parentRootReservations: created=${r.parentUpgradeRootReservationCreatedTotal} consumed=${r.parentUpgradeRootReservationConsumedTotal} rejected=${r.parentUpgradeRootReservationRejectedTotal} marginRejected=${r.parentUpgradeRootReservationMarginRejectedTotal} blocked=${r.parentUpgradeRootReservationBlockedTotal} expired=${r.parentUpgradeRootReservationExpiredTotal}`,
249
- `parentShadow: start=${r.parentShadowStartTotal} observe=${r.parentShadowObserveTotal} promote=${r.parentShadowPromoteTotal} reset=${r.parentShadowResetTotal} reject noReply=${r.parentShadowRejectNoReplyTotal} notRooted=${r.parentShadowRejectNotRootedTotal} capacity=${r.parentShadowRejectCapacityTotal} repair=${r.parentShadowRejectRepairTotal} lag=${r.parentShadowRejectLagTotal} overloaded=${r.parentShadowRejectOverloadedTotal} level=${r.parentShadowRejectLevelTotal}`,
250
- `tree: maxLevel=${r.treeMaxLevel} p95Level=${r.treeLevelP95.toFixed(1)} avgLevel=${r.treeLevelAvg.toFixed(2)} orphans=${r.treeOrphans} rootChildren=${r.treeRootChildren} children(p95/max)=${r.treeChildrenP95.toFixed(1)}/${r.treeChildrenMax}`,
251
- `upload: max=${r.maxUploadBps} B/s (${r.maxUploadFracPct.toFixed(1)}% of cap) node=${r.maxUploadNode ?? "-"} root=${r.rootUploadBps} B/s (${r.rootUploadFracPct.toFixed(1)}% of cap)`,
252
- `stream: queuedBytes total=${r.streamQueuedBytesTotal} max=${r.streamQueuedBytesMax} p95=${r.streamQueuedBytesP95.toFixed(0)} node=${r.streamQueuedBytesMaxNode ?? "-"} lanes=${r.streamQueuedBytesByLane.join(",")}`,
253
- `overhead: dataFactor=${r.overheadFactorData.toFixed(3)} (sentPayloadBytes / ideal)`,
254
- `economics: earningsTotal=${r.earningsTotal} relayCount=${r.earningsRelayCount} p50=${r.earningsRelayP50} p95=${r.earningsRelayP95} max=${r.earningsRelayMax}`,
255
- `protocol: controlBytesSent=${r.protocolControlBytesSent} (join=${r.protocolControlBytesSentJoin} tracker=${r.protocolControlBytesSentTracker} repair=${r.protocolControlBytesSentRepair}) bpp=${r.controlBpp.toFixed(4)} (tracker=${r.trackerBpp.toFixed(4)} repair=${r.repairBpp.toFixed(4)}) dataPayloadBytesSent=${r.protocolDataPayloadBytesSent} fetchReqSent=${r.protocolFetchReqSent} ihaveSent=${r.protocolIHaveSent} tracker(a/q/r/f)=${r.protocolTrackerAnnounceSent}/${r.protocolTrackerQuerySent}/${r.protocolTrackerReplySent}/${r.protocolTrackerFeedbackSent} holeFills=${r.protocolHoleFillsFromNeighbor} routeCache(h/m/x/e)=${r.protocolRouteCacheHits}/${r.protocolRouteCacheMisses}/${r.protocolRouteCacheExpirations}/${r.protocolRouteCacheEvictions} routeProxy(q/t/f)=${r.protocolRouteProxyQueries}/${r.protocolRouteProxyTimeouts}/${r.protocolRouteProxyFanout}`,
256
- `network: dials=${r.network.dials} connsOpened=${r.network.connectionsOpened} streamsOpened=${r.network.streamsOpened} framesSent=${r.network.framesSent} bytesSent=${r.network.bytesSent} framesDropped=${r.network.framesDropped} bytesDropped=${r.network.bytesDropped}`,
257
- ...(r.profile
258
- ? [
259
- `profile: cpuUserMs=${r.profile.cpuUserMs.toFixed(1)} cpuSystemMs=${r.profile.cpuSystemMs.toFixed(1)} rssMb=${r.profile.rssMb.toFixed(1)} heapUsedMb=${r.profile.heapUsedMb.toFixed(1)} eldP95Ms=${r.profile.eventLoopDelayP95Ms.toFixed(2)} eldMaxMs=${r.profile.eventLoopDelayMaxMs.toFixed(2)}`,
260
- ]
261
- : []),
262
- ].join("\n");
263
- };
264
- export const runFanoutTreeSim = async (input) => {
265
- const params = resolveFanoutTreeSimParams(input);
266
- const timeoutMs = Math.max(0, Math.floor(params.timeoutMs));
267
- const rng = mulberry32(params.seed);
268
- const profileEnabled = params.profile === true;
269
- let profileCpuStart;
270
- let profileEld;
271
- if (profileEnabled) {
272
- profileCpuStart = process.cpuUsage();
273
- try {
274
- profileEld = monitorEventLoopDelay({ resolution: 20 });
275
- profileEld.enable();
276
- }
277
- catch {
278
- // ignore
279
- }
280
- }
281
- const rootIndex = Math.max(0, Math.min(params.nodes - 1, params.rootIndex));
282
- const bootstrapCount = Math.max(0, Math.min(params.nodes - 1, Math.floor(params.bootstraps)));
283
- const bootstrapIndices = Array.from({ length: bootstrapCount }, (_, i) => {
284
- const idx = i + 1;
285
- return idx >= params.nodes ? 0 : idx;
286
- }).filter((i) => i !== rootIndex);
287
- const exclude = new Set([rootIndex, ...bootstrapIndices]);
288
- const subscriberCount = Math.max(0, Math.min(params.nodes - exclude.size, Math.floor(params.subscribers)));
289
- const subscriberIndices = pickDistinct(rng, params.nodes, subscriberCount, exclude);
290
- // Ensure we have at least one relay when scale > root fanout.
291
- const wantsRelays = subscriberCount > Math.max(0, params.rootMaxChildren);
292
- let relayTarget = Math.floor(subscriberCount * Math.max(0, Math.min(1, params.relayFraction)));
293
- if (wantsRelays && relayTarget === 0)
294
- relayTarget = Math.min(1, subscriberCount);
295
- const relaySet = new Set();
296
- const shuffledSubscribers = [...subscriberIndices].sort(() => rng() - 0.5);
297
- for (const idx of shuffledSubscribers) {
298
- if (relaySet.size >= relayTarget)
299
- break;
300
- relaySet.add(idx);
301
- }
302
- const network = new InMemoryNetwork({
303
- streamRxDelayMs: params.streamRxDelayMs,
304
- streamHighWaterMarkBytes: params.streamHighWaterMarkBytes,
305
- dialDelayMs: params.dialDelayMs,
306
- dropDataFrameRate: params.dropDataFrameRate,
307
- dropSeed: params.seed,
308
- });
309
- const bootstrapIndexSet = new Set(bootstrapIndices);
310
- // Sim-only connection caps must account for tracker/bootstrap fan-in. Capping those
311
- // peers near the overlay degree works for small runs, but at 5k+ it starves join
312
- // formation long before the tree itself is saturated.
313
- const rootConnectionCap = Math.max(256, params.rootMaxChildren * 8);
314
- const relayConnectionCap = Math.max(64, params.relayMaxChildren * 4);
315
- const bootstrapConnectionCap = Math.max(512, Math.min(2_048, Math.ceil((subscriberIndices.length + 1) / Math.max(1, bootstrapIndices.length))));
316
- const maxConnectionsFor = (index) => {
317
- if (index === rootIndex)
318
- return rootConnectionCap;
319
- if (bootstrapIndexSet.has(index))
320
- return bootstrapConnectionCap;
321
- if (relaySet.has(index))
322
- return relayConnectionCap;
323
- return 16;
324
- };
325
- const seenCacheMaxFor = (index) => {
326
- if (index === rootIndex)
327
- return 200_000;
328
- if (bootstrapIndexSet.has(index))
329
- return 100_000;
330
- if (relaySet.has(index))
331
- return 50_000;
332
- return 20_000;
333
- };
334
- const seenCacheTtlMsFor = (index) => {
335
- if (index === rootIndex || bootstrapIndexSet.has(index))
336
- return 120_000;
337
- return 60_000;
338
- };
339
- const session = await InMemorySession.disconnected(params.nodes, {
340
- network,
341
- basePort: 30_000,
342
- services: {
343
- fanout: (c) => {
344
- const index = parseSimPeerIndex(c?.peerId);
345
- return new SimFanoutTree(c, {
346
- // Keep sims bounded: limit per-node connections to roughly the fanout degree,
347
- // so large networks can run in a single process without OOM.
348
- connectionManager: {
349
- minConnections: 0,
350
- maxConnections: maxConnectionsFor(index),
351
- dialer: false,
352
- pruner: { interval: 1_000 },
353
- },
354
- seenCacheMax: seenCacheMaxFor(index),
355
- seenCacheTtlMs: seenCacheTtlMsFor(index),
356
- random: mulberry32((params.seed >>> 0) ^ index),
357
- });
358
- },
359
- },
360
- });
361
- let timer;
362
- const timeoutController = new AbortController();
363
- const timeoutSignal = timeoutController.signal;
364
- try {
365
- if (timeoutMs > 0) {
366
- timer = setTimeout(() => {
367
- timeoutController.abort(new Error(`fanout-tree-sim timed out after ${timeoutMs}ms (override with --timeoutMs)`));
368
- }, timeoutMs);
369
- }
370
- const run = async () => {
371
- const bootstrapAddrs = bootstrapIndices.flatMap((i) => session.peers[i].getMultiaddrs());
372
- if (bootstrapAddrs.length === 0) {
373
- throw new Error("No bootstrap addrs; pass --bootstraps >= 1");
374
- }
375
- for (const p of session.peers) {
376
- p.services.fanout.setBootstraps(bootstrapAddrs);
377
- }
378
- const root = session.peers[rootIndex].services.fanout;
379
- const rootId = root.publicKeyHash;
380
- // Root opens channel and starts announcing capacity to trackers immediately.
381
- root.openChannel(params.topic, rootId, {
382
- role: "root",
383
- msgRate: params.msgRate,
384
- msgSize: params.msgSize,
385
- ...(params.maxDataAgeMs > 0
386
- ? { maxDataAgeMs: params.maxDataAgeMs }
387
- : {}),
388
- uploadLimitBps: params.rootUploadLimitBps,
389
- maxChildren: params.rootMaxChildren,
390
- bidPerByte: params.bidPerByte,
391
- allowKick: params.allowKick,
392
- repair: params.repair,
393
- repairWindowMessages: params.repairWindowMessages,
394
- ...(params.repairMaxBackfillMessages >= 0
395
- ? { repairMaxBackfillMessages: params.repairMaxBackfillMessages }
396
- : {}),
397
- repairIntervalMs: params.repairIntervalMs,
398
- repairMaxPerReq: params.repairMaxPerReq,
399
- neighborRepair: params.neighborRepair,
400
- neighborRepairPeers: params.neighborRepairPeers,
401
- ...(params.neighborMeshPeers >= 0
402
- ? { neighborMeshPeers: params.neighborMeshPeers }
403
- : {}),
404
- ...(params.neighborAnnounceIntervalMs >= 0
405
- ? { neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs }
406
- : {}),
407
- ...(params.neighborMeshRefreshIntervalMs >= 0
408
- ? {
409
- neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs,
410
- }
411
- : {}),
412
- ...(params.neighborHaveTtlMs >= 0
413
- ? { neighborHaveTtlMs: params.neighborHaveTtlMs }
414
- : {}),
415
- ...(params.neighborRepairBudgetBps >= 0
416
- ? { neighborRepairBudgetBps: params.neighborRepairBudgetBps }
417
- : {}),
418
- ...(params.neighborRepairBurstMs >= 0
419
- ? { neighborRepairBurstMs: params.neighborRepairBurstMs }
420
- : {}),
421
- });
422
- // Join subscribers (bounded concurrency).
423
- const joinStart = Date.now();
424
- const joined = new Array(subscriberIndices.length).fill(false);
425
- const attachDurationsByPos = new Array(subscriberIndices.length).fill(-1);
426
- let joinCompleted = 0;
427
- let joinOk = 0;
428
- const progressEveryMs = Math.max(250, Math.floor(params.progressEveryMs || 5_000));
429
- let joinProgressTimer;
430
- if (params.progress) {
431
- console.log(`[fanout-tree-sim] phase=join subscribers=${subscriberIndices.length} relays=${relaySet.size} joinConcurrency=${params.joinConcurrency}`);
432
- joinProgressTimer = setInterval(() => {
433
- const mu = process.memoryUsage();
434
- const rssMb = mu.rss / (1024 * 1024);
435
- const heapUsedMb = mu.heapUsed / (1024 * 1024);
436
- const openConns = Math.max(0, network.metrics.connectionsOpened -
437
- network.metrics.connectionsClosed);
438
- console.log(`[fanout-tree-sim] join progress ok=${joinOk}/${subscriberIndices.length} done=${joinCompleted}/${subscriberIndices.length} openConns=${openConns} dials=${network.metrics.dials} streamsOpened=${network.metrics.streamsOpened} rssMb=${rssMb.toFixed(1)} heapUsedMb=${heapUsedMb.toFixed(1)}`);
439
- }, progressEveryMs);
440
- joinProgressTimer.unref?.();
441
- }
442
- const joinOne = async (idx) => {
443
- const node = session.peers[idx].services.fanout;
444
- const isRelay = relaySet.has(idx);
445
- try {
446
- await node.joinChannel(params.topic, rootId, {
447
- msgRate: params.msgRate,
448
- msgSize: params.msgSize,
449
- ...(params.maxDataAgeMs > 0
450
- ? { maxDataAgeMs: params.maxDataAgeMs }
451
- : {}),
452
- uploadLimitBps: isRelay ? params.relayUploadLimitBps : 0,
453
- maxChildren: isRelay ? params.relayMaxChildren : 0,
454
- bidPerByte: isRelay
455
- ? params.bidPerByteRelay
456
- : params.bidPerByteLeaf,
457
- allowKick: params.allowKick,
458
- repair: params.repair,
459
- repairWindowMessages: params.repairWindowMessages,
460
- ...(params.repairMaxBackfillMessages >= 0
461
- ? {
462
- repairMaxBackfillMessages: params.repairMaxBackfillMessages,
463
- }
464
- : {}),
465
- repairIntervalMs: params.repairIntervalMs,
466
- repairMaxPerReq: params.repairMaxPerReq,
467
- neighborRepair: params.neighborRepair,
468
- neighborRepairPeers: params.neighborRepairPeers,
469
- ...(params.neighborMeshPeers >= 0
470
- ? { neighborMeshPeers: params.neighborMeshPeers }
471
- : {}),
472
- ...(params.neighborAnnounceIntervalMs >= 0
473
- ? {
474
- neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs,
475
- }
476
- : {}),
477
- ...(params.neighborMeshRefreshIntervalMs >= 0
478
- ? {
479
- neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs,
480
- }
481
- : {}),
482
- ...(params.neighborHaveTtlMs >= 0
483
- ? { neighborHaveTtlMs: params.neighborHaveTtlMs }
484
- : {}),
485
- ...(params.neighborRepairBudgetBps >= 0
486
- ? { neighborRepairBudgetBps: params.neighborRepairBudgetBps }
487
- : {}),
488
- ...(params.neighborRepairBurstMs >= 0
489
- ? { neighborRepairBurstMs: params.neighborRepairBurstMs }
490
- : {}),
491
- }, {
492
- timeoutMs: Math.max(10_000, Math.min(120_000, timeoutMs || 120_000)),
493
- ...(params.maxDataAgeMs > 0
494
- ? { staleAfterMs: params.maxDataAgeMs }
495
- : {}),
496
- ...(params.joinReqTimeoutMs >= 0
497
- ? { joinReqTimeoutMs: params.joinReqTimeoutMs }
498
- : {}),
499
- ...(params.candidateShuffleTopK >= 0
500
- ? { candidateShuffleTopK: params.candidateShuffleTopK }
501
- : {}),
502
- candidateScoringMode: params.candidateScoringMode,
503
- ...parentUpgradeRuntimeOptions(params),
504
- ...(params.bootstrapEnsureIntervalMs >= 0
505
- ? {
506
- bootstrapEnsureIntervalMs: params.bootstrapEnsureIntervalMs,
507
- }
508
- : {}),
509
- ...(params.trackerQueryIntervalMs >= 0
510
- ? { trackerQueryIntervalMs: params.trackerQueryIntervalMs }
511
- : {}),
512
- ...(params.joinAttemptsPerRound >= 0
513
- ? { joinAttemptsPerRound: params.joinAttemptsPerRound }
514
- : {}),
515
- ...(params.candidateCooldownMs >= 0
516
- ? { candidateCooldownMs: params.candidateCooldownMs }
517
- : {}),
518
- signal: timeoutSignal,
519
- bootstrapMaxPeers: params.bootstrapMaxPeers,
520
- });
521
- return true;
522
- }
523
- catch {
524
- return false;
525
- }
526
- };
527
- const runPhase = async (indices) => {
528
- const tasks = indices.map((pos) => async () => {
529
- const ok = await joinOne(subscriberIndices[pos]);
530
- joined[pos] = ok;
531
- if (ok) {
532
- joinOk += 1;
533
- attachDurationsByPos[pos] = Date.now() - joinStart;
534
- }
535
- joinCompleted += 1;
536
- return ok;
537
- });
538
- await runWithConcurrency(tasks, params.joinConcurrency);
539
- };
540
- try {
541
- if (params.joinPhases) {
542
- const relayPositions = [];
543
- const leafPositions = [];
544
- for (let i = 0; i < subscriberIndices.length; i++) {
545
- const idx = subscriberIndices[i];
546
- if (relaySet.has(idx))
547
- relayPositions.push(i);
548
- else
549
- leafPositions.push(i);
550
- }
551
- await runPhase(relayPositions);
552
- const settleMs = Math.max(0, Math.floor(params.joinPhaseSettleMs));
553
- if (settleMs > 0) {
554
- await delay(settleMs, { signal: timeoutSignal });
555
- }
556
- await runPhase(leafPositions);
557
- }
558
- else {
559
- await runPhase(subscriberIndices.map((_, i) => i));
560
- }
561
- }
562
- finally {
563
- if (joinProgressTimer)
564
- clearInterval(joinProgressTimer);
565
- }
566
- const joinDone = Date.now();
567
- if (params.progress) {
568
- const mu = process.memoryUsage();
569
- const rssMb = mu.rss / (1024 * 1024);
570
- const heapUsedMb = mu.heapUsed / (1024 * 1024);
571
- const openConns = Math.max(0, network.metrics.connectionsOpened - network.metrics.connectionsClosed);
572
- console.log(`[fanout-tree-sim] phase=join_done ok=${joinOk}/${subscriberIndices.length} openConns=${openConns} rssMb=${rssMb.toFixed(1)} heapUsedMb=${heapUsedMb.toFixed(1)} joinMs=${joinDone - joinStart}`);
573
- }
574
- const attachDurations = attachDurationsByPos
575
- .filter((d) => d >= 0)
576
- .sort((a, b) => a - b);
577
- const attachSamples = attachDurations.length;
578
- const attachP50 = attachSamples > 0 ? quantile(attachDurations, 0.5) : NaN;
579
- const attachP95 = attachSamples > 0 ? quantile(attachDurations, 0.95) : NaN;
580
- const attachP99 = attachSamples > 0 ? quantile(attachDurations, 0.99) : NaN;
581
- const attachMax = attachSamples > 0 ? attachDurations[attachSamples - 1] : NaN;
582
- const joinedHashes = new Set(subscriberIndices
583
- .filter((_, i) => joined[i])
584
- .map((i) => session.peers[i].services.fanout.publicKeyHash));
585
- const joinedCount = joinedHashes.size;
586
- const joinedSubscriberIndices = subscriberIndices.filter((_, i) => joined[i]);
587
- const computeTreeShapeStats = () => {
588
- const levels = [];
589
- const levelByIndex = new Array(params.nodes).fill(NaN);
590
- const childrenByHash = new Map();
591
- const childrenCounts = [];
592
- let treeOrphans = 0;
593
- let treeRootChildren = 0;
594
- for (let i = 0; i < session.peers.length; i++) {
595
- const hash = session.peers[i].services.fanout.publicKeyHash;
596
- const s = session.peers[i].services.fanout.getChannelStats(params.topic, rootId);
597
- if (!s)
598
- continue;
599
- if (!childrenByHash.has(hash))
600
- childrenByHash.set(hash, []);
601
- if (Number.isFinite(s.level)) {
602
- levels.push(s.level);
603
- levelByIndex[i] = s.level;
604
- }
605
- if (s.effectiveMaxChildren > 0) {
606
- childrenCounts.push(s.children);
607
- }
608
- if (s.parent) {
609
- const children = childrenByHash.get(s.parent) ?? [];
610
- children.push(hash);
611
- childrenByHash.set(s.parent, children);
612
- }
613
- if (s.level === 0) {
614
- treeRootChildren = s.children;
615
- }
616
- else if (Number.isFinite(s.level) && !s.parent) {
617
- treeOrphans += 1;
618
- }
619
- }
620
- levels.sort((a, b) => a - b);
621
- childrenCounts.sort((a, b) => a - b);
622
- const treeMaxLevel = levels.length > 0 ? levels[levels.length - 1] : 0;
623
- const treeLevelP95 = levels.length > 0 ? quantile(levels, 0.95) : 0;
624
- const treeLevelAvg = levels.length > 0
625
- ? levels.reduce((a, b) => a + b, 0) / levels.length
626
- : 0;
627
- const treeChildrenP95 = childrenCounts.length > 0 ? quantile(childrenCounts, 0.95) : 0;
628
- const treeChildrenMax = childrenCounts.length > 0
629
- ? childrenCounts[childrenCounts.length - 1]
630
- : 0;
631
- return {
632
- treeMaxLevel,
633
- treeLevelP95,
634
- treeLevelAvg,
635
- treeOrphans,
636
- treeChildrenP95,
637
- treeChildrenMax,
638
- treeRootChildren,
639
- levelByIndex,
640
- childrenByHash,
641
- };
642
- };
643
- const formationTree = computeTreeShapeStats();
644
- // Underlay (libp2p connection graph) shortest paths, used to spot wasted
645
- // open connections that don't contribute to the overlay tree.
646
- const underlayAdj = Array.from({ length: params.nodes }, () => new Set());
647
- for (let i = 0; i < session.peers.length; i++) {
648
- for (const c of session.peers[i].getConnections()) {
649
- // @ts-ignore - bench shim uses the same field name as real libp2p connections
650
- if (c.status && c.status !== "open")
651
- continue;
652
- const j = parseSimPeerIndex(c.remotePeer);
653
- if (j === i)
654
- continue;
655
- if (j < 0 || j >= params.nodes)
656
- continue;
657
- underlayAdj[i].add(j);
658
- }
659
- }
660
- let formationUnderlayEdges = 0;
661
- for (const s of underlayAdj)
662
- formationUnderlayEdges += s.size;
663
- formationUnderlayEdges = Math.floor(formationUnderlayEdges / 2);
664
- const underlayDist = new Array(params.nodes).fill(Infinity);
665
- const q = [];
666
- underlayDist[rootIndex] = 0;
667
- q.push(rootIndex);
668
- for (let qi = 0; qi < q.length; qi++) {
669
- const u = q[qi];
670
- const du = underlayDist[u];
671
- for (const v of underlayAdj[u]) {
672
- if (underlayDist[v] !== Infinity)
673
- continue;
674
- underlayDist[v] = du + 1;
675
- q.push(v);
676
- }
677
- }
678
- const formationUnderlayDists = joinedSubscriberIndices
679
- .map((i) => underlayDist[i])
680
- .filter((d) => Number.isFinite(d))
681
- .sort((a, b) => a - b);
682
- const formationUnderlayDistP95 = formationUnderlayDists.length > 0
683
- ? quantile(formationUnderlayDists, 0.95)
684
- : NaN;
685
- const formationUnderlayDistMax = formationUnderlayDists.length > 0
686
- ? formationUnderlayDists[formationUnderlayDists.length - 1]
687
- : NaN;
688
- const formationStretches = joinedSubscriberIndices
689
- .map((i) => {
690
- const overlay = formationTree.levelByIndex[i];
691
- const under = underlayDist[i];
692
- if (!Number.isFinite(overlay) ||
693
- !Number.isFinite(under) ||
694
- under <= 0)
695
- return NaN;
696
- return overlay / under;
697
- })
698
- .filter((x) => Number.isFinite(x))
699
- .sort((a, b) => a - b);
700
- const formationStretchP95 = formationStretches.length > 0
701
- ? quantile(formationStretches, 0.95)
702
- : NaN;
703
- const formationStretchMax = formationStretches.length > 0
704
- ? formationStretches[formationStretches.length - 1]
705
- : NaN;
706
- const formationOrphanPct = joinedCount === 0 ? 0 : (100 * formationTree.treeOrphans) / joinedCount;
707
- const formationStretchPenalty = Number.isFinite(formationStretchP95)
708
- ? Math.max(0, formationStretchP95 - 1) * 10
709
- : 0;
710
- const formationScore = (Number.isFinite(attachP95) ? attachP95 / 1000 : 0) +
711
- formationTree.treeLevelP95 +
712
- formationOrphanPct +
713
- formationStretchPenalty;
714
- const churnController = new AbortController();
715
- const churnSignal = anySignal([
716
- timeoutSignal,
717
- churnController.signal,
718
- ]);
719
- let churnEvents = 0;
720
- let churnedPeersTotal = 0;
721
- const wantsMaintenance = (params.churnEveryMs > 0 &&
722
- params.churnDownMs > 0 &&
723
- params.churnFraction > 0) ||
724
- params.parentUpgradeIntervalMs > 0 ||
725
- params.assertMaxOrphans > 0 ||
726
- params.assertMaxOrphanArea > 0 ||
727
- params.assertRecoveryP95Ms > 0 ||
728
- params.assertMaxReparentsPerMin > 0;
729
- const maintenanceController = new AbortController();
730
- const maintenanceSignal = anySignal([
731
- timeoutSignal,
732
- maintenanceController.signal,
733
- ]);
734
- let maintDurationMs = 0;
735
- let maintSamples = 0;
736
- let maintMaxOrphans = 0;
737
- let maintOrphanAreaMs = 0;
738
- const pendingRecoveryStarts = [];
739
- const recoveryDurations = [];
740
- let maintLevelP95DriftMax = 0;
741
- let maintChildrenP95DriftMax = 0;
742
- const reparentBaselineByHash = new Map();
743
- let maintReparentsTotal = 0;
744
- let maintMaxReparentsPerPeer = 0;
745
- // Delivery tracking
746
- const firstBatchMessages = Math.max(0, Math.floor(params.messages));
747
- const secondBatchMessages = Math.max(0, Math.floor(params.secondBatchMessages));
748
- const totalMessages = firstBatchMessages + secondBatchMessages;
749
- const secondBatchStartSeq = firstBatchMessages;
750
- const publishAt = new Map();
751
- const joinedHashList = [...joinedHashes];
752
- const hashToIndex = new Map();
753
- for (let i = 0; i < joinedHashList.length; i++) {
754
- hashToIndex.set(joinedHashList[i], i);
755
- }
756
- const bitsetBytes = Math.ceil(totalMessages / 8);
757
- const receivedBits = joinedHashList.map(() => new Uint8Array(bitsetBytes));
758
- const receivedCounts = new Uint32Array(joinedHashList.length);
759
- const secondBatchReceivedCounts = new Uint32Array(joinedHashList.length);
760
- let duplicates = 0;
761
- let deliveredWithinDeadline = 0;
762
- let deliveredSamples = [];
763
- let secondBatchDeliveredWithinDeadline = 0;
764
- let secondBatchDeliveredSamples = [];
765
- const secondBatchDeliveredSamplesByPeer = joinedHashList.map(() => []);
766
- const sampleCap = Math.max(1, Math.floor(params.maxLatencySamples));
767
- let sampleSeen = 0;
768
- let secondBatchSampleSeen = 0;
769
- const makeOnData = (localHash) => (ev) => {
770
- const d = ev?.detail;
771
- if (!d)
772
- return;
773
- if (d.topic !== params.topic)
774
- return;
775
- if (d.root !== rootId)
776
- return;
777
- const seq = d.seq >>> 0;
778
- const index = hashToIndex.get(localHash);
779
- if (index == null)
780
- return; // not joined / not tracked
781
- if (seq >= totalMessages)
782
- return;
783
- const bits = receivedBits[index];
784
- const byteIndex = seq >>> 3;
785
- const mask = 1 << (seq & 7);
786
- if (byteIndex >= bits.length)
787
- return;
788
- if ((bits[byteIndex] & mask) !== 0) {
789
- duplicates += 1;
790
- return;
791
- }
792
- bits[byteIndex] |= mask;
793
- receivedCounts[index] += 1;
794
- if (seq >= secondBatchStartSeq) {
795
- secondBatchReceivedCounts[index] += 1;
796
- }
797
- const sentAt = publishAt.get(seq);
798
- if (sentAt != null) {
799
- const latency = Date.now() - sentAt;
800
- if (params.deadlineMs > 0 && latency <= params.deadlineMs) {
801
- deliveredWithinDeadline += 1;
802
- }
803
- sampleSeen += 1;
804
- if (deliveredSamples.length < sampleCap) {
805
- deliveredSamples.push(latency);
806
- }
807
- else {
808
- const j = int(rng, sampleSeen);
809
- if (j < sampleCap)
810
- deliveredSamples[j] = latency;
811
- }
812
- if (seq >= secondBatchStartSeq) {
813
- if (params.deadlineMs > 0 && latency <= params.deadlineMs) {
814
- secondBatchDeliveredWithinDeadline += 1;
815
- }
816
- secondBatchDeliveredSamplesByPeer[index].push(latency);
817
- secondBatchSampleSeen += 1;
818
- if (secondBatchDeliveredSamples.length < sampleCap) {
819
- secondBatchDeliveredSamples.push(latency);
820
- }
821
- else {
822
- const j = int(rng, secondBatchSampleSeen);
823
- if (j < sampleCap)
824
- secondBatchDeliveredSamples[j] = latency;
825
- }
826
- }
827
- }
828
- };
829
- for (let i = 0; i < subscriberIndices.length; i++) {
830
- if (!joined[i])
831
- continue;
832
- const idx = subscriberIndices[i];
833
- const node = session.peers[idx].services.fanout;
834
- const localHash = node.publicKeyHash;
835
- node.addEventListener("fanout:data", makeOnData(localHash));
836
- }
837
- // Publish
838
- const payload = new Uint8Array(Math.max(0, params.msgSize));
839
- for (let i = 0; i < payload.length; i++)
840
- payload[i] = i & 0xff;
841
- const quantileFromCounts = (counts, maxValue, total, q) => {
842
- if (total <= 0)
843
- return 0;
844
- const target = Math.min(total - 1, Math.max(0, Math.floor(q * (total - 1))));
845
- let seen = 0;
846
- for (let v = 0; v <= maxValue; v++) {
847
- seen += counts[v] ?? 0;
848
- if (seen > target)
849
- return v;
850
- }
851
- return maxValue;
852
- };
853
- const levelCounts = [];
854
- const childCounts = [];
855
- const sampleMaintenance = (now) => {
856
- let onlineJoined = 0;
857
- let orphansOnline = 0;
858
- let levelsTotal = 0;
859
- let levelsMax = 0;
860
- levelCounts.fill(0);
861
- let childrenTotal = 0;
862
- let childrenMax = 0;
863
- childCounts.fill(0);
864
- for (const idx of joinedSubscriberIndices) {
865
- const peer = session.peers[idx];
866
- if (network.isPeerOffline(peer.peerId, now))
867
- continue;
868
- onlineJoined += 1;
869
- const s = peer.services.fanout.getChannelStats(params.topic, rootId);
870
- if (!s) {
871
- orphansOnline += 1;
872
- continue;
873
- }
874
- if (Number.isFinite(s.level)) {
875
- const lvl = Math.max(0, Math.floor(s.level));
876
- levelsMax = Math.max(levelsMax, lvl);
877
- while (levelCounts.length <= lvl)
878
- levelCounts.push(0);
879
- levelCounts[lvl] = (levelCounts[lvl] ?? 0) + 1;
880
- levelsTotal += 1;
881
- }
882
- if (s.effectiveMaxChildren > 0) {
883
- const c = Math.max(0, Math.floor(s.children));
884
- childrenMax = Math.max(childrenMax, c);
885
- while (childCounts.length <= c)
886
- childCounts.push(0);
887
- childCounts[c] = (childCounts[c] ?? 0) + 1;
888
- childrenTotal += 1;
889
- }
890
- if (s.level > 0 && !s.parent) {
891
- orphansOnline += 1;
892
- }
893
- }
894
- const levelP95 = levelsTotal > 0
895
- ? quantileFromCounts(levelCounts, levelsMax, levelsTotal, 0.95)
896
- : NaN;
897
- const childrenP95 = childrenTotal > 0
898
- ? quantileFromCounts(childCounts, childrenMax, childrenTotal, 0.95)
899
- : NaN;
900
- return {
901
- onlineJoined,
902
- orphansOnline,
903
- levelP95,
904
- childrenP95,
905
- };
906
- };
907
- const maintenanceLoop = async () => {
908
- if (!wantsMaintenance)
909
- return;
910
- const sampleEveryMs = Math.max(100, Math.min(1_000, Math.floor(params.nodes / 10)));
911
- const baselineOrphans = formationTree.treeOrphans;
912
- const baselineLevelP95 = formationTree.treeLevelP95;
913
- const baselineChildrenP95 = formationTree.treeChildrenP95;
914
- let lastAt = Date.now();
915
- let lastOrphans = 0;
916
- const startAt = lastAt;
917
- try {
918
- for (;;) {
919
- if (maintenanceSignal.aborted)
920
- return;
921
- const now = Date.now();
922
- const dt = Math.max(0, now - lastAt);
923
- if (dt > 0) {
924
- maintOrphanAreaMs += lastOrphans * dt;
925
- }
926
- lastAt = now;
927
- const snap = sampleMaintenance(now);
928
- lastOrphans = snap.orphansOnline;
929
- maintMaxOrphans = Math.max(maintMaxOrphans, snap.orphansOnline);
930
- maintSamples += 1;
931
- if (Number.isFinite(snap.levelP95)) {
932
- maintLevelP95DriftMax = Math.max(maintLevelP95DriftMax, Math.abs(snap.levelP95 - baselineLevelP95));
933
- }
934
- if (Number.isFinite(snap.childrenP95)) {
935
- maintChildrenP95DriftMax = Math.max(maintChildrenP95DriftMax, Math.abs(snap.childrenP95 - baselineChildrenP95));
936
- }
937
- if (snap.orphansOnline <= baselineOrphans &&
938
- pendingRecoveryStarts.length > 0) {
939
- for (const s of pendingRecoveryStarts)
940
- recoveryDurations.push(now - s);
941
- pendingRecoveryStarts.length = 0;
942
- }
943
- await delay(sampleEveryMs, { signal: maintenanceSignal });
944
- }
945
- }
946
- finally {
947
- const endAt = Date.now();
948
- const dt = Math.max(0, endAt - lastAt);
949
- if (dt > 0) {
950
- maintOrphanAreaMs += lastOrphans * dt;
951
- }
952
- maintDurationMs = Math.max(0, endAt - startAt);
953
- for (const s of pendingRecoveryStarts)
954
- recoveryDurations.push(endAt - s);
955
- pendingRecoveryStarts.length = 0;
956
- }
957
- };
958
- const churnLoop = async () => {
959
- const everyMs = Math.max(0, Math.floor(params.churnEveryMs));
960
- const downMs = Math.max(0, Math.floor(params.churnDownMs));
961
- const fraction = Math.max(0, Math.min(1, Number(params.churnFraction)));
962
- if (everyMs <= 0 || downMs <= 0 || fraction <= 0)
963
- return;
964
- if (joinedSubscriberIndices.length === 0)
965
- return;
966
- for (;;) {
967
- if (churnSignal.aborted)
968
- return;
969
- await delay(everyMs, { signal: churnSignal });
970
- if (churnSignal.aborted)
971
- return;
972
- const target = Math.min(joinedSubscriberIndices.length, Math.max(1, Math.floor(joinedSubscriberIndices.length * fraction)));
973
- const chosen = new Set();
974
- const maxAttempts = Math.max(10, target * 20);
975
- for (let tries = 0; chosen.size < target && tries < maxAttempts; tries++) {
976
- const idx = joinedSubscriberIndices[int(rng, joinedSubscriberIndices.length)];
977
- const peer = session.peers[idx];
978
- if (network.isPeerOffline(peer.peerId))
979
- continue;
980
- chosen.add(idx);
981
- }
982
- if (chosen.size === 0)
983
- continue;
984
- churnEvents += 1;
985
- churnedPeersTotal += chosen.size;
986
- const now = Date.now();
987
- if (wantsMaintenance)
988
- pendingRecoveryStarts.push(now);
989
- await Promise.all([...chosen].map(async (idx) => {
990
- const peer = session.peers[idx];
991
- network.setPeerOffline(peer.peerId, downMs, now);
992
- await network.disconnectPeer(peer.peerId);
993
- }));
994
- }
995
- };
996
- const collectParentUpgradeActivity = () => {
997
- let reparentUpgrade = 0;
998
- let reparentUpgradeSkipData = 0;
999
- let reparentUpgradeSkipRepair = 0;
1000
- let reparentUpgradeSkipQuiet = 0;
1001
- let parentProbeReqSent = 0;
1002
- let parentShadowStart = 0;
1003
- let parentShadowPromote = 0;
1004
- for (const p of session.peers) {
1005
- const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
1006
- reparentUpgrade += m.reparentUpgrade;
1007
- reparentUpgradeSkipData += m.reparentUpgradeSkipData;
1008
- reparentUpgradeSkipRepair += m.reparentUpgradeSkipRepair;
1009
- reparentUpgradeSkipQuiet += m.reparentUpgradeSkipQuiet;
1010
- parentProbeReqSent += m.parentProbeReqSent;
1011
- parentShadowStart += m.parentShadowStart;
1012
- parentShadowPromote += m.parentShadowPromote;
1013
- }
1014
- return {
1015
- reparentUpgrade,
1016
- reparentUpgradeSkipData,
1017
- reparentUpgradeSkipRepair,
1018
- reparentUpgradeSkipQuiet,
1019
- parentProbeReqSent,
1020
- parentShadowStart,
1021
- parentShadowPromote,
1022
- };
1023
- };
1024
- const diffParentUpgradeActivity = (after, before) => ({
1025
- reparentUpgrade: Math.max(0, after.reparentUpgrade - before.reparentUpgrade),
1026
- reparentUpgradeSkipData: Math.max(0, after.reparentUpgradeSkipData - before.reparentUpgradeSkipData),
1027
- reparentUpgradeSkipRepair: Math.max(0, after.reparentUpgradeSkipRepair - before.reparentUpgradeSkipRepair),
1028
- reparentUpgradeSkipQuiet: Math.max(0, after.reparentUpgradeSkipQuiet - before.reparentUpgradeSkipQuiet),
1029
- parentProbeReqSent: Math.max(0, after.parentProbeReqSent - before.parentProbeReqSent),
1030
- parentShadowStart: Math.max(0, after.parentShadowStart - before.parentShadowStart),
1031
- parentShadowPromote: Math.max(0, after.parentShadowPromote - before.parentShadowPromote),
1032
- });
1033
- const publishStartParentUpgrade = collectParentUpgradeActivity();
1034
- let publishActiveParentUpgrade = diffParentUpgradeActivity(publishStartParentUpgrade, publishStartParentUpgrade);
1035
- const publishStart = Date.now();
1036
- let publishActiveMs = 0;
1037
- if (wantsMaintenance) {
1038
- for (const p of session.peers) {
1039
- const nodeHash = p.services.fanout.publicKeyHash;
1040
- const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
1041
- reparentBaselineByHash.set(nodeHash, m.reparentDisconnect +
1042
- m.reparentStale +
1043
- m.reparentKicked +
1044
- m.reparentUpgrade);
1045
- }
1046
- }
1047
- const maintenancePromise = maintenanceLoop().catch(() => { });
1048
- const churnPromise = churnLoop().catch(() => { });
1049
- const lateAfterMs = Math.max(-1, Math.floor(params.lateRootConnectAfterMs));
1050
- let lateRootApplied = false;
1051
- const applyLateRootTopology = async () => {
1052
- if (lateRootApplied || lateAfterMs < 0)
1053
- return;
1054
- lateRootApplied = true;
1055
- const lateMaxChildren = Math.max(0, Math.floor(params.lateRootMaxChildren));
1056
- if (lateMaxChildren > 0) {
1057
- const id = root.getChannelId(params.topic, rootId);
1058
- const ch = root.channelsBySuffixKey?.get?.(id.suffixKey);
1059
- if (ch) {
1060
- ch.maxChildren = Math.max(ch.maxChildren ?? 0, lateMaxChildren);
1061
- const uploadBoundedMaxChildren = effectiveMaxChildrenForUpload(params, ch, lateMaxChildren);
1062
- ch.effectiveMaxChildren = Math.max(ch.effectiveMaxChildren ?? 0, uploadBoundedMaxChildren);
1063
- void root
1064
- .announceToTrackers?.(ch, timeoutSignal)
1065
- ?.catch?.(() => { });
1066
- }
1067
- }
1068
- const fraction = Math.max(0, Math.min(1, Number(params.lateRootConnectFraction)));
1069
- const target = Math.min(joinedSubscriberIndices.length, Math.max(0, Math.ceil(joinedSubscriberIndices.length * fraction)));
1070
- for (let i = 0; i < target; i++) {
1071
- const idx = joinedSubscriberIndices[i];
1072
- if (idx === rootIndex)
1073
- continue;
1074
- try {
1075
- await session.peers[idx].dial(session.peers[rootIndex].getMultiaddrs());
1076
- }
1077
- catch {
1078
- // best-effort late underlay shortcut
1079
- }
1080
- }
1081
- };
1082
- const lateRootDuringPublishPromise = params.lateRootDuringPublish && lateAfterMs >= 0
1083
- ? (async () => {
1084
- if (lateAfterMs > 0) {
1085
- await delay(lateAfterMs, { signal: timeoutSignal });
1086
- }
1087
- await applyLateRootTopology();
1088
- })().catch((error) => {
1089
- if (!timeoutSignal.aborted)
1090
- throw error;
1091
- })
1092
- : undefined;
1093
- try {
1094
- for (let seq = 0; seq < firstBatchMessages; seq++) {
1095
- if (timeoutSignal.aborted) {
1096
- throw timeoutSignal.reason ?? new Error("fanout-tree-sim aborted");
1097
- }
1098
- publishAt.set(seq, Date.now());
1099
- await root.publishData(params.topic, rootId, payload);
1100
- if (params.intervalMs > 0) {
1101
- await delay(params.intervalMs, { signal: timeoutSignal });
1102
- }
1103
- }
1104
- }
1105
- finally {
1106
- if (firstBatchMessages > 0) {
1107
- publishActiveMs += Math.max(0, Date.now() - publishStart);
1108
- }
1109
- publishActiveParentUpgrade = diffParentUpgradeActivity(collectParentUpgradeActivity(), publishStartParentUpgrade);
1110
- churnController.abort();
1111
- await churnPromise;
1112
- churnSignal.clear?.();
1113
- await lateRootDuringPublishPromise;
1114
- }
1115
- let publishDone = Date.now();
1116
- // Signal end-of-stream so subscribers can detect tail gaps and repair.
1117
- if (params.repair && firstBatchMessages > 0) {
1118
- await root.publishEnd(params.topic, rootId, firstBatchMessages);
1119
- }
1120
- if (params.settleMs > 0) {
1121
- if (!params.lateRootDuringPublish &&
1122
- lateAfterMs >= 0 &&
1123
- lateAfterMs < params.settleMs) {
1124
- if (lateAfterMs > 0) {
1125
- await delay(lateAfterMs, { signal: timeoutSignal });
1126
- }
1127
- await applyLateRootTopology();
1128
- const remaining = Math.max(0, params.settleMs - lateAfterMs);
1129
- if (remaining > 0) {
1130
- await delay(remaining, { signal: timeoutSignal });
1131
- }
1132
- }
1133
- else {
1134
- await delay(params.settleMs, { signal: timeoutSignal });
1135
- }
1136
- }
1137
- if (secondBatchMessages > 0) {
1138
- const secondPublishStart = Date.now();
1139
- for (let seq = secondBatchStartSeq; seq < totalMessages; seq++) {
1140
- if (timeoutSignal.aborted) {
1141
- throw timeoutSignal.reason ?? new Error("fanout-tree-sim aborted");
1142
- }
1143
- publishAt.set(seq, Date.now());
1144
- await root.publishData(params.topic, rootId, payload);
1145
- if (params.intervalMs > 0) {
1146
- await delay(params.intervalMs, { signal: timeoutSignal });
1147
- }
1148
- }
1149
- publishDone = Date.now();
1150
- publishActiveMs += Math.max(0, publishDone - secondPublishStart);
1151
- if (params.repair) {
1152
- await root.publishEnd(params.topic, rootId, totalMessages);
1153
- }
1154
- const secondSettleMs = Math.max(0, Math.floor(params.secondBatchSettleMs));
1155
- if (secondSettleMs > 0) {
1156
- await delay(secondSettleMs, { signal: timeoutSignal });
1157
- }
1158
- }
1159
- maintenanceController.abort();
1160
- await maintenancePromise;
1161
- maintenanceSignal.clear?.();
1162
- recoveryDurations.sort((a, b) => a - b);
1163
- const maintRecoveryCount = recoveryDurations.length;
1164
- const maintRecoveryP50Ms = maintRecoveryCount > 0 ? quantile(recoveryDurations, 0.5) : 0;
1165
- const maintRecoveryP95Ms = maintRecoveryCount > 0 ? quantile(recoveryDurations, 0.95) : 0;
1166
- if (wantsMaintenance) {
1167
- for (const p of session.peers) {
1168
- const nodeHash = p.services.fanout.publicKeyHash;
1169
- const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
1170
- const total = m.reparentDisconnect +
1171
- m.reparentStale +
1172
- m.reparentKicked +
1173
- m.reparentUpgrade;
1174
- const base = reparentBaselineByHash.get(nodeHash) ?? 0;
1175
- const delta = Math.max(0, total - base);
1176
- maintReparentsTotal += delta;
1177
- maintMaxReparentsPerPeer = Math.max(maintMaxReparentsPerPeer, delta);
1178
- }
1179
- }
1180
- const maintDurationMin = maintDurationMs / 60_000;
1181
- const maintReparentsPerMin = maintDurationMin > 0 ? maintReparentsTotal / maintDurationMin : 0;
1182
- const maintOrphanArea = maintOrphanAreaMs / 1_000;
1183
- // Compute delivery
1184
- const expected = joinedCount * totalMessages;
1185
- let delivered = 0;
1186
- for (const c of receivedCounts)
1187
- delivered += c;
1188
- const secondBatchExpected = joinedCount * secondBatchMessages;
1189
- let secondBatchDelivered = 0;
1190
- for (const c of secondBatchReceivedCounts)
1191
- secondBatchDelivered += c;
1192
- const joinedPct = subscriberCount === 0 ? 100 : (100 * joinedCount) / subscriberCount;
1193
- const deliveredPct = expected === 0 ? 100 : (100 * delivered) / expected;
1194
- const deliveredWithinDeadlinePct = expected === 0 ? 100 : (100 * deliveredWithinDeadline) / expected;
1195
- const secondBatchDeliveredPct = secondBatchExpected === 0
1196
- ? 100
1197
- : (100 * secondBatchDelivered) / secondBatchExpected;
1198
- const secondBatchDeliveredWithinDeadlinePct = secondBatchExpected === 0
1199
- ? 100
1200
- : (100 * secondBatchDeliveredWithinDeadline) / secondBatchExpected;
1201
- // Internal protocol drops (from upload shaping / overload logic)
1202
- let droppedForwardsTotal = 0;
1203
- let droppedForwardsMax = 0;
1204
- let droppedForwardsMaxNode;
1205
- for (const p of session.peers) {
1206
- const s = p.services.fanout.getChannelStats(params.topic, rootId);
1207
- if (!s)
1208
- continue;
1209
- droppedForwardsTotal += s.droppedForwards;
1210
- if (s.droppedForwards > droppedForwardsMax) {
1211
- droppedForwardsMax = s.droppedForwards;
1212
- droppedForwardsMaxNode = p.services.fanout.publicKeyHash;
1213
- }
1214
- }
1215
- // Tree shape stats (best-effort)
1216
- const tree = computeTreeShapeStats();
1217
- const treeMaxLevel = tree.treeMaxLevel;
1218
- const treeLevelP95 = tree.treeLevelP95;
1219
- const treeLevelAvg = tree.treeLevelAvg;
1220
- const treeOrphans = tree.treeOrphans;
1221
- const treeChildrenP95 = tree.treeChildrenP95;
1222
- const treeChildrenMax = tree.treeChildrenMax;
1223
- const treeRootChildren = tree.treeRootChildren;
1224
- // Stream backpressure stats (queued bytes)
1225
- const queuedBytesSamples = [];
1226
- let streamQueuedBytesTotal = 0;
1227
- let streamQueuedBytesMax = 0;
1228
- let streamQueuedBytesMaxNode;
1229
- for (const p of session.peers) {
1230
- const q = Math.max(0, Math.floor(p.services.fanout.getQueuedBytes()));
1231
- streamQueuedBytesTotal += q;
1232
- queuedBytesSamples.push(q);
1233
- if (q > streamQueuedBytesMax) {
1234
- streamQueuedBytesMax = q;
1235
- streamQueuedBytesMaxNode = p.services.fanout.publicKeyHash;
1236
- }
1237
- }
1238
- queuedBytesSamples.sort((a, b) => a - b);
1239
- const streamQueuedBytesP95 = queuedBytesSamples.length > 0 ? quantile(queuedBytesSamples, 0.95) : 0;
1240
- const streamQueuedBytesByLane = [];
1241
- for (const p of session.peers) {
1242
- for (const ps of p.services.fanout.peers.values()) {
1243
- const byLane =
1244
- // @ts-ignore - optional debug helper (may not exist in built typings yet)
1245
- ps.getOutboundQueuedBytesByLane?.() ?? [0, 0, 0, 0];
1246
- for (let lane = 0; lane < byLane.length; lane++) {
1247
- streamQueuedBytesByLane[lane] =
1248
- (streamQueuedBytesByLane[lane] ?? 0) + (byLane[lane] ?? 0);
1249
- }
1250
- }
1251
- }
1252
- for (let lane = 0; lane < 4; lane++) {
1253
- streamQueuedBytesByLane[lane] = streamQueuedBytesByLane[lane] ?? 0;
1254
- }
1255
- // Peak upload vs cap (best-effort; counts framed bytes, including overhead)
1256
- const uploadCapByHash = new Map();
1257
- if (params.rootUploadLimitBps > 0) {
1258
- uploadCapByHash.set(root.publicKeyHash, params.rootUploadLimitBps);
1259
- }
1260
- for (let i = 0; i < subscriberIndices.length; i++) {
1261
- if (!joined[i])
1262
- continue;
1263
- const idx = subscriberIndices[i];
1264
- if (!relaySet.has(idx))
1265
- continue;
1266
- if (params.relayUploadLimitBps <= 0)
1267
- continue;
1268
- const h = session.peers[idx].services.fanout.publicKeyHash;
1269
- uploadCapByHash.set(h, params.relayUploadLimitBps);
1270
- }
1271
- let maxUploadFracPct = 0;
1272
- let maxUploadNode;
1273
- let maxUploadBps = 0;
1274
- const rootUploadBps = session.network.peerMetricsByHash.get(root.publicKeyHash)
1275
- ?.maxBytesPerSecond ?? 0;
1276
- const rootUploadFracPct = params.rootUploadLimitBps > 0
1277
- ? (100 * rootUploadBps) / params.rootUploadLimitBps
1278
- : 0;
1279
- for (const [hash, pm] of session.network.peerMetricsByHash) {
1280
- const cap = uploadCapByHash.get(hash);
1281
- if (!cap || cap <= 0)
1282
- continue;
1283
- const frac = (100 * pm.maxBytesPerSecond) / cap;
1284
- if (frac > maxUploadFracPct) {
1285
- maxUploadFracPct = frac;
1286
- maxUploadNode = hash;
1287
- maxUploadBps = pm.maxBytesPerSecond;
1288
- }
1289
- }
1290
- deliveredSamples.sort((a, b) => a - b);
1291
- secondBatchDeliveredSamples.sort((a, b) => a - b);
1292
- const secondBatchLatencyP95ByHash = {};
1293
- for (let i = 0; i < joinedHashList.length; i++) {
1294
- const samples = secondBatchDeliveredSamplesByPeer[i];
1295
- if (samples.length === 0)
1296
- continue;
1297
- samples.sort((a, b) => a - b);
1298
- secondBatchLatencyP95ByHash[joinedHashList[i]] = quantile(samples, 0.95);
1299
- }
1300
- let protocolControlSends = 0;
1301
- let protocolControlBytesSent = 0;
1302
- let protocolControlBytesSentJoin = 0;
1303
- let protocolControlBytesSentRepair = 0;
1304
- let protocolControlBytesSentTracker = 0;
1305
- let protocolControlReceives = 0;
1306
- let protocolControlBytesReceived = 0;
1307
- let protocolDataSends = 0;
1308
- let protocolDataPayloadBytesSent = 0;
1309
- let protocolDataReceives = 0;
1310
- let protocolDataPayloadBytesReceived = 0;
1311
- let protocolRepairReqSent = 0;
1312
- let protocolFetchReqSent = 0;
1313
- let protocolIHaveSent = 0;
1314
- let protocolTrackerAnnounceSent = 0;
1315
- let protocolTrackerQuerySent = 0;
1316
- let protocolTrackerReplySent = 0;
1317
- let protocolTrackerFeedbackSent = 0;
1318
- let protocolCacheHitsServed = 0;
1319
- let protocolHoleFillsFromNeighbor = 0;
1320
- let protocolRouteCacheHits = 0;
1321
- let protocolRouteCacheMisses = 0;
1322
- let protocolRouteCacheExpirations = 0;
1323
- let protocolRouteCacheEvictions = 0;
1324
- let protocolRouteProxyQueries = 0;
1325
- let protocolRouteProxyTimeouts = 0;
1326
- let protocolRouteProxyFanout = 0;
1327
- let staleForwardsDroppedTotal = 0;
1328
- let staleForwardsDroppedMax = 0;
1329
- let staleForwardsDroppedMaxNode;
1330
- let dataWriteDropsTotal = 0;
1331
- let dataWriteDropsMax = 0;
1332
- let dataWriteDropsMaxNode;
1333
- let reparentDisconnectTotal = 0;
1334
- let reparentStaleTotal = 0;
1335
- let reparentKickedTotal = 0;
1336
- let reparentUpgradeTotal = 0;
1337
- const upgradedPeerHashes = [];
1338
- let reparentUpgradeSkipLeafTotal = 0;
1339
- let reparentUpgradeSkipRepairTotal = 0;
1340
- let reparentUpgradeSkipDataTotal = 0;
1341
- let reparentUpgradeSkipCooldownTotal = 0;
1342
- let reparentUpgradeSkipQuietTotal = 0;
1343
- let reparentUpgradeSkipBudgetTotal = 0;
1344
- let reparentUpgradeSkipCandidateLevelTotal = 0;
1345
- let reparentUpgradeSkipCandidateSlotsTotal = 0;
1346
- let reparentUpgradeSkipCandidatePressureTotal = 0;
1347
- let reparentUpgradeSkipRootPressureTotal = 0;
1348
- let reparentUpgradeSkipProbeNoReplyTotal = 0;
1349
- let reparentUpgradeSkipProbeNotRootedTotal = 0;
1350
- let reparentUpgradeSkipProbeRepairTotal = 0;
1351
- let reparentUpgradeSkipProbeLagTotal = 0;
1352
- let reparentUpgradeSkipProbeOverloadedTotal = 0;
1353
- let reparentUpgradeSkipProbeCooldownTotal = 0;
1354
- let parentProbeReqSentTotal = 0;
1355
- let parentProbeReqReceivedTotal = 0;
1356
- let parentProbeReplySentTotal = 0;
1357
- let parentProbeReplyReceivedTotal = 0;
1358
- let parentUpgradeRootReservationCreatedTotal = 0;
1359
- let parentUpgradeRootReservationConsumedTotal = 0;
1360
- let parentUpgradeRootReservationRejectedTotal = 0;
1361
- let parentUpgradeRootReservationMarginRejectedTotal = 0;
1362
- let parentUpgradeRootReservationBlockedTotal = 0;
1363
- let parentUpgradeRootReservationExpiredTotal = 0;
1364
- let parentShadowStartTotal = 0;
1365
- let parentShadowObserveTotal = 0;
1366
- let parentShadowPromoteTotal = 0;
1367
- let parentShadowResetTotal = 0;
1368
- let parentShadowRejectNoReplyTotal = 0;
1369
- let parentShadowRejectNotRootedTotal = 0;
1370
- let parentShadowRejectCapacityTotal = 0;
1371
- let parentShadowRejectRepairTotal = 0;
1372
- let parentShadowRejectLagTotal = 0;
1373
- let parentShadowRejectOverloadedTotal = 0;
1374
- let parentShadowRejectLevelTotal = 0;
1375
- let earningsTotal = 0;
1376
- const earningsByHash = new Map();
1377
- for (const p of session.peers) {
1378
- const nodeHash = p.services.fanout.publicKeyHash;
1379
- const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
1380
- staleForwardsDroppedTotal += m.staleForwardsDropped;
1381
- if (m.staleForwardsDropped > staleForwardsDroppedMax) {
1382
- staleForwardsDroppedMax = m.staleForwardsDropped;
1383
- staleForwardsDroppedMaxNode = nodeHash;
1384
- }
1385
- dataWriteDropsTotal += m.dataWriteDrops;
1386
- if (m.dataWriteDrops > dataWriteDropsMax) {
1387
- dataWriteDropsMax = m.dataWriteDrops;
1388
- dataWriteDropsMaxNode = nodeHash;
1389
- }
1390
- reparentDisconnectTotal += m.reparentDisconnect;
1391
- reparentStaleTotal += m.reparentStale;
1392
- reparentKickedTotal += m.reparentKicked;
1393
- reparentUpgradeTotal += m.reparentUpgrade;
1394
- if (m.reparentUpgrade > 0)
1395
- upgradedPeerHashes.push(nodeHash);
1396
- reparentUpgradeSkipLeafTotal += m.reparentUpgradeSkipLeaf;
1397
- reparentUpgradeSkipRepairTotal += m.reparentUpgradeSkipRepair;
1398
- reparentUpgradeSkipDataTotal += m.reparentUpgradeSkipData;
1399
- reparentUpgradeSkipCooldownTotal += m.reparentUpgradeSkipCooldown;
1400
- reparentUpgradeSkipQuietTotal += m.reparentUpgradeSkipQuiet;
1401
- reparentUpgradeSkipBudgetTotal += m.reparentUpgradeSkipBudget;
1402
- reparentUpgradeSkipCandidateLevelTotal +=
1403
- m.reparentUpgradeSkipCandidateLevel;
1404
- reparentUpgradeSkipCandidateSlotsTotal +=
1405
- m.reparentUpgradeSkipCandidateSlots;
1406
- reparentUpgradeSkipCandidatePressureTotal +=
1407
- m.reparentUpgradeSkipCandidatePressure;
1408
- reparentUpgradeSkipRootPressureTotal +=
1409
- m.reparentUpgradeSkipRootPressure;
1410
- reparentUpgradeSkipProbeNoReplyTotal +=
1411
- m.reparentUpgradeSkipProbeNoReply;
1412
- reparentUpgradeSkipProbeNotRootedTotal +=
1413
- m.reparentUpgradeSkipProbeNotRooted;
1414
- reparentUpgradeSkipProbeRepairTotal += m.reparentUpgradeSkipProbeRepair;
1415
- reparentUpgradeSkipProbeLagTotal += m.reparentUpgradeSkipProbeLag;
1416
- reparentUpgradeSkipProbeOverloadedTotal +=
1417
- m.reparentUpgradeSkipProbeOverloaded;
1418
- reparentUpgradeSkipProbeCooldownTotal +=
1419
- m.reparentUpgradeSkipProbeCooldown;
1420
- parentProbeReqSentTotal += m.parentProbeReqSent;
1421
- parentProbeReqReceivedTotal += m.parentProbeReqReceived;
1422
- parentProbeReplySentTotal += m.parentProbeReplySent;
1423
- parentProbeReplyReceivedTotal += m.parentProbeReplyReceived;
1424
- parentUpgradeRootReservationCreatedTotal +=
1425
- m.parentUpgradeRootReservationCreated;
1426
- parentUpgradeRootReservationConsumedTotal +=
1427
- m.parentUpgradeRootReservationConsumed;
1428
- parentUpgradeRootReservationRejectedTotal +=
1429
- m.parentUpgradeRootReservationRejected;
1430
- parentUpgradeRootReservationMarginRejectedTotal +=
1431
- m.parentUpgradeRootReservationMarginRejected;
1432
- parentUpgradeRootReservationBlockedTotal +=
1433
- m.parentUpgradeRootReservationBlocked;
1434
- parentUpgradeRootReservationExpiredTotal +=
1435
- m.parentUpgradeRootReservationExpired;
1436
- parentShadowStartTotal += m.parentShadowStart;
1437
- parentShadowObserveTotal += m.parentShadowObserve;
1438
- parentShadowPromoteTotal += m.parentShadowPromote;
1439
- parentShadowResetTotal += m.parentShadowReset;
1440
- parentShadowRejectNoReplyTotal += m.parentShadowRejectNoReply;
1441
- parentShadowRejectNotRootedTotal += m.parentShadowRejectNotRooted;
1442
- parentShadowRejectCapacityTotal += m.parentShadowRejectCapacity;
1443
- parentShadowRejectRepairTotal += m.parentShadowRejectRepair;
1444
- parentShadowRejectLagTotal += m.parentShadowRejectLag;
1445
- parentShadowRejectOverloadedTotal += m.parentShadowRejectOverloaded;
1446
- parentShadowRejectLevelTotal += m.parentShadowRejectLevel;
1447
- earningsTotal += m.earnings;
1448
- earningsByHash.set(nodeHash, m.earnings);
1449
- protocolControlSends += m.controlSends;
1450
- protocolControlBytesSent += m.controlBytesSent;
1451
- protocolControlBytesSentJoin += m.controlBytesSentJoin;
1452
- protocolControlBytesSentRepair += m.controlBytesSentRepair;
1453
- protocolControlBytesSentTracker += m.controlBytesSentTracker;
1454
- protocolControlReceives += m.controlReceives;
1455
- protocolControlBytesReceived += m.controlBytesReceived;
1456
- protocolDataSends += m.dataSends;
1457
- protocolDataPayloadBytesSent += m.dataPayloadBytesSent;
1458
- protocolDataReceives += m.dataReceives;
1459
- protocolDataPayloadBytesReceived += m.dataPayloadBytesReceived;
1460
- protocolRepairReqSent += m.repairReqSent;
1461
- protocolFetchReqSent += m.fetchReqSent;
1462
- protocolIHaveSent += m.ihaveSent;
1463
- protocolTrackerAnnounceSent += m.trackerAnnounceSent;
1464
- protocolTrackerQuerySent += m.trackerQuerySent;
1465
- protocolTrackerReplySent += m.trackerReplySent;
1466
- protocolTrackerFeedbackSent += m.trackerFeedbackSent;
1467
- protocolCacheHitsServed += m.cacheHitsServed;
1468
- protocolHoleFillsFromNeighbor += m.holeFillsFromNeighbor;
1469
- protocolRouteCacheHits += m.routeCacheHits;
1470
- protocolRouteCacheMisses += m.routeCacheMisses;
1471
- protocolRouteCacheExpirations += m.routeCacheExpirations;
1472
- protocolRouteCacheEvictions += m.routeCacheEvictions;
1473
- protocolRouteProxyQueries += m.routeProxyQueries;
1474
- protocolRouteProxyTimeouts += m.routeProxyTimeouts;
1475
- protocolRouteProxyFanout += m.routeProxyFanout;
1476
- }
1477
- const upgradedBranchHashSet = new Set();
1478
- for (const hash of upgradedPeerHashes) {
1479
- const stack = [hash];
1480
- while (stack.length > 0) {
1481
- const next = stack.pop();
1482
- if (upgradedBranchHashSet.has(next))
1483
- continue;
1484
- if (joinedHashes.has(next))
1485
- upgradedBranchHashSet.add(next);
1486
- for (const child of tree.childrenByHash.get(next) ?? []) {
1487
- stack.push(child);
1488
- }
1489
- }
1490
- }
1491
- const upgradedBranchPeerHashes = [...upgradedBranchHashSet];
1492
- const relayHashes = [...uploadCapByHash.keys()];
1493
- const relayEarnings = relayHashes
1494
- .map((h) => earningsByHash.get(h) ?? 0)
1495
- .sort((a, b) => a - b);
1496
- const earningsRelayCount = relayEarnings.length;
1497
- const earningsRelayP50 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.5) : 0;
1498
- const earningsRelayP95 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.95) : 0;
1499
- const earningsRelayMax = earningsRelayCount > 0 ? relayEarnings[relayEarnings.length - 1] : 0;
1500
- const idealPayloadBytes = expected * Math.max(0, params.msgSize);
1501
- const overheadFactorData = idealPayloadBytes <= 0
1502
- ? 1
1503
- : protocolDataPayloadBytesSent / idealPayloadBytes;
1504
- const deliveredPayloadBytes = delivered * Math.max(0, params.msgSize);
1505
- const controlBpp = deliveredPayloadBytes <= 0
1506
- ? 0
1507
- : protocolControlBytesSent / deliveredPayloadBytes;
1508
- const trackerBpp = deliveredPayloadBytes <= 0
1509
- ? 0
1510
- : protocolControlBytesSentTracker / deliveredPayloadBytes;
1511
- const repairBpp = deliveredPayloadBytes <= 0
1512
- ? 0
1513
- : protocolControlBytesSentRepair / deliveredPayloadBytes;
1514
- return {
1515
- params,
1516
- bootstrapCount: bootstrapIndices.length,
1517
- subscriberCount,
1518
- relayCount: relaySet.size,
1519
- joinedCount,
1520
- joinedPct,
1521
- joinMs: joinDone - joinStart,
1522
- attachSamples,
1523
- attachP50,
1524
- attachP95,
1525
- attachP99,
1526
- attachMax,
1527
- formationTreeMaxLevel: formationTree.treeMaxLevel,
1528
- formationTreeLevelP95: formationTree.treeLevelP95,
1529
- formationTreeLevelAvg: formationTree.treeLevelAvg,
1530
- formationTreeOrphans: formationTree.treeOrphans,
1531
- formationTreeChildrenP95: formationTree.treeChildrenP95,
1532
- formationTreeChildrenMax: formationTree.treeChildrenMax,
1533
- formationTreeRootChildren: formationTree.treeRootChildren,
1534
- formationUnderlayEdges,
1535
- formationUnderlayDistP95,
1536
- formationUnderlayDistMax,
1537
- formationStretchP95,
1538
- formationStretchMax,
1539
- formationScore,
1540
- publishMs: publishActiveMs,
1541
- expected,
1542
- delivered,
1543
- deliveredPct,
1544
- deliveredWithinDeadline,
1545
- deliveredWithinDeadlinePct,
1546
- duplicates,
1547
- secondBatchExpected,
1548
- secondBatchDelivered,
1549
- secondBatchDeliveredPct,
1550
- secondBatchDeliveredWithinDeadline,
1551
- secondBatchDeliveredWithinDeadlinePct,
1552
- secondBatchLatencySamples: secondBatchDeliveredSamples.length,
1553
- secondBatchLatencyP50: quantile(secondBatchDeliveredSamples, 0.5),
1554
- secondBatchLatencyP95: quantile(secondBatchDeliveredSamples, 0.95),
1555
- secondBatchLatencyP99: quantile(secondBatchDeliveredSamples, 0.99),
1556
- secondBatchLatencyMax: secondBatchDeliveredSamples[secondBatchDeliveredSamples.length - 1] ??
1557
- NaN,
1558
- secondBatchLatencyP95ByHash,
1559
- latencySamples: deliveredSamples.length,
1560
- latencyP50: quantile(deliveredSamples, 0.5),
1561
- latencyP95: quantile(deliveredSamples, 0.95),
1562
- latencyP99: quantile(deliveredSamples, 0.99),
1563
- latencyMax: deliveredSamples[deliveredSamples.length - 1] ?? NaN,
1564
- droppedForwardsTotal,
1565
- droppedForwardsMax,
1566
- droppedForwardsMaxNode,
1567
- staleForwardsDroppedTotal,
1568
- staleForwardsDroppedMax,
1569
- staleForwardsDroppedMaxNode,
1570
- dataWriteDropsTotal,
1571
- dataWriteDropsMax,
1572
- dataWriteDropsMaxNode,
1573
- reparentDisconnectTotal,
1574
- reparentStaleTotal,
1575
- reparentKickedTotal,
1576
- publishActiveReparentUpgradeTotal: publishActiveParentUpgrade.reparentUpgrade,
1577
- publishActiveReparentUpgradeSkipDataTotal: publishActiveParentUpgrade.reparentUpgradeSkipData,
1578
- publishActiveReparentUpgradeSkipRepairTotal: publishActiveParentUpgrade.reparentUpgradeSkipRepair,
1579
- publishActiveReparentUpgradeSkipQuietTotal: publishActiveParentUpgrade.reparentUpgradeSkipQuiet,
1580
- publishActiveParentProbeReqSentTotal: publishActiveParentUpgrade.parentProbeReqSent,
1581
- publishActiveParentShadowStartTotal: publishActiveParentUpgrade.parentShadowStart,
1582
- publishActiveParentShadowPromoteTotal: publishActiveParentUpgrade.parentShadowPromote,
1583
- reparentUpgradeTotal,
1584
- upgradedPeerHashes,
1585
- upgradedBranchPeerHashes,
1586
- reparentUpgradeSkipLeafTotal,
1587
- reparentUpgradeSkipRepairTotal,
1588
- reparentUpgradeSkipDataTotal,
1589
- reparentUpgradeSkipCooldownTotal,
1590
- reparentUpgradeSkipQuietTotal,
1591
- reparentUpgradeSkipBudgetTotal,
1592
- reparentUpgradeSkipCandidateLevelTotal,
1593
- reparentUpgradeSkipCandidateSlotsTotal,
1594
- reparentUpgradeSkipCandidatePressureTotal,
1595
- reparentUpgradeSkipRootPressureTotal,
1596
- reparentUpgradeSkipProbeNoReplyTotal,
1597
- reparentUpgradeSkipProbeNotRootedTotal,
1598
- reparentUpgradeSkipProbeRepairTotal,
1599
- reparentUpgradeSkipProbeLagTotal,
1600
- reparentUpgradeSkipProbeOverloadedTotal,
1601
- reparentUpgradeSkipProbeCooldownTotal,
1602
- parentProbeReqSentTotal,
1603
- parentProbeReqReceivedTotal,
1604
- parentProbeReplySentTotal,
1605
- parentProbeReplyReceivedTotal,
1606
- parentUpgradeRootReservationCreatedTotal,
1607
- parentUpgradeRootReservationConsumedTotal,
1608
- parentUpgradeRootReservationRejectedTotal,
1609
- parentUpgradeRootReservationMarginRejectedTotal,
1610
- parentUpgradeRootReservationBlockedTotal,
1611
- parentUpgradeRootReservationExpiredTotal,
1612
- parentShadowStartTotal,
1613
- parentShadowObserveTotal,
1614
- parentShadowPromoteTotal,
1615
- parentShadowResetTotal,
1616
- parentShadowRejectNoReplyTotal,
1617
- parentShadowRejectNotRootedTotal,
1618
- parentShadowRejectCapacityTotal,
1619
- parentShadowRejectRepairTotal,
1620
- parentShadowRejectLagTotal,
1621
- parentShadowRejectOverloadedTotal,
1622
- parentShadowRejectLevelTotal,
1623
- treeMaxLevel,
1624
- treeLevelP95,
1625
- treeLevelAvg,
1626
- treeOrphans,
1627
- treeChildrenP95,
1628
- treeChildrenMax,
1629
- treeRootChildren,
1630
- maxUploadBps,
1631
- maxUploadFracPct,
1632
- maxUploadNode,
1633
- rootUploadBps,
1634
- rootUploadFracPct,
1635
- streamQueuedBytesTotal,
1636
- streamQueuedBytesMax,
1637
- streamQueuedBytesP95,
1638
- streamQueuedBytesMaxNode,
1639
- streamQueuedBytesByLane,
1640
- churnEvents,
1641
- churnedPeersTotal,
1642
- maintDurationMs,
1643
- maintSamples,
1644
- maintMaxOrphans,
1645
- maintOrphanArea,
1646
- maintRecoveryCount,
1647
- maintRecoveryP50Ms,
1648
- maintRecoveryP95Ms,
1649
- maintReparentsPerMin,
1650
- maintMaxReparentsPerPeer,
1651
- maintLevelP95DriftMax,
1652
- maintChildrenP95DriftMax,
1653
- overheadFactorData,
1654
- controlBpp,
1655
- trackerBpp,
1656
- repairBpp,
1657
- earningsTotal,
1658
- earningsRelayCount,
1659
- earningsRelayP50,
1660
- earningsRelayP95,
1661
- earningsRelayMax,
1662
- protocolControlSends,
1663
- protocolControlBytesSent,
1664
- protocolControlBytesSentJoin,
1665
- protocolControlBytesSentRepair,
1666
- protocolControlBytesSentTracker,
1667
- protocolControlReceives,
1668
- protocolControlBytesReceived,
1669
- protocolDataSends,
1670
- protocolDataPayloadBytesSent,
1671
- protocolDataReceives,
1672
- protocolDataPayloadBytesReceived,
1673
- protocolRepairReqSent,
1674
- protocolFetchReqSent,
1675
- protocolIHaveSent,
1676
- protocolTrackerAnnounceSent,
1677
- protocolTrackerQuerySent,
1678
- protocolTrackerReplySent,
1679
- protocolTrackerFeedbackSent,
1680
- protocolCacheHitsServed,
1681
- protocolHoleFillsFromNeighbor,
1682
- protocolRouteCacheHits,
1683
- protocolRouteCacheMisses,
1684
- protocolRouteCacheExpirations,
1685
- protocolRouteCacheEvictions,
1686
- protocolRouteProxyQueries,
1687
- protocolRouteProxyTimeouts,
1688
- protocolRouteProxyFanout,
1689
- network: session.network.metrics,
1690
- };
1691
- };
1692
- const abortPromise = timeoutMs > 0
1693
- ? new Promise((_, reject) => {
1694
- const onAbort = () => {
1695
- timeoutSignal.removeEventListener("abort", onAbort);
1696
- const reason = timeoutSignal.reason;
1697
- reject(reason instanceof Error
1698
- ? reason
1699
- : new Error(typeof reason === "string"
1700
- ? reason
1701
- : "fanout-tree-sim aborted"));
1702
- };
1703
- if (timeoutSignal.aborted) {
1704
- onAbort();
1705
- return;
1706
- }
1707
- timeoutSignal.addEventListener("abort", onAbort, { once: true });
1708
- })
1709
- : undefined;
1710
- const result = abortPromise
1711
- ? await Promise.race([run(), abortPromise])
1712
- : await run();
1713
- if (profileEnabled && profileCpuStart) {
1714
- try {
1715
- profileEld?.disable();
1716
- }
1717
- catch {
1718
- // ignore
1719
- }
1720
- const cpu = process.cpuUsage(profileCpuStart);
1721
- const mem = process.memoryUsage();
1722
- const p95 = profileEld ? profileEld.percentile(95) / 1e6 : 0;
1723
- const max = profileEld ? profileEld.max / 1e6 : 0;
1724
- result.profile = {
1725
- cpuUserMs: cpu.user / 1_000,
1726
- cpuSystemMs: cpu.system / 1_000,
1727
- rssMb: mem.rss / 1e6,
1728
- heapUsedMb: mem.heapUsed / 1e6,
1729
- eventLoopDelayP95Ms: p95,
1730
- eventLoopDelayMaxMs: max,
1731
- };
1732
- }
1733
- return result;
1734
- }
1735
- finally {
1736
- if (timer)
1737
- clearTimeout(timer);
1738
- try {
1739
- profileEld?.disable();
1740
- }
1741
- catch {
1742
- // ignore
1743
- }
1744
- try {
1745
- await session.stop();
1746
- }
1747
- catch {
1748
- // ignore teardown aborts in the shim
1749
- }
1750
- }
1751
- };
1752
- //# sourceMappingURL=fanout-tree-sim-lib.js.map