@peerbit/pubsub 4.1.4 → 5.0.0-2d88223
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.
- package/README.md +23 -20
- package/dist/benchmark/fanout-tree-sim-lib.d.ts +201 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-sim-lib.js +1225 -0
- package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -0
- package/dist/benchmark/fanout-tree-sim.d.ts +11 -0
- package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-sim.js +521 -0
- package/dist/benchmark/fanout-tree-sim.js.map +1 -0
- package/dist/benchmark/index.d.ts +6 -0
- package/dist/benchmark/index.d.ts.map +1 -1
- package/dist/benchmark/index.js +38 -80
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts +82 -0
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -0
- package/dist/benchmark/pubsub-topic-sim-lib.js +625 -0
- package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -0
- package/dist/benchmark/pubsub-topic-sim.d.ts +9 -0
- package/dist/benchmark/pubsub-topic-sim.d.ts.map +1 -0
- package/dist/benchmark/pubsub-topic-sim.js +116 -0
- package/dist/benchmark/pubsub-topic-sim.js.map +1 -0
- package/dist/benchmark/sim/bench-utils.d.ts +25 -0
- package/dist/benchmark/sim/bench-utils.d.ts.map +1 -0
- package/dist/benchmark/sim/bench-utils.js +141 -0
- package/dist/benchmark/sim/bench-utils.js.map +1 -0
- package/dist/src/fanout-channel.d.ts +62 -0
- package/dist/src/fanout-channel.d.ts.map +1 -0
- package/dist/src/fanout-channel.js +114 -0
- package/dist/src/fanout-channel.js.map +1 -0
- package/dist/src/fanout-tree.d.ts +551 -0
- package/dist/src/fanout-tree.d.ts.map +1 -0
- package/dist/src/fanout-tree.js +4980 -0
- package/dist/src/fanout-tree.js.map +1 -0
- package/dist/src/index.d.ts +168 -39
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1416 -457
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +43 -0
- package/dist/src/topic-root-control-plane.d.ts.map +1 -0
- package/dist/src/topic-root-control-plane.js +120 -0
- package/dist/src/topic-root-control-plane.js.map +1 -0
- package/package.json +11 -10
- package/src/fanout-channel.ts +150 -0
- package/src/fanout-tree.ts +6346 -0
- package/src/index.ts +1693 -591
- package/src/topic-root-control-plane.ts +160 -0
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
import { PreHash, SignatureWithKey } from "@peerbit/crypto";
|
|
2
|
+
import { delay } from "@peerbit/time";
|
|
3
|
+
import { anySignal } from "any-signal";
|
|
4
|
+
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
5
|
+
import { FanoutTree } from "../src/index.js";
|
|
6
|
+
import { InMemoryNetwork, InMemorySession, } from "@peerbit/libp2p-test-utils/inmemory-libp2p.js";
|
|
7
|
+
import { int, mulberry32, quantile, runWithConcurrency } from "./sim/bench-utils.js";
|
|
8
|
+
class SimFanoutTree extends FanoutTree {
|
|
9
|
+
constructor(c, opts) {
|
|
10
|
+
super(c, opts);
|
|
11
|
+
// Fast/mock signing: keep signer identity semantics but skip crypto work.
|
|
12
|
+
this.sign = async () => new SignatureWithKey({
|
|
13
|
+
signature: new Uint8Array([0]),
|
|
14
|
+
publicKey: this.publicKey,
|
|
15
|
+
prehash: PreHash.NONE,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async verifyAndProcess(message) {
|
|
19
|
+
// Skip expensive crypto verify for large sims, but keep session handling behavior
|
|
20
|
+
// consistent with the real implementation.
|
|
21
|
+
const from = message.header.signatures.publicKeys[0];
|
|
22
|
+
if (!this.peers.has(from.hashcode())) {
|
|
23
|
+
this.updateSession(from, Number(message.header.session));
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pickDistinct = (rng, n, k, exclude) => {
|
|
29
|
+
if (k <= 0)
|
|
30
|
+
return [];
|
|
31
|
+
const out = new Set();
|
|
32
|
+
while (out.size < k) {
|
|
33
|
+
const candidate = int(rng, n);
|
|
34
|
+
if (exclude.has(candidate))
|
|
35
|
+
continue;
|
|
36
|
+
out.add(candidate);
|
|
37
|
+
}
|
|
38
|
+
return [...out];
|
|
39
|
+
};
|
|
40
|
+
const parseSimPeerIndex = (peerId) => {
|
|
41
|
+
const s = String(peerId?.toString?.() ?? "");
|
|
42
|
+
const m = s.match(/sim-(\d+)/);
|
|
43
|
+
if (!m)
|
|
44
|
+
return 0;
|
|
45
|
+
const n = Number(m[1]);
|
|
46
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
47
|
+
};
|
|
48
|
+
export const resolveFanoutTreeSimParams = (input) => {
|
|
49
|
+
const nodes = Number(input.nodes ?? 2000);
|
|
50
|
+
const bootstraps = Number(input.bootstraps ?? 1);
|
|
51
|
+
const subscribersDefault = Math.max(0, nodes - 1 - bootstraps);
|
|
52
|
+
const msgRate = Number(input.msgRate ?? 30);
|
|
53
|
+
const intervalMsRaw = Number(input.intervalMs ?? 0);
|
|
54
|
+
const intervalMs = intervalMsRaw > 0
|
|
55
|
+
? intervalMsRaw
|
|
56
|
+
: msgRate > 0
|
|
57
|
+
? Math.floor(1000 / msgRate)
|
|
58
|
+
: 0;
|
|
59
|
+
return {
|
|
60
|
+
nodes,
|
|
61
|
+
rootIndex: Number(input.rootIndex ?? 0),
|
|
62
|
+
bootstraps,
|
|
63
|
+
bootstrapMaxPeers: Number(input.bootstrapMaxPeers ?? 0),
|
|
64
|
+
subscribers: Number(input.subscribers ?? subscribersDefault),
|
|
65
|
+
relayFraction: Number(input.relayFraction ?? 0.25),
|
|
66
|
+
messages: Number(input.messages ?? 200),
|
|
67
|
+
msgRate,
|
|
68
|
+
msgSize: Number(input.msgSize ?? 1024),
|
|
69
|
+
intervalMs,
|
|
70
|
+
settleMs: Number(input.settleMs ?? 2_000),
|
|
71
|
+
deadlineMs: Number(input.deadlineMs ?? 0),
|
|
72
|
+
maxDataAgeMs: Number(input.maxDataAgeMs ?? 0),
|
|
73
|
+
timeoutMs: Number(input.timeoutMs ?? 300_000),
|
|
74
|
+
seed: Number(input.seed ?? 1),
|
|
75
|
+
topic: String(input.topic ?? "concert"),
|
|
76
|
+
rootUploadLimitBps: Number(input.rootUploadLimitBps ?? 20_000_000),
|
|
77
|
+
rootMaxChildren: Number(input.rootMaxChildren ?? 64),
|
|
78
|
+
relayUploadLimitBps: Number(input.relayUploadLimitBps ?? 10_000_000),
|
|
79
|
+
relayMaxChildren: Number(input.relayMaxChildren ?? 32),
|
|
80
|
+
allowKick: Boolean(input.allowKick ?? false),
|
|
81
|
+
bidPerByte: Number(input.bidPerByte ?? 0),
|
|
82
|
+
bidPerByteRelay: Number(input.bidPerByteRelay ?? input.bidPerByte ?? 0),
|
|
83
|
+
bidPerByteLeaf: Number(input.bidPerByteLeaf ?? input.bidPerByte ?? 0),
|
|
84
|
+
repair: Boolean(input.repair ?? true),
|
|
85
|
+
repairWindowMessages: Number(input.repairWindowMessages ?? 1024),
|
|
86
|
+
repairMaxBackfillMessages: Number(input.repairMaxBackfillMessages ?? -1),
|
|
87
|
+
repairIntervalMs: Number(input.repairIntervalMs ?? 200),
|
|
88
|
+
repairMaxPerReq: Number(input.repairMaxPerReq ?? 64),
|
|
89
|
+
neighborRepair: Boolean(input.neighborRepair ?? false),
|
|
90
|
+
neighborRepairPeers: Number(input.neighborRepairPeers ?? 2),
|
|
91
|
+
neighborMeshPeers: Number(input.neighborMeshPeers ?? -1),
|
|
92
|
+
neighborAnnounceIntervalMs: Number(input.neighborAnnounceIntervalMs ?? -1),
|
|
93
|
+
neighborMeshRefreshIntervalMs: Number(input.neighborMeshRefreshIntervalMs ?? -1),
|
|
94
|
+
neighborHaveTtlMs: Number(input.neighborHaveTtlMs ?? -1),
|
|
95
|
+
neighborRepairBudgetBps: Number(input.neighborRepairBudgetBps ?? -1),
|
|
96
|
+
neighborRepairBurstMs: Number(input.neighborRepairBurstMs ?? -1),
|
|
97
|
+
streamRxDelayMs: Number(input.streamRxDelayMs ?? 0),
|
|
98
|
+
streamHighWaterMarkBytes: Number(input.streamHighWaterMarkBytes ?? 256 * 1024),
|
|
99
|
+
dialDelayMs: Number(input.dialDelayMs ?? 0),
|
|
100
|
+
joinConcurrency: Number(input.joinConcurrency ?? 256),
|
|
101
|
+
joinReqTimeoutMs: Number(input.joinReqTimeoutMs ?? -1),
|
|
102
|
+
candidateShuffleTopK: Number(input.candidateShuffleTopK ?? -1),
|
|
103
|
+
candidateScoringMode: input.candidateScoringMode === "ranked-strict" ||
|
|
104
|
+
input.candidateScoringMode === "weighted" ||
|
|
105
|
+
input.candidateScoringMode === "ranked-shuffle"
|
|
106
|
+
? input.candidateScoringMode
|
|
107
|
+
: "ranked-shuffle",
|
|
108
|
+
bootstrapEnsureIntervalMs: Number(input.bootstrapEnsureIntervalMs ?? -1),
|
|
109
|
+
trackerQueryIntervalMs: Number(input.trackerQueryIntervalMs ?? -1),
|
|
110
|
+
joinAttemptsPerRound: Number(input.joinAttemptsPerRound ?? -1),
|
|
111
|
+
candidateCooldownMs: Number(input.candidateCooldownMs ?? -1),
|
|
112
|
+
joinPhases: Boolean(input.joinPhases ?? false),
|
|
113
|
+
joinPhaseSettleMs: Number(input.joinPhaseSettleMs ?? 2_000),
|
|
114
|
+
maxLatencySamples: Number(input.maxLatencySamples ?? 1_000_000),
|
|
115
|
+
profile: Boolean(input.profile ?? false),
|
|
116
|
+
progress: Boolean(input.progress ?? false),
|
|
117
|
+
progressEveryMs: Number(input.progressEveryMs ?? 5_000),
|
|
118
|
+
dropDataFrameRate: Number(input.dropDataFrameRate ?? 0),
|
|
119
|
+
churnEveryMs: Number(input.churnEveryMs ?? 0),
|
|
120
|
+
churnDownMs: Number(input.churnDownMs ?? 0),
|
|
121
|
+
churnFraction: Number(input.churnFraction ?? 0),
|
|
122
|
+
assertMinJoinedPct: Number(input.assertMinJoinedPct ?? 0),
|
|
123
|
+
assertMinDeliveryPct: Number(input.assertMinDeliveryPct ?? 0),
|
|
124
|
+
assertMinDeadlineDeliveryPct: Number(input.assertMinDeadlineDeliveryPct ?? 0),
|
|
125
|
+
assertMaxUploadFracPct: Number(input.assertMaxUploadFracPct ?? 0),
|
|
126
|
+
assertMaxOverheadFactor: Number(input.assertMaxOverheadFactor ?? 0),
|
|
127
|
+
assertMaxControlBpp: Number(input.assertMaxControlBpp ?? 0),
|
|
128
|
+
assertMaxTrackerBpp: Number(input.assertMaxTrackerBpp ?? 0),
|
|
129
|
+
assertMaxRepairBpp: Number(input.assertMaxRepairBpp ?? 0),
|
|
130
|
+
assertAttachP95Ms: Number(input.assertAttachP95Ms ?? 0),
|
|
131
|
+
assertMaxTreeLevelP95: Number(input.assertMaxTreeLevelP95 ?? 0),
|
|
132
|
+
assertMaxFormationScore: Number(input.assertMaxFormationScore ?? 0),
|
|
133
|
+
assertMaxOrphans: Number(input.assertMaxOrphans ?? 0),
|
|
134
|
+
assertRecoveryP95Ms: Number(input.assertRecoveryP95Ms ?? 0),
|
|
135
|
+
assertMaxReparentsPerMin: Number(input.assertMaxReparentsPerMin ?? 0),
|
|
136
|
+
assertMaxOrphanArea: Number(input.assertMaxOrphanArea ?? 0),
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
export const formatFanoutTreeSimResult = (r) => {
|
|
140
|
+
const p = r.params;
|
|
141
|
+
return [
|
|
142
|
+
"fanout-tree-sim",
|
|
143
|
+
`nodes=${p.nodes} bootstraps=${r.bootstrapCount} bootstrapMaxPeers=${p.bootstrapMaxPeers} subscribers=${r.subscriberCount} relays=${r.relayCount}`,
|
|
144
|
+
`joined=${r.joinedCount}/${r.subscriberCount} (${r.joinedPct.toFixed(2)}%)`,
|
|
145
|
+
`join: ${(r.joinMs / 1000).toFixed(3)}s`,
|
|
146
|
+
`attachMs samples=${r.attachSamples} p50=${r.attachP50.toFixed(1)} p95=${r.attachP95.toFixed(1)} p99=${r.attachP99.toFixed(1)} max=${r.attachMax.toFixed(1)}`,
|
|
147
|
+
`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)}`,
|
|
148
|
+
`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}`,
|
|
149
|
+
`publish: ${(r.publishMs / 1000).toFixed(3)}s intervalMs=${p.intervalMs}`,
|
|
150
|
+
`churn: everyMs=${p.churnEveryMs} downMs=${p.churnDownMs} fraction=${p.churnFraction} events=${r.churnEvents} peers=${r.churnedPeersTotal}`,
|
|
151
|
+
...(r.maintSamples > 0
|
|
152
|
+
? [
|
|
153
|
+
`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)}`,
|
|
154
|
+
]
|
|
155
|
+
: []),
|
|
156
|
+
`delivered=${r.delivered}/${r.expected} (${r.deliveredPct.toFixed(2)}%) dup=${r.duplicates}`,
|
|
157
|
+
p.deadlineMs > 0
|
|
158
|
+
? `deadline=${p.deadlineMs}ms${p.maxDataAgeMs > 0 ? ` maxAgeMs=${p.maxDataAgeMs}` : ""} delivered=${r.deliveredWithinDeadline}/${r.expected} (${r.deliveredWithinDeadlinePct.toFixed(2)}%)`
|
|
159
|
+
: `deadline=off${p.maxDataAgeMs > 0 ? ` maxAgeMs=${p.maxDataAgeMs}` : ""}`,
|
|
160
|
+
`latencyMs p50=${r.latencyP50.toFixed(1)} p95=${r.latencyP95.toFixed(1)} p99=${r.latencyP99.toFixed(1)} max=${r.latencyMax.toFixed(1)}`,
|
|
161
|
+
`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 ?? "-"}`,
|
|
162
|
+
`reparent: disconnect=${r.reparentDisconnectTotal} stale=${r.reparentStaleTotal} kicked=${r.reparentKickedTotal}`,
|
|
163
|
+
`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}`,
|
|
164
|
+
`upload: max=${r.maxUploadBps} B/s (${r.maxUploadFracPct.toFixed(1)}% of cap) node=${r.maxUploadNode ?? "-"}`,
|
|
165
|
+
`stream: queuedBytes total=${r.streamQueuedBytesTotal} max=${r.streamQueuedBytesMax} p95=${r.streamQueuedBytesP95.toFixed(0)} node=${r.streamQueuedBytesMaxNode ?? "-"} lanes=${r.streamQueuedBytesByLane.join(",")}`,
|
|
166
|
+
`overhead: dataFactor=${r.overheadFactorData.toFixed(3)} (sentPayloadBytes / ideal)`,
|
|
167
|
+
`economics: earningsTotal=${r.earningsTotal} relayCount=${r.earningsRelayCount} p50=${r.earningsRelayP50} p95=${r.earningsRelayP95} max=${r.earningsRelayMax}`,
|
|
168
|
+
`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} trackerFeedbackSent=${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}`,
|
|
169
|
+
`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}`,
|
|
170
|
+
...(r.profile
|
|
171
|
+
? [
|
|
172
|
+
`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)}`,
|
|
173
|
+
]
|
|
174
|
+
: []),
|
|
175
|
+
].join("\n");
|
|
176
|
+
};
|
|
177
|
+
export const runFanoutTreeSim = async (input) => {
|
|
178
|
+
const params = resolveFanoutTreeSimParams(input);
|
|
179
|
+
const timeoutMs = Math.max(0, Math.floor(params.timeoutMs));
|
|
180
|
+
const rng = mulberry32(params.seed);
|
|
181
|
+
const profileEnabled = params.profile === true;
|
|
182
|
+
let profileCpuStart;
|
|
183
|
+
let profileEld;
|
|
184
|
+
if (profileEnabled) {
|
|
185
|
+
profileCpuStart = process.cpuUsage();
|
|
186
|
+
try {
|
|
187
|
+
profileEld = monitorEventLoopDelay({ resolution: 20 });
|
|
188
|
+
profileEld.enable();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// ignore
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const rootIndex = Math.max(0, Math.min(params.nodes - 1, params.rootIndex));
|
|
195
|
+
const bootstrapCount = Math.max(0, Math.min(params.nodes - 1, Math.floor(params.bootstraps)));
|
|
196
|
+
const bootstrapIndices = Array.from({ length: bootstrapCount }, (_, i) => {
|
|
197
|
+
const idx = i + 1;
|
|
198
|
+
return idx >= params.nodes ? 0 : idx;
|
|
199
|
+
}).filter((i) => i !== rootIndex);
|
|
200
|
+
const exclude = new Set([rootIndex, ...bootstrapIndices]);
|
|
201
|
+
const subscriberCount = Math.max(0, Math.min(params.nodes - exclude.size, Math.floor(params.subscribers)));
|
|
202
|
+
const subscriberIndices = pickDistinct(rng, params.nodes, subscriberCount, exclude);
|
|
203
|
+
// Ensure we have at least one relay when scale > root fanout.
|
|
204
|
+
const wantsRelays = subscriberCount > Math.max(0, params.rootMaxChildren);
|
|
205
|
+
let relayTarget = Math.floor(subscriberCount * Math.max(0, Math.min(1, params.relayFraction)));
|
|
206
|
+
if (wantsRelays && relayTarget === 0)
|
|
207
|
+
relayTarget = Math.min(1, subscriberCount);
|
|
208
|
+
const relaySet = new Set();
|
|
209
|
+
const shuffledSubscribers = [...subscriberIndices].sort(() => rng() - 0.5);
|
|
210
|
+
for (const idx of shuffledSubscribers) {
|
|
211
|
+
if (relaySet.size >= relayTarget)
|
|
212
|
+
break;
|
|
213
|
+
relaySet.add(idx);
|
|
214
|
+
}
|
|
215
|
+
const network = new InMemoryNetwork({
|
|
216
|
+
streamRxDelayMs: params.streamRxDelayMs,
|
|
217
|
+
streamHighWaterMarkBytes: params.streamHighWaterMarkBytes,
|
|
218
|
+
dialDelayMs: params.dialDelayMs,
|
|
219
|
+
dropDataFrameRate: params.dropDataFrameRate,
|
|
220
|
+
dropSeed: params.seed,
|
|
221
|
+
});
|
|
222
|
+
const bootstrapIndexSet = new Set(bootstrapIndices);
|
|
223
|
+
const maxConnectionsFor = (index) => {
|
|
224
|
+
if (index === rootIndex)
|
|
225
|
+
return 256;
|
|
226
|
+
if (bootstrapIndexSet.has(index))
|
|
227
|
+
return 128;
|
|
228
|
+
if (relaySet.has(index))
|
|
229
|
+
return 64;
|
|
230
|
+
return 16;
|
|
231
|
+
};
|
|
232
|
+
const seenCacheMaxFor = (index) => {
|
|
233
|
+
if (index === rootIndex)
|
|
234
|
+
return 200_000;
|
|
235
|
+
if (bootstrapIndexSet.has(index))
|
|
236
|
+
return 100_000;
|
|
237
|
+
if (relaySet.has(index))
|
|
238
|
+
return 50_000;
|
|
239
|
+
return 20_000;
|
|
240
|
+
};
|
|
241
|
+
const seenCacheTtlMsFor = (index) => {
|
|
242
|
+
if (index === rootIndex || bootstrapIndexSet.has(index))
|
|
243
|
+
return 120_000;
|
|
244
|
+
return 60_000;
|
|
245
|
+
};
|
|
246
|
+
const session = await InMemorySession.disconnected(params.nodes, {
|
|
247
|
+
network,
|
|
248
|
+
basePort: 30_000,
|
|
249
|
+
services: {
|
|
250
|
+
fanout: (c) => {
|
|
251
|
+
const index = parseSimPeerIndex(c?.peerId);
|
|
252
|
+
return new SimFanoutTree(c, {
|
|
253
|
+
// Keep sims bounded: limit per-node connections to roughly the fanout degree,
|
|
254
|
+
// so large networks can run in a single process without OOM.
|
|
255
|
+
connectionManager: {
|
|
256
|
+
minConnections: 0,
|
|
257
|
+
maxConnections: maxConnectionsFor(index),
|
|
258
|
+
dialer: false,
|
|
259
|
+
pruner: { interval: 1_000 },
|
|
260
|
+
},
|
|
261
|
+
seenCacheMax: seenCacheMaxFor(index),
|
|
262
|
+
seenCacheTtlMs: seenCacheTtlMsFor(index),
|
|
263
|
+
random: mulberry32((params.seed >>> 0) ^ index),
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
let timer;
|
|
269
|
+
const timeoutController = new AbortController();
|
|
270
|
+
const timeoutSignal = timeoutController.signal;
|
|
271
|
+
try {
|
|
272
|
+
if (timeoutMs > 0) {
|
|
273
|
+
timer = setTimeout(() => {
|
|
274
|
+
timeoutController.abort(new Error(`fanout-tree-sim timed out after ${timeoutMs}ms (override with --timeoutMs)`));
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
}
|
|
277
|
+
const run = async () => {
|
|
278
|
+
const bootstrapAddrs = bootstrapIndices.flatMap((i) => session.peers[i].getMultiaddrs());
|
|
279
|
+
if (bootstrapAddrs.length === 0) {
|
|
280
|
+
throw new Error("No bootstrap addrs; pass --bootstraps >= 1");
|
|
281
|
+
}
|
|
282
|
+
for (const p of session.peers) {
|
|
283
|
+
p.services.fanout.setBootstraps(bootstrapAddrs);
|
|
284
|
+
}
|
|
285
|
+
const root = session.peers[rootIndex].services.fanout;
|
|
286
|
+
const rootId = root.publicKeyHash;
|
|
287
|
+
// Root opens channel and starts announcing capacity to trackers immediately.
|
|
288
|
+
root.openChannel(params.topic, rootId, {
|
|
289
|
+
role: "root",
|
|
290
|
+
msgRate: params.msgRate,
|
|
291
|
+
msgSize: params.msgSize,
|
|
292
|
+
...(params.maxDataAgeMs > 0 ? { maxDataAgeMs: params.maxDataAgeMs } : {}),
|
|
293
|
+
uploadLimitBps: params.rootUploadLimitBps,
|
|
294
|
+
maxChildren: params.rootMaxChildren,
|
|
295
|
+
bidPerByte: params.bidPerByte,
|
|
296
|
+
allowKick: params.allowKick,
|
|
297
|
+
repair: params.repair,
|
|
298
|
+
repairWindowMessages: params.repairWindowMessages,
|
|
299
|
+
...(params.repairMaxBackfillMessages >= 0
|
|
300
|
+
? { repairMaxBackfillMessages: params.repairMaxBackfillMessages }
|
|
301
|
+
: {}),
|
|
302
|
+
repairIntervalMs: params.repairIntervalMs,
|
|
303
|
+
repairMaxPerReq: params.repairMaxPerReq,
|
|
304
|
+
neighborRepair: params.neighborRepair,
|
|
305
|
+
neighborRepairPeers: params.neighborRepairPeers,
|
|
306
|
+
...(params.neighborMeshPeers >= 0
|
|
307
|
+
? { neighborMeshPeers: params.neighborMeshPeers }
|
|
308
|
+
: {}),
|
|
309
|
+
...(params.neighborAnnounceIntervalMs >= 0
|
|
310
|
+
? { neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs }
|
|
311
|
+
: {}),
|
|
312
|
+
...(params.neighborMeshRefreshIntervalMs >= 0
|
|
313
|
+
? { neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs }
|
|
314
|
+
: {}),
|
|
315
|
+
...(params.neighborHaveTtlMs >= 0
|
|
316
|
+
? { neighborHaveTtlMs: params.neighborHaveTtlMs }
|
|
317
|
+
: {}),
|
|
318
|
+
...(params.neighborRepairBudgetBps >= 0
|
|
319
|
+
? { neighborRepairBudgetBps: params.neighborRepairBudgetBps }
|
|
320
|
+
: {}),
|
|
321
|
+
...(params.neighborRepairBurstMs >= 0
|
|
322
|
+
? { neighborRepairBurstMs: params.neighborRepairBurstMs }
|
|
323
|
+
: {}),
|
|
324
|
+
});
|
|
325
|
+
// Join subscribers (bounded concurrency).
|
|
326
|
+
const joinStart = Date.now();
|
|
327
|
+
const joined = new Array(subscriberIndices.length).fill(false);
|
|
328
|
+
const attachDurationsByPos = new Array(subscriberIndices.length).fill(-1);
|
|
329
|
+
let joinCompleted = 0;
|
|
330
|
+
let joinOk = 0;
|
|
331
|
+
const progressEveryMs = Math.max(250, Math.floor(params.progressEveryMs || 5_000));
|
|
332
|
+
let joinProgressTimer;
|
|
333
|
+
if (params.progress) {
|
|
334
|
+
console.log(`[fanout-tree-sim] phase=join subscribers=${subscriberIndices.length} relays=${relaySet.size} joinConcurrency=${params.joinConcurrency}`);
|
|
335
|
+
joinProgressTimer = setInterval(() => {
|
|
336
|
+
const mu = process.memoryUsage();
|
|
337
|
+
const rssMb = mu.rss / (1024 * 1024);
|
|
338
|
+
const heapUsedMb = mu.heapUsed / (1024 * 1024);
|
|
339
|
+
const openConns = Math.max(0, network.metrics.connectionsOpened - network.metrics.connectionsClosed);
|
|
340
|
+
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)}`);
|
|
341
|
+
}, progressEveryMs);
|
|
342
|
+
joinProgressTimer.unref?.();
|
|
343
|
+
}
|
|
344
|
+
const joinOne = async (idx) => {
|
|
345
|
+
const node = session.peers[idx].services.fanout;
|
|
346
|
+
const isRelay = relaySet.has(idx);
|
|
347
|
+
try {
|
|
348
|
+
await node.joinChannel(params.topic, rootId, {
|
|
349
|
+
msgRate: params.msgRate,
|
|
350
|
+
msgSize: params.msgSize,
|
|
351
|
+
...(params.maxDataAgeMs > 0 ? { maxDataAgeMs: params.maxDataAgeMs } : {}),
|
|
352
|
+
uploadLimitBps: isRelay ? params.relayUploadLimitBps : 0,
|
|
353
|
+
maxChildren: isRelay ? params.relayMaxChildren : 0,
|
|
354
|
+
bidPerByte: isRelay ? params.bidPerByteRelay : params.bidPerByteLeaf,
|
|
355
|
+
allowKick: params.allowKick,
|
|
356
|
+
repair: params.repair,
|
|
357
|
+
repairWindowMessages: params.repairWindowMessages,
|
|
358
|
+
...(params.repairMaxBackfillMessages >= 0
|
|
359
|
+
? { repairMaxBackfillMessages: params.repairMaxBackfillMessages }
|
|
360
|
+
: {}),
|
|
361
|
+
repairIntervalMs: params.repairIntervalMs,
|
|
362
|
+
repairMaxPerReq: params.repairMaxPerReq,
|
|
363
|
+
neighborRepair: params.neighborRepair,
|
|
364
|
+
neighborRepairPeers: params.neighborRepairPeers,
|
|
365
|
+
...(params.neighborMeshPeers >= 0
|
|
366
|
+
? { neighborMeshPeers: params.neighborMeshPeers }
|
|
367
|
+
: {}),
|
|
368
|
+
...(params.neighborAnnounceIntervalMs >= 0
|
|
369
|
+
? { neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs }
|
|
370
|
+
: {}),
|
|
371
|
+
...(params.neighborMeshRefreshIntervalMs >= 0
|
|
372
|
+
? { neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs }
|
|
373
|
+
: {}),
|
|
374
|
+
...(params.neighborHaveTtlMs >= 0
|
|
375
|
+
? { neighborHaveTtlMs: params.neighborHaveTtlMs }
|
|
376
|
+
: {}),
|
|
377
|
+
...(params.neighborRepairBudgetBps >= 0
|
|
378
|
+
? { neighborRepairBudgetBps: params.neighborRepairBudgetBps }
|
|
379
|
+
: {}),
|
|
380
|
+
...(params.neighborRepairBurstMs >= 0
|
|
381
|
+
? { neighborRepairBurstMs: params.neighborRepairBurstMs }
|
|
382
|
+
: {}),
|
|
383
|
+
}, {
|
|
384
|
+
timeoutMs: Math.max(10_000, Math.min(120_000, timeoutMs || 120_000)),
|
|
385
|
+
...(params.maxDataAgeMs > 0 ? { staleAfterMs: params.maxDataAgeMs } : {}),
|
|
386
|
+
...(params.joinReqTimeoutMs >= 0
|
|
387
|
+
? { joinReqTimeoutMs: params.joinReqTimeoutMs }
|
|
388
|
+
: {}),
|
|
389
|
+
...(params.candidateShuffleTopK >= 0
|
|
390
|
+
? { candidateShuffleTopK: params.candidateShuffleTopK }
|
|
391
|
+
: {}),
|
|
392
|
+
candidateScoringMode: params.candidateScoringMode,
|
|
393
|
+
...(params.bootstrapEnsureIntervalMs >= 0
|
|
394
|
+
? { bootstrapEnsureIntervalMs: params.bootstrapEnsureIntervalMs }
|
|
395
|
+
: {}),
|
|
396
|
+
...(params.trackerQueryIntervalMs >= 0
|
|
397
|
+
? { trackerQueryIntervalMs: params.trackerQueryIntervalMs }
|
|
398
|
+
: {}),
|
|
399
|
+
...(params.joinAttemptsPerRound >= 0
|
|
400
|
+
? { joinAttemptsPerRound: params.joinAttemptsPerRound }
|
|
401
|
+
: {}),
|
|
402
|
+
...(params.candidateCooldownMs >= 0
|
|
403
|
+
? { candidateCooldownMs: params.candidateCooldownMs }
|
|
404
|
+
: {}),
|
|
405
|
+
signal: timeoutSignal,
|
|
406
|
+
bootstrapMaxPeers: params.bootstrapMaxPeers,
|
|
407
|
+
});
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const runPhase = async (indices) => {
|
|
415
|
+
const tasks = indices.map((pos) => async () => {
|
|
416
|
+
const ok = await joinOne(subscriberIndices[pos]);
|
|
417
|
+
joined[pos] = ok;
|
|
418
|
+
if (ok) {
|
|
419
|
+
joinOk += 1;
|
|
420
|
+
attachDurationsByPos[pos] = Date.now() - joinStart;
|
|
421
|
+
}
|
|
422
|
+
joinCompleted += 1;
|
|
423
|
+
return ok;
|
|
424
|
+
});
|
|
425
|
+
await runWithConcurrency(tasks, params.joinConcurrency);
|
|
426
|
+
};
|
|
427
|
+
try {
|
|
428
|
+
if (params.joinPhases) {
|
|
429
|
+
const relayPositions = [];
|
|
430
|
+
const leafPositions = [];
|
|
431
|
+
for (let i = 0; i < subscriberIndices.length; i++) {
|
|
432
|
+
const idx = subscriberIndices[i];
|
|
433
|
+
if (relaySet.has(idx))
|
|
434
|
+
relayPositions.push(i);
|
|
435
|
+
else
|
|
436
|
+
leafPositions.push(i);
|
|
437
|
+
}
|
|
438
|
+
await runPhase(relayPositions);
|
|
439
|
+
const settleMs = Math.max(0, Math.floor(params.joinPhaseSettleMs));
|
|
440
|
+
if (settleMs > 0) {
|
|
441
|
+
await delay(settleMs, { signal: timeoutSignal });
|
|
442
|
+
}
|
|
443
|
+
await runPhase(leafPositions);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
await runPhase(subscriberIndices.map((_, i) => i));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
if (joinProgressTimer)
|
|
451
|
+
clearInterval(joinProgressTimer);
|
|
452
|
+
}
|
|
453
|
+
const joinDone = Date.now();
|
|
454
|
+
if (params.progress) {
|
|
455
|
+
const mu = process.memoryUsage();
|
|
456
|
+
const rssMb = mu.rss / (1024 * 1024);
|
|
457
|
+
const heapUsedMb = mu.heapUsed / (1024 * 1024);
|
|
458
|
+
const openConns = Math.max(0, network.metrics.connectionsOpened - network.metrics.connectionsClosed);
|
|
459
|
+
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}`);
|
|
460
|
+
}
|
|
461
|
+
const attachDurations = attachDurationsByPos.filter((d) => d >= 0).sort((a, b) => a - b);
|
|
462
|
+
const attachSamples = attachDurations.length;
|
|
463
|
+
const attachP50 = attachSamples > 0 ? quantile(attachDurations, 0.5) : NaN;
|
|
464
|
+
const attachP95 = attachSamples > 0 ? quantile(attachDurations, 0.95) : NaN;
|
|
465
|
+
const attachP99 = attachSamples > 0 ? quantile(attachDurations, 0.99) : NaN;
|
|
466
|
+
const attachMax = attachSamples > 0 ? attachDurations[attachSamples - 1] : NaN;
|
|
467
|
+
const joinedHashes = new Set(subscriberIndices
|
|
468
|
+
.filter((_, i) => joined[i])
|
|
469
|
+
.map((i) => session.peers[i].services.fanout.publicKeyHash));
|
|
470
|
+
const joinedCount = joinedHashes.size;
|
|
471
|
+
const joinedSubscriberIndices = subscriberIndices.filter((_, i) => joined[i]);
|
|
472
|
+
const computeTreeShapeStats = () => {
|
|
473
|
+
const levels = [];
|
|
474
|
+
const levelByIndex = new Array(params.nodes).fill(NaN);
|
|
475
|
+
const childrenCounts = [];
|
|
476
|
+
let treeOrphans = 0;
|
|
477
|
+
let treeRootChildren = 0;
|
|
478
|
+
for (let i = 0; i < session.peers.length; i++) {
|
|
479
|
+
const s = session.peers[i].services.fanout.getChannelStats(params.topic, rootId);
|
|
480
|
+
if (!s)
|
|
481
|
+
continue;
|
|
482
|
+
if (Number.isFinite(s.level)) {
|
|
483
|
+
levels.push(s.level);
|
|
484
|
+
levelByIndex[i] = s.level;
|
|
485
|
+
}
|
|
486
|
+
if (s.effectiveMaxChildren > 0) {
|
|
487
|
+
childrenCounts.push(s.children);
|
|
488
|
+
}
|
|
489
|
+
if (s.level === 0) {
|
|
490
|
+
treeRootChildren = s.children;
|
|
491
|
+
}
|
|
492
|
+
else if (Number.isFinite(s.level) && !s.parent) {
|
|
493
|
+
treeOrphans += 1;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
levels.sort((a, b) => a - b);
|
|
497
|
+
childrenCounts.sort((a, b) => a - b);
|
|
498
|
+
const treeMaxLevel = levels.length > 0 ? levels[levels.length - 1] : 0;
|
|
499
|
+
const treeLevelP95 = levels.length > 0 ? quantile(levels, 0.95) : 0;
|
|
500
|
+
const treeLevelAvg = levels.length > 0 ? levels.reduce((a, b) => a + b, 0) / levels.length : 0;
|
|
501
|
+
const treeChildrenP95 = childrenCounts.length > 0 ? quantile(childrenCounts, 0.95) : 0;
|
|
502
|
+
const treeChildrenMax = childrenCounts.length > 0 ? childrenCounts[childrenCounts.length - 1] : 0;
|
|
503
|
+
return {
|
|
504
|
+
treeMaxLevel,
|
|
505
|
+
treeLevelP95,
|
|
506
|
+
treeLevelAvg,
|
|
507
|
+
treeOrphans,
|
|
508
|
+
treeChildrenP95,
|
|
509
|
+
treeChildrenMax,
|
|
510
|
+
treeRootChildren,
|
|
511
|
+
levelByIndex,
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
const formationTree = computeTreeShapeStats();
|
|
515
|
+
// Underlay (libp2p connection graph) shortest paths, used to spot wasted
|
|
516
|
+
// open connections that don't contribute to the overlay tree.
|
|
517
|
+
const underlayAdj = Array.from({ length: params.nodes }, () => new Set());
|
|
518
|
+
for (let i = 0; i < session.peers.length; i++) {
|
|
519
|
+
for (const c of session.peers[i].getConnections()) {
|
|
520
|
+
// @ts-ignore - bench shim uses the same field name as real libp2p connections
|
|
521
|
+
if (c.status && c.status !== "open")
|
|
522
|
+
continue;
|
|
523
|
+
const j = parseSimPeerIndex(c.remotePeer);
|
|
524
|
+
if (j === i)
|
|
525
|
+
continue;
|
|
526
|
+
if (j < 0 || j >= params.nodes)
|
|
527
|
+
continue;
|
|
528
|
+
underlayAdj[i].add(j);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
let formationUnderlayEdges = 0;
|
|
532
|
+
for (const s of underlayAdj)
|
|
533
|
+
formationUnderlayEdges += s.size;
|
|
534
|
+
formationUnderlayEdges = Math.floor(formationUnderlayEdges / 2);
|
|
535
|
+
const underlayDist = new Array(params.nodes).fill(Infinity);
|
|
536
|
+
const q = [];
|
|
537
|
+
underlayDist[rootIndex] = 0;
|
|
538
|
+
q.push(rootIndex);
|
|
539
|
+
for (let qi = 0; qi < q.length; qi++) {
|
|
540
|
+
const u = q[qi];
|
|
541
|
+
const du = underlayDist[u];
|
|
542
|
+
for (const v of underlayAdj[u]) {
|
|
543
|
+
if (underlayDist[v] !== Infinity)
|
|
544
|
+
continue;
|
|
545
|
+
underlayDist[v] = du + 1;
|
|
546
|
+
q.push(v);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const formationUnderlayDists = joinedSubscriberIndices
|
|
550
|
+
.map((i) => underlayDist[i])
|
|
551
|
+
.filter((d) => Number.isFinite(d))
|
|
552
|
+
.sort((a, b) => a - b);
|
|
553
|
+
const formationUnderlayDistP95 = formationUnderlayDists.length > 0 ? quantile(formationUnderlayDists, 0.95) : NaN;
|
|
554
|
+
const formationUnderlayDistMax = formationUnderlayDists.length > 0
|
|
555
|
+
? formationUnderlayDists[formationUnderlayDists.length - 1]
|
|
556
|
+
: NaN;
|
|
557
|
+
const formationStretches = joinedSubscriberIndices
|
|
558
|
+
.map((i) => {
|
|
559
|
+
const overlay = formationTree.levelByIndex[i];
|
|
560
|
+
const under = underlayDist[i];
|
|
561
|
+
if (!Number.isFinite(overlay) || !Number.isFinite(under) || under <= 0)
|
|
562
|
+
return NaN;
|
|
563
|
+
return overlay / under;
|
|
564
|
+
})
|
|
565
|
+
.filter((x) => Number.isFinite(x))
|
|
566
|
+
.sort((a, b) => a - b);
|
|
567
|
+
const formationStretchP95 = formationStretches.length > 0 ? quantile(formationStretches, 0.95) : NaN;
|
|
568
|
+
const formationStretchMax = formationStretches.length > 0 ? formationStretches[formationStretches.length - 1] : NaN;
|
|
569
|
+
const formationOrphanPct = joinedCount === 0 ? 0 : (100 * formationTree.treeOrphans) / joinedCount;
|
|
570
|
+
const formationStretchPenalty = Number.isFinite(formationStretchP95)
|
|
571
|
+
? Math.max(0, formationStretchP95 - 1) * 10
|
|
572
|
+
: 0;
|
|
573
|
+
const formationScore = (Number.isFinite(attachP95) ? attachP95 / 1000 : 0) +
|
|
574
|
+
formationTree.treeLevelP95 +
|
|
575
|
+
formationOrphanPct +
|
|
576
|
+
formationStretchPenalty;
|
|
577
|
+
const churnController = new AbortController();
|
|
578
|
+
const churnSignal = anySignal([timeoutSignal, churnController.signal]);
|
|
579
|
+
let churnEvents = 0;
|
|
580
|
+
let churnedPeersTotal = 0;
|
|
581
|
+
const wantsMaintenance = (params.churnEveryMs > 0 && params.churnDownMs > 0 && params.churnFraction > 0) ||
|
|
582
|
+
params.assertMaxOrphans > 0 ||
|
|
583
|
+
params.assertMaxOrphanArea > 0 ||
|
|
584
|
+
params.assertRecoveryP95Ms > 0 ||
|
|
585
|
+
params.assertMaxReparentsPerMin > 0;
|
|
586
|
+
const maintenanceController = new AbortController();
|
|
587
|
+
const maintenanceSignal = anySignal([
|
|
588
|
+
timeoutSignal,
|
|
589
|
+
maintenanceController.signal,
|
|
590
|
+
]);
|
|
591
|
+
let maintDurationMs = 0;
|
|
592
|
+
let maintSamples = 0;
|
|
593
|
+
let maintMaxOrphans = 0;
|
|
594
|
+
let maintOrphanAreaMs = 0;
|
|
595
|
+
const pendingRecoveryStarts = [];
|
|
596
|
+
const recoveryDurations = [];
|
|
597
|
+
let maintLevelP95DriftMax = 0;
|
|
598
|
+
let maintChildrenP95DriftMax = 0;
|
|
599
|
+
const reparentBaselineByHash = new Map();
|
|
600
|
+
let maintReparentsTotal = 0;
|
|
601
|
+
let maintMaxReparentsPerPeer = 0;
|
|
602
|
+
// Delivery tracking
|
|
603
|
+
const publishAt = new Map();
|
|
604
|
+
const joinedHashList = [...joinedHashes];
|
|
605
|
+
const hashToIndex = new Map();
|
|
606
|
+
for (let i = 0; i < joinedHashList.length; i++) {
|
|
607
|
+
hashToIndex.set(joinedHashList[i], i);
|
|
608
|
+
}
|
|
609
|
+
const bitsetBytes = Math.ceil(Math.max(0, params.messages) / 8);
|
|
610
|
+
const receivedBits = joinedHashList.map(() => new Uint8Array(bitsetBytes));
|
|
611
|
+
const receivedCounts = new Uint32Array(joinedHashList.length);
|
|
612
|
+
let duplicates = 0;
|
|
613
|
+
let deliveredWithinDeadline = 0;
|
|
614
|
+
let deliveredSamples = [];
|
|
615
|
+
const sampleCap = Math.max(1, Math.floor(params.maxLatencySamples));
|
|
616
|
+
let sampleSeen = 0;
|
|
617
|
+
const makeOnData = (localHash) => (ev) => {
|
|
618
|
+
const d = ev?.detail;
|
|
619
|
+
if (!d)
|
|
620
|
+
return;
|
|
621
|
+
if (d.topic !== params.topic)
|
|
622
|
+
return;
|
|
623
|
+
if (d.root !== rootId)
|
|
624
|
+
return;
|
|
625
|
+
const seq = d.seq >>> 0;
|
|
626
|
+
const index = hashToIndex.get(localHash);
|
|
627
|
+
if (index == null)
|
|
628
|
+
return; // not joined / not tracked
|
|
629
|
+
if (seq >= params.messages)
|
|
630
|
+
return;
|
|
631
|
+
const bits = receivedBits[index];
|
|
632
|
+
const byteIndex = seq >>> 3;
|
|
633
|
+
const mask = 1 << (seq & 7);
|
|
634
|
+
if (byteIndex >= bits.length)
|
|
635
|
+
return;
|
|
636
|
+
if ((bits[byteIndex] & mask) !== 0) {
|
|
637
|
+
duplicates += 1;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
bits[byteIndex] |= mask;
|
|
641
|
+
receivedCounts[index] += 1;
|
|
642
|
+
const sentAt = publishAt.get(seq);
|
|
643
|
+
if (sentAt != null) {
|
|
644
|
+
const latency = Date.now() - sentAt;
|
|
645
|
+
if (params.deadlineMs > 0 && latency <= params.deadlineMs) {
|
|
646
|
+
deliveredWithinDeadline += 1;
|
|
647
|
+
}
|
|
648
|
+
sampleSeen += 1;
|
|
649
|
+
if (deliveredSamples.length < sampleCap) {
|
|
650
|
+
deliveredSamples.push(latency);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const j = int(rng, sampleSeen);
|
|
654
|
+
if (j < sampleCap)
|
|
655
|
+
deliveredSamples[j] = latency;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
for (let i = 0; i < subscriberIndices.length; i++) {
|
|
660
|
+
if (!joined[i])
|
|
661
|
+
continue;
|
|
662
|
+
const idx = subscriberIndices[i];
|
|
663
|
+
const node = session.peers[idx].services.fanout;
|
|
664
|
+
const localHash = node.publicKeyHash;
|
|
665
|
+
node.addEventListener("fanout:data", makeOnData(localHash));
|
|
666
|
+
}
|
|
667
|
+
// Publish
|
|
668
|
+
const payload = new Uint8Array(Math.max(0, params.msgSize));
|
|
669
|
+
for (let i = 0; i < payload.length; i++)
|
|
670
|
+
payload[i] = i & 0xff;
|
|
671
|
+
const quantileFromCounts = (counts, maxValue, total, q) => {
|
|
672
|
+
if (total <= 0)
|
|
673
|
+
return 0;
|
|
674
|
+
const target = Math.min(total - 1, Math.max(0, Math.floor(q * (total - 1))));
|
|
675
|
+
let seen = 0;
|
|
676
|
+
for (let v = 0; v <= maxValue; v++) {
|
|
677
|
+
seen += counts[v] ?? 0;
|
|
678
|
+
if (seen > target)
|
|
679
|
+
return v;
|
|
680
|
+
}
|
|
681
|
+
return maxValue;
|
|
682
|
+
};
|
|
683
|
+
const levelCounts = [];
|
|
684
|
+
const childCounts = [];
|
|
685
|
+
const sampleMaintenance = (now) => {
|
|
686
|
+
let onlineJoined = 0;
|
|
687
|
+
let orphansOnline = 0;
|
|
688
|
+
let levelsTotal = 0;
|
|
689
|
+
let levelsMax = 0;
|
|
690
|
+
levelCounts.fill(0);
|
|
691
|
+
let childrenTotal = 0;
|
|
692
|
+
let childrenMax = 0;
|
|
693
|
+
childCounts.fill(0);
|
|
694
|
+
for (const idx of joinedSubscriberIndices) {
|
|
695
|
+
const peer = session.peers[idx];
|
|
696
|
+
if (network.isPeerOffline(peer.peerId, now))
|
|
697
|
+
continue;
|
|
698
|
+
onlineJoined += 1;
|
|
699
|
+
const s = peer.services.fanout.getChannelStats(params.topic, rootId);
|
|
700
|
+
if (!s) {
|
|
701
|
+
orphansOnline += 1;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (Number.isFinite(s.level)) {
|
|
705
|
+
const lvl = Math.max(0, Math.floor(s.level));
|
|
706
|
+
levelsMax = Math.max(levelsMax, lvl);
|
|
707
|
+
while (levelCounts.length <= lvl)
|
|
708
|
+
levelCounts.push(0);
|
|
709
|
+
levelCounts[lvl] = (levelCounts[lvl] ?? 0) + 1;
|
|
710
|
+
levelsTotal += 1;
|
|
711
|
+
}
|
|
712
|
+
if (s.effectiveMaxChildren > 0) {
|
|
713
|
+
const c = Math.max(0, Math.floor(s.children));
|
|
714
|
+
childrenMax = Math.max(childrenMax, c);
|
|
715
|
+
while (childCounts.length <= c)
|
|
716
|
+
childCounts.push(0);
|
|
717
|
+
childCounts[c] = (childCounts[c] ?? 0) + 1;
|
|
718
|
+
childrenTotal += 1;
|
|
719
|
+
}
|
|
720
|
+
if (s.level > 0 && !s.parent) {
|
|
721
|
+
orphansOnline += 1;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const levelP95 = levelsTotal > 0
|
|
725
|
+
? quantileFromCounts(levelCounts, levelsMax, levelsTotal, 0.95)
|
|
726
|
+
: NaN;
|
|
727
|
+
const childrenP95 = childrenTotal > 0
|
|
728
|
+
? quantileFromCounts(childCounts, childrenMax, childrenTotal, 0.95)
|
|
729
|
+
: NaN;
|
|
730
|
+
return {
|
|
731
|
+
onlineJoined,
|
|
732
|
+
orphansOnline,
|
|
733
|
+
levelP95,
|
|
734
|
+
childrenP95,
|
|
735
|
+
};
|
|
736
|
+
};
|
|
737
|
+
const maintenanceLoop = async () => {
|
|
738
|
+
if (!wantsMaintenance)
|
|
739
|
+
return;
|
|
740
|
+
const sampleEveryMs = Math.max(100, Math.min(1_000, Math.floor(params.nodes / 10)));
|
|
741
|
+
const baselineOrphans = formationTree.treeOrphans;
|
|
742
|
+
const baselineLevelP95 = formationTree.treeLevelP95;
|
|
743
|
+
const baselineChildrenP95 = formationTree.treeChildrenP95;
|
|
744
|
+
let lastAt = Date.now();
|
|
745
|
+
let lastOrphans = 0;
|
|
746
|
+
const startAt = lastAt;
|
|
747
|
+
try {
|
|
748
|
+
for (;;) {
|
|
749
|
+
if (maintenanceSignal.aborted)
|
|
750
|
+
return;
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
const dt = Math.max(0, now - lastAt);
|
|
753
|
+
if (dt > 0) {
|
|
754
|
+
maintOrphanAreaMs += lastOrphans * dt;
|
|
755
|
+
}
|
|
756
|
+
lastAt = now;
|
|
757
|
+
const snap = sampleMaintenance(now);
|
|
758
|
+
lastOrphans = snap.orphansOnline;
|
|
759
|
+
maintMaxOrphans = Math.max(maintMaxOrphans, snap.orphansOnline);
|
|
760
|
+
maintSamples += 1;
|
|
761
|
+
if (Number.isFinite(snap.levelP95)) {
|
|
762
|
+
maintLevelP95DriftMax = Math.max(maintLevelP95DriftMax, Math.abs(snap.levelP95 - baselineLevelP95));
|
|
763
|
+
}
|
|
764
|
+
if (Number.isFinite(snap.childrenP95)) {
|
|
765
|
+
maintChildrenP95DriftMax = Math.max(maintChildrenP95DriftMax, Math.abs(snap.childrenP95 - baselineChildrenP95));
|
|
766
|
+
}
|
|
767
|
+
if (snap.orphansOnline <= baselineOrphans &&
|
|
768
|
+
pendingRecoveryStarts.length > 0) {
|
|
769
|
+
for (const s of pendingRecoveryStarts)
|
|
770
|
+
recoveryDurations.push(now - s);
|
|
771
|
+
pendingRecoveryStarts.length = 0;
|
|
772
|
+
}
|
|
773
|
+
await delay(sampleEveryMs, { signal: maintenanceSignal });
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
const endAt = Date.now();
|
|
778
|
+
const dt = Math.max(0, endAt - lastAt);
|
|
779
|
+
if (dt > 0) {
|
|
780
|
+
maintOrphanAreaMs += lastOrphans * dt;
|
|
781
|
+
}
|
|
782
|
+
maintDurationMs = Math.max(0, endAt - startAt);
|
|
783
|
+
for (const s of pendingRecoveryStarts)
|
|
784
|
+
recoveryDurations.push(endAt - s);
|
|
785
|
+
pendingRecoveryStarts.length = 0;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
const churnLoop = async () => {
|
|
789
|
+
const everyMs = Math.max(0, Math.floor(params.churnEveryMs));
|
|
790
|
+
const downMs = Math.max(0, Math.floor(params.churnDownMs));
|
|
791
|
+
const fraction = Math.max(0, Math.min(1, Number(params.churnFraction)));
|
|
792
|
+
if (everyMs <= 0 || downMs <= 0 || fraction <= 0)
|
|
793
|
+
return;
|
|
794
|
+
if (joinedSubscriberIndices.length === 0)
|
|
795
|
+
return;
|
|
796
|
+
for (;;) {
|
|
797
|
+
if (churnSignal.aborted)
|
|
798
|
+
return;
|
|
799
|
+
await delay(everyMs, { signal: churnSignal });
|
|
800
|
+
if (churnSignal.aborted)
|
|
801
|
+
return;
|
|
802
|
+
const target = Math.min(joinedSubscriberIndices.length, Math.max(1, Math.floor(joinedSubscriberIndices.length * fraction)));
|
|
803
|
+
const chosen = new Set();
|
|
804
|
+
const maxAttempts = Math.max(10, target * 20);
|
|
805
|
+
for (let tries = 0; chosen.size < target && tries < maxAttempts; tries++) {
|
|
806
|
+
const idx = joinedSubscriberIndices[int(rng, joinedSubscriberIndices.length)];
|
|
807
|
+
const peer = session.peers[idx];
|
|
808
|
+
if (network.isPeerOffline(peer.peerId))
|
|
809
|
+
continue;
|
|
810
|
+
chosen.add(idx);
|
|
811
|
+
}
|
|
812
|
+
if (chosen.size === 0)
|
|
813
|
+
continue;
|
|
814
|
+
churnEvents += 1;
|
|
815
|
+
churnedPeersTotal += chosen.size;
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
if (wantsMaintenance)
|
|
818
|
+
pendingRecoveryStarts.push(now);
|
|
819
|
+
await Promise.all([...chosen].map(async (idx) => {
|
|
820
|
+
const peer = session.peers[idx];
|
|
821
|
+
network.setPeerOffline(peer.peerId, downMs, now);
|
|
822
|
+
await network.disconnectPeer(peer.peerId);
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const publishStart = Date.now();
|
|
827
|
+
if (wantsMaintenance) {
|
|
828
|
+
for (const p of session.peers) {
|
|
829
|
+
const nodeHash = p.services.fanout.publicKeyHash;
|
|
830
|
+
const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
|
|
831
|
+
reparentBaselineByHash.set(nodeHash, m.reparentDisconnect + m.reparentStale + m.reparentKicked);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const maintenancePromise = maintenanceLoop().catch(() => { });
|
|
835
|
+
const churnPromise = churnLoop().catch(() => { });
|
|
836
|
+
try {
|
|
837
|
+
for (let seq = 0; seq < params.messages; seq++) {
|
|
838
|
+
if (timeoutSignal.aborted) {
|
|
839
|
+
throw timeoutSignal.reason ?? new Error("fanout-tree-sim aborted");
|
|
840
|
+
}
|
|
841
|
+
publishAt.set(seq, Date.now());
|
|
842
|
+
await root.publishData(params.topic, rootId, payload);
|
|
843
|
+
if (params.intervalMs > 0) {
|
|
844
|
+
await delay(params.intervalMs, { signal: timeoutSignal });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
finally {
|
|
849
|
+
churnController.abort();
|
|
850
|
+
await churnPromise;
|
|
851
|
+
churnSignal.clear?.();
|
|
852
|
+
}
|
|
853
|
+
const publishDone = Date.now();
|
|
854
|
+
// Signal end-of-stream so subscribers can detect tail gaps and repair.
|
|
855
|
+
if (params.repair && params.messages > 0) {
|
|
856
|
+
await root.publishEnd(params.topic, rootId, params.messages);
|
|
857
|
+
}
|
|
858
|
+
if (params.settleMs > 0) {
|
|
859
|
+
await delay(params.settleMs, { signal: timeoutSignal });
|
|
860
|
+
}
|
|
861
|
+
maintenanceController.abort();
|
|
862
|
+
await maintenancePromise;
|
|
863
|
+
maintenanceSignal.clear?.();
|
|
864
|
+
recoveryDurations.sort((a, b) => a - b);
|
|
865
|
+
const maintRecoveryCount = recoveryDurations.length;
|
|
866
|
+
const maintRecoveryP50Ms = maintRecoveryCount > 0 ? quantile(recoveryDurations, 0.5) : 0;
|
|
867
|
+
const maintRecoveryP95Ms = maintRecoveryCount > 0 ? quantile(recoveryDurations, 0.95) : 0;
|
|
868
|
+
if (wantsMaintenance) {
|
|
869
|
+
for (const p of session.peers) {
|
|
870
|
+
const nodeHash = p.services.fanout.publicKeyHash;
|
|
871
|
+
const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
|
|
872
|
+
const total = m.reparentDisconnect + m.reparentStale + m.reparentKicked;
|
|
873
|
+
const base = reparentBaselineByHash.get(nodeHash) ?? 0;
|
|
874
|
+
const delta = Math.max(0, total - base);
|
|
875
|
+
maintReparentsTotal += delta;
|
|
876
|
+
maintMaxReparentsPerPeer = Math.max(maintMaxReparentsPerPeer, delta);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const maintDurationMin = maintDurationMs / 60_000;
|
|
880
|
+
const maintReparentsPerMin = maintDurationMin > 0 ? maintReparentsTotal / maintDurationMin : 0;
|
|
881
|
+
const maintOrphanArea = maintOrphanAreaMs / 1_000;
|
|
882
|
+
// Compute delivery
|
|
883
|
+
const expected = joinedCount * params.messages;
|
|
884
|
+
let delivered = 0;
|
|
885
|
+
for (const c of receivedCounts)
|
|
886
|
+
delivered += c;
|
|
887
|
+
const joinedPct = subscriberCount === 0 ? 100 : (100 * joinedCount) / subscriberCount;
|
|
888
|
+
const deliveredPct = expected === 0 ? 100 : (100 * delivered) / expected;
|
|
889
|
+
const deliveredWithinDeadlinePct = expected === 0 ? 100 : (100 * deliveredWithinDeadline) / expected;
|
|
890
|
+
// Internal protocol drops (from upload shaping / overload logic)
|
|
891
|
+
let droppedForwardsTotal = 0;
|
|
892
|
+
let droppedForwardsMax = 0;
|
|
893
|
+
let droppedForwardsMaxNode;
|
|
894
|
+
for (const p of session.peers) {
|
|
895
|
+
const s = p.services.fanout.getChannelStats(params.topic, rootId);
|
|
896
|
+
if (!s)
|
|
897
|
+
continue;
|
|
898
|
+
droppedForwardsTotal += s.droppedForwards;
|
|
899
|
+
if (s.droppedForwards > droppedForwardsMax) {
|
|
900
|
+
droppedForwardsMax = s.droppedForwards;
|
|
901
|
+
droppedForwardsMaxNode = p.services.fanout.publicKeyHash;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
// Tree shape stats (best-effort)
|
|
905
|
+
const tree = computeTreeShapeStats();
|
|
906
|
+
const treeMaxLevel = tree.treeMaxLevel;
|
|
907
|
+
const treeLevelP95 = tree.treeLevelP95;
|
|
908
|
+
const treeLevelAvg = tree.treeLevelAvg;
|
|
909
|
+
const treeOrphans = tree.treeOrphans;
|
|
910
|
+
const treeChildrenP95 = tree.treeChildrenP95;
|
|
911
|
+
const treeChildrenMax = tree.treeChildrenMax;
|
|
912
|
+
const treeRootChildren = tree.treeRootChildren;
|
|
913
|
+
// Stream backpressure stats (queued bytes)
|
|
914
|
+
const queuedBytesSamples = [];
|
|
915
|
+
let streamQueuedBytesTotal = 0;
|
|
916
|
+
let streamQueuedBytesMax = 0;
|
|
917
|
+
let streamQueuedBytesMaxNode;
|
|
918
|
+
for (const p of session.peers) {
|
|
919
|
+
const q = Math.max(0, Math.floor(p.services.fanout.getQueuedBytes()));
|
|
920
|
+
streamQueuedBytesTotal += q;
|
|
921
|
+
queuedBytesSamples.push(q);
|
|
922
|
+
if (q > streamQueuedBytesMax) {
|
|
923
|
+
streamQueuedBytesMax = q;
|
|
924
|
+
streamQueuedBytesMaxNode = p.services.fanout.publicKeyHash;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
queuedBytesSamples.sort((a, b) => a - b);
|
|
928
|
+
const streamQueuedBytesP95 = queuedBytesSamples.length > 0 ? quantile(queuedBytesSamples, 0.95) : 0;
|
|
929
|
+
const streamQueuedBytesByLane = [];
|
|
930
|
+
for (const p of session.peers) {
|
|
931
|
+
for (const ps of p.services.fanout.peers.values()) {
|
|
932
|
+
const byLane =
|
|
933
|
+
// @ts-ignore - optional debug helper (may not exist in built typings yet)
|
|
934
|
+
ps.getOutboundQueuedBytesByLane?.() ?? [0, 0, 0, 0];
|
|
935
|
+
for (let lane = 0; lane < byLane.length; lane++) {
|
|
936
|
+
streamQueuedBytesByLane[lane] =
|
|
937
|
+
(streamQueuedBytesByLane[lane] ?? 0) + (byLane[lane] ?? 0);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
942
|
+
streamQueuedBytesByLane[lane] = streamQueuedBytesByLane[lane] ?? 0;
|
|
943
|
+
}
|
|
944
|
+
// Peak upload vs cap (best-effort; counts framed bytes, including overhead)
|
|
945
|
+
const uploadCapByHash = new Map();
|
|
946
|
+
if (params.rootUploadLimitBps > 0) {
|
|
947
|
+
uploadCapByHash.set(root.publicKeyHash, params.rootUploadLimitBps);
|
|
948
|
+
}
|
|
949
|
+
for (let i = 0; i < subscriberIndices.length; i++) {
|
|
950
|
+
if (!joined[i])
|
|
951
|
+
continue;
|
|
952
|
+
const idx = subscriberIndices[i];
|
|
953
|
+
if (!relaySet.has(idx))
|
|
954
|
+
continue;
|
|
955
|
+
if (params.relayUploadLimitBps <= 0)
|
|
956
|
+
continue;
|
|
957
|
+
const h = session.peers[idx].services.fanout.publicKeyHash;
|
|
958
|
+
uploadCapByHash.set(h, params.relayUploadLimitBps);
|
|
959
|
+
}
|
|
960
|
+
let maxUploadFracPct = 0;
|
|
961
|
+
let maxUploadNode;
|
|
962
|
+
let maxUploadBps = 0;
|
|
963
|
+
for (const [hash, pm] of session.network.peerMetricsByHash) {
|
|
964
|
+
const cap = uploadCapByHash.get(hash);
|
|
965
|
+
if (!cap || cap <= 0)
|
|
966
|
+
continue;
|
|
967
|
+
const frac = (100 * pm.maxBytesPerSecond) / cap;
|
|
968
|
+
if (frac > maxUploadFracPct) {
|
|
969
|
+
maxUploadFracPct = frac;
|
|
970
|
+
maxUploadNode = hash;
|
|
971
|
+
maxUploadBps = pm.maxBytesPerSecond;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
deliveredSamples.sort((a, b) => a - b);
|
|
975
|
+
let protocolControlSends = 0;
|
|
976
|
+
let protocolControlBytesSent = 0;
|
|
977
|
+
let protocolControlBytesSentJoin = 0;
|
|
978
|
+
let protocolControlBytesSentRepair = 0;
|
|
979
|
+
let protocolControlBytesSentTracker = 0;
|
|
980
|
+
let protocolControlReceives = 0;
|
|
981
|
+
let protocolControlBytesReceived = 0;
|
|
982
|
+
let protocolDataSends = 0;
|
|
983
|
+
let protocolDataPayloadBytesSent = 0;
|
|
984
|
+
let protocolDataReceives = 0;
|
|
985
|
+
let protocolDataPayloadBytesReceived = 0;
|
|
986
|
+
let protocolRepairReqSent = 0;
|
|
987
|
+
let protocolFetchReqSent = 0;
|
|
988
|
+
let protocolIHaveSent = 0;
|
|
989
|
+
let protocolTrackerFeedbackSent = 0;
|
|
990
|
+
let protocolCacheHitsServed = 0;
|
|
991
|
+
let protocolHoleFillsFromNeighbor = 0;
|
|
992
|
+
let protocolRouteCacheHits = 0;
|
|
993
|
+
let protocolRouteCacheMisses = 0;
|
|
994
|
+
let protocolRouteCacheExpirations = 0;
|
|
995
|
+
let protocolRouteCacheEvictions = 0;
|
|
996
|
+
let protocolRouteProxyQueries = 0;
|
|
997
|
+
let protocolRouteProxyTimeouts = 0;
|
|
998
|
+
let protocolRouteProxyFanout = 0;
|
|
999
|
+
let staleForwardsDroppedTotal = 0;
|
|
1000
|
+
let staleForwardsDroppedMax = 0;
|
|
1001
|
+
let staleForwardsDroppedMaxNode;
|
|
1002
|
+
let dataWriteDropsTotal = 0;
|
|
1003
|
+
let dataWriteDropsMax = 0;
|
|
1004
|
+
let dataWriteDropsMaxNode;
|
|
1005
|
+
let reparentDisconnectTotal = 0;
|
|
1006
|
+
let reparentStaleTotal = 0;
|
|
1007
|
+
let reparentKickedTotal = 0;
|
|
1008
|
+
let earningsTotal = 0;
|
|
1009
|
+
const earningsByHash = new Map();
|
|
1010
|
+
for (const p of session.peers) {
|
|
1011
|
+
const nodeHash = p.services.fanout.publicKeyHash;
|
|
1012
|
+
const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
|
|
1013
|
+
staleForwardsDroppedTotal += m.staleForwardsDropped;
|
|
1014
|
+
if (m.staleForwardsDropped > staleForwardsDroppedMax) {
|
|
1015
|
+
staleForwardsDroppedMax = m.staleForwardsDropped;
|
|
1016
|
+
staleForwardsDroppedMaxNode = nodeHash;
|
|
1017
|
+
}
|
|
1018
|
+
dataWriteDropsTotal += m.dataWriteDrops;
|
|
1019
|
+
if (m.dataWriteDrops > dataWriteDropsMax) {
|
|
1020
|
+
dataWriteDropsMax = m.dataWriteDrops;
|
|
1021
|
+
dataWriteDropsMaxNode = nodeHash;
|
|
1022
|
+
}
|
|
1023
|
+
reparentDisconnectTotal += m.reparentDisconnect;
|
|
1024
|
+
reparentStaleTotal += m.reparentStale;
|
|
1025
|
+
reparentKickedTotal += m.reparentKicked;
|
|
1026
|
+
earningsTotal += m.earnings;
|
|
1027
|
+
earningsByHash.set(nodeHash, m.earnings);
|
|
1028
|
+
protocolControlSends += m.controlSends;
|
|
1029
|
+
protocolControlBytesSent += m.controlBytesSent;
|
|
1030
|
+
protocolControlBytesSentJoin += m.controlBytesSentJoin;
|
|
1031
|
+
protocolControlBytesSentRepair += m.controlBytesSentRepair;
|
|
1032
|
+
protocolControlBytesSentTracker += m.controlBytesSentTracker;
|
|
1033
|
+
protocolControlReceives += m.controlReceives;
|
|
1034
|
+
protocolControlBytesReceived += m.controlBytesReceived;
|
|
1035
|
+
protocolDataSends += m.dataSends;
|
|
1036
|
+
protocolDataPayloadBytesSent += m.dataPayloadBytesSent;
|
|
1037
|
+
protocolDataReceives += m.dataReceives;
|
|
1038
|
+
protocolDataPayloadBytesReceived += m.dataPayloadBytesReceived;
|
|
1039
|
+
protocolRepairReqSent += m.repairReqSent;
|
|
1040
|
+
protocolFetchReqSent += m.fetchReqSent;
|
|
1041
|
+
protocolIHaveSent += m.ihaveSent;
|
|
1042
|
+
protocolTrackerFeedbackSent += m.trackerFeedbackSent;
|
|
1043
|
+
protocolCacheHitsServed += m.cacheHitsServed;
|
|
1044
|
+
protocolHoleFillsFromNeighbor += m.holeFillsFromNeighbor;
|
|
1045
|
+
protocolRouteCacheHits += m.routeCacheHits;
|
|
1046
|
+
protocolRouteCacheMisses += m.routeCacheMisses;
|
|
1047
|
+
protocolRouteCacheExpirations += m.routeCacheExpirations;
|
|
1048
|
+
protocolRouteCacheEvictions += m.routeCacheEvictions;
|
|
1049
|
+
protocolRouteProxyQueries += m.routeProxyQueries;
|
|
1050
|
+
protocolRouteProxyTimeouts += m.routeProxyTimeouts;
|
|
1051
|
+
protocolRouteProxyFanout += m.routeProxyFanout;
|
|
1052
|
+
}
|
|
1053
|
+
const relayHashes = [...uploadCapByHash.keys()];
|
|
1054
|
+
const relayEarnings = relayHashes.map((h) => earningsByHash.get(h) ?? 0).sort((a, b) => a - b);
|
|
1055
|
+
const earningsRelayCount = relayEarnings.length;
|
|
1056
|
+
const earningsRelayP50 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.5) : 0;
|
|
1057
|
+
const earningsRelayP95 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.95) : 0;
|
|
1058
|
+
const earningsRelayMax = earningsRelayCount > 0 ? relayEarnings[relayEarnings.length - 1] : 0;
|
|
1059
|
+
const idealPayloadBytes = expected * Math.max(0, params.msgSize);
|
|
1060
|
+
const overheadFactorData = idealPayloadBytes <= 0
|
|
1061
|
+
? 1
|
|
1062
|
+
: protocolDataPayloadBytesSent / idealPayloadBytes;
|
|
1063
|
+
const deliveredPayloadBytes = delivered * Math.max(0, params.msgSize);
|
|
1064
|
+
const controlBpp = deliveredPayloadBytes <= 0 ? 0 : protocolControlBytesSent / deliveredPayloadBytes;
|
|
1065
|
+
const trackerBpp = deliveredPayloadBytes <= 0
|
|
1066
|
+
? 0
|
|
1067
|
+
: protocolControlBytesSentTracker / deliveredPayloadBytes;
|
|
1068
|
+
const repairBpp = deliveredPayloadBytes <= 0
|
|
1069
|
+
? 0
|
|
1070
|
+
: protocolControlBytesSentRepair / deliveredPayloadBytes;
|
|
1071
|
+
return {
|
|
1072
|
+
params,
|
|
1073
|
+
bootstrapCount: bootstrapIndices.length,
|
|
1074
|
+
subscriberCount,
|
|
1075
|
+
relayCount: relaySet.size,
|
|
1076
|
+
joinedCount,
|
|
1077
|
+
joinedPct,
|
|
1078
|
+
joinMs: joinDone - joinStart,
|
|
1079
|
+
attachSamples,
|
|
1080
|
+
attachP50,
|
|
1081
|
+
attachP95,
|
|
1082
|
+
attachP99,
|
|
1083
|
+
attachMax,
|
|
1084
|
+
formationTreeMaxLevel: formationTree.treeMaxLevel,
|
|
1085
|
+
formationTreeLevelP95: formationTree.treeLevelP95,
|
|
1086
|
+
formationTreeLevelAvg: formationTree.treeLevelAvg,
|
|
1087
|
+
formationTreeOrphans: formationTree.treeOrphans,
|
|
1088
|
+
formationTreeChildrenP95: formationTree.treeChildrenP95,
|
|
1089
|
+
formationTreeChildrenMax: formationTree.treeChildrenMax,
|
|
1090
|
+
formationTreeRootChildren: formationTree.treeRootChildren,
|
|
1091
|
+
formationUnderlayEdges,
|
|
1092
|
+
formationUnderlayDistP95,
|
|
1093
|
+
formationUnderlayDistMax,
|
|
1094
|
+
formationStretchP95,
|
|
1095
|
+
formationStretchMax,
|
|
1096
|
+
formationScore,
|
|
1097
|
+
publishMs: publishDone - publishStart,
|
|
1098
|
+
expected,
|
|
1099
|
+
delivered,
|
|
1100
|
+
deliveredPct,
|
|
1101
|
+
deliveredWithinDeadline,
|
|
1102
|
+
deliveredWithinDeadlinePct,
|
|
1103
|
+
duplicates,
|
|
1104
|
+
latencySamples: deliveredSamples.length,
|
|
1105
|
+
latencyP50: quantile(deliveredSamples, 0.5),
|
|
1106
|
+
latencyP95: quantile(deliveredSamples, 0.95),
|
|
1107
|
+
latencyP99: quantile(deliveredSamples, 0.99),
|
|
1108
|
+
latencyMax: deliveredSamples[deliveredSamples.length - 1] ?? NaN,
|
|
1109
|
+
droppedForwardsTotal,
|
|
1110
|
+
droppedForwardsMax,
|
|
1111
|
+
droppedForwardsMaxNode,
|
|
1112
|
+
staleForwardsDroppedTotal,
|
|
1113
|
+
staleForwardsDroppedMax,
|
|
1114
|
+
staleForwardsDroppedMaxNode,
|
|
1115
|
+
dataWriteDropsTotal,
|
|
1116
|
+
dataWriteDropsMax,
|
|
1117
|
+
dataWriteDropsMaxNode,
|
|
1118
|
+
reparentDisconnectTotal,
|
|
1119
|
+
reparentStaleTotal,
|
|
1120
|
+
reparentKickedTotal,
|
|
1121
|
+
treeMaxLevel,
|
|
1122
|
+
treeLevelP95,
|
|
1123
|
+
treeLevelAvg,
|
|
1124
|
+
treeOrphans,
|
|
1125
|
+
treeChildrenP95,
|
|
1126
|
+
treeChildrenMax,
|
|
1127
|
+
treeRootChildren,
|
|
1128
|
+
maxUploadBps,
|
|
1129
|
+
maxUploadFracPct,
|
|
1130
|
+
maxUploadNode,
|
|
1131
|
+
streamQueuedBytesTotal,
|
|
1132
|
+
streamQueuedBytesMax,
|
|
1133
|
+
streamQueuedBytesP95,
|
|
1134
|
+
streamQueuedBytesMaxNode,
|
|
1135
|
+
streamQueuedBytesByLane,
|
|
1136
|
+
churnEvents,
|
|
1137
|
+
churnedPeersTotal,
|
|
1138
|
+
maintDurationMs,
|
|
1139
|
+
maintSamples,
|
|
1140
|
+
maintMaxOrphans,
|
|
1141
|
+
maintOrphanArea,
|
|
1142
|
+
maintRecoveryCount,
|
|
1143
|
+
maintRecoveryP50Ms,
|
|
1144
|
+
maintRecoveryP95Ms,
|
|
1145
|
+
maintReparentsPerMin,
|
|
1146
|
+
maintMaxReparentsPerPeer,
|
|
1147
|
+
maintLevelP95DriftMax,
|
|
1148
|
+
maintChildrenP95DriftMax,
|
|
1149
|
+
overheadFactorData,
|
|
1150
|
+
controlBpp,
|
|
1151
|
+
trackerBpp,
|
|
1152
|
+
repairBpp,
|
|
1153
|
+
earningsTotal,
|
|
1154
|
+
earningsRelayCount,
|
|
1155
|
+
earningsRelayP50,
|
|
1156
|
+
earningsRelayP95,
|
|
1157
|
+
earningsRelayMax,
|
|
1158
|
+
protocolControlSends,
|
|
1159
|
+
protocolControlBytesSent,
|
|
1160
|
+
protocolControlBytesSentJoin,
|
|
1161
|
+
protocolControlBytesSentRepair,
|
|
1162
|
+
protocolControlBytesSentTracker,
|
|
1163
|
+
protocolControlReceives,
|
|
1164
|
+
protocolControlBytesReceived,
|
|
1165
|
+
protocolDataSends,
|
|
1166
|
+
protocolDataPayloadBytesSent,
|
|
1167
|
+
protocolDataReceives,
|
|
1168
|
+
protocolDataPayloadBytesReceived,
|
|
1169
|
+
protocolRepairReqSent,
|
|
1170
|
+
protocolFetchReqSent,
|
|
1171
|
+
protocolIHaveSent,
|
|
1172
|
+
protocolTrackerFeedbackSent,
|
|
1173
|
+
protocolCacheHitsServed,
|
|
1174
|
+
protocolHoleFillsFromNeighbor,
|
|
1175
|
+
protocolRouteCacheHits,
|
|
1176
|
+
protocolRouteCacheMisses,
|
|
1177
|
+
protocolRouteCacheExpirations,
|
|
1178
|
+
protocolRouteCacheEvictions,
|
|
1179
|
+
protocolRouteProxyQueries,
|
|
1180
|
+
protocolRouteProxyTimeouts,
|
|
1181
|
+
protocolRouteProxyFanout,
|
|
1182
|
+
network: session.network.metrics,
|
|
1183
|
+
};
|
|
1184
|
+
};
|
|
1185
|
+
const result = await run();
|
|
1186
|
+
if (profileEnabled && profileCpuStart) {
|
|
1187
|
+
try {
|
|
1188
|
+
profileEld?.disable();
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
// ignore
|
|
1192
|
+
}
|
|
1193
|
+
const cpu = process.cpuUsage(profileCpuStart);
|
|
1194
|
+
const mem = process.memoryUsage();
|
|
1195
|
+
const p95 = profileEld ? profileEld.percentile(95) / 1e6 : 0;
|
|
1196
|
+
const max = profileEld ? profileEld.max / 1e6 : 0;
|
|
1197
|
+
result.profile = {
|
|
1198
|
+
cpuUserMs: cpu.user / 1_000,
|
|
1199
|
+
cpuSystemMs: cpu.system / 1_000,
|
|
1200
|
+
rssMb: mem.rss / 1e6,
|
|
1201
|
+
heapUsedMb: mem.heapUsed / 1e6,
|
|
1202
|
+
eventLoopDelayP95Ms: p95,
|
|
1203
|
+
eventLoopDelayMaxMs: max,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
finally {
|
|
1209
|
+
if (timer)
|
|
1210
|
+
clearTimeout(timer);
|
|
1211
|
+
try {
|
|
1212
|
+
profileEld?.disable();
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
// ignore
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
await session.stop();
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
// ignore teardown aborts in the shim
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
//# sourceMappingURL=fanout-tree-sim-lib.js.map
|