@peerbit/pubsub 5.2.10 → 5.3.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 (67) hide show
  1. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
  2. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
  3. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
  4. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
  5. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
  6. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
  7. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
  8. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
  9. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
  10. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
  11. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
  12. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
  13. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
  14. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
  15. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
  16. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
  17. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
  18. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
  19. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
  20. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
  21. package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
  22. package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
  23. package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
  24. package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
  25. package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
  26. package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
  27. package/dist/benchmark/fanout-tree-sim.js +369 -28
  28. package/dist/benchmark/fanout-tree-sim.js.map +1 -1
  29. package/dist/benchmark/index.js +20 -0
  30. package/dist/benchmark/index.js.map +1 -1
  31. package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
  32. package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
  33. package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
  34. package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
  35. package/dist/benchmark/pubsub-topic-sim.js +18 -0
  36. package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
  37. package/dist/src/debounced-set.d.ts +1 -1
  38. package/dist/src/debounced-set.d.ts.map +1 -1
  39. package/dist/src/debounced-set.js +5 -1
  40. package/dist/src/debounced-set.js.map +1 -1
  41. package/dist/src/fanout-tree-codec.d.ts +330 -0
  42. package/dist/src/fanout-tree-codec.d.ts.map +1 -0
  43. package/dist/src/fanout-tree-codec.js +1209 -0
  44. package/dist/src/fanout-tree-codec.js.map +1 -0
  45. package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
  46. package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
  47. package/dist/src/fanout-tree-parent-upgrade.js +135 -0
  48. package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
  49. package/dist/src/fanout-tree.d.ts +241 -73
  50. package/dist/src/fanout-tree.d.ts.map +1 -1
  51. package/dist/src/fanout-tree.js +2224 -1050
  52. package/dist/src/fanout-tree.js.map +1 -1
  53. package/dist/src/index.d.ts +57 -0
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +293 -41
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/topic-root-control-plane.d.ts +13 -0
  58. package/dist/src/topic-root-control-plane.d.ts.map +1 -1
  59. package/dist/src/topic-root-control-plane.js +45 -0
  60. package/dist/src/topic-root-control-plane.js.map +1 -1
  61. package/package.json +6 -6
  62. package/src/debounced-set.ts +5 -0
  63. package/src/fanout-tree-codec.ts +1580 -0
  64. package/src/fanout-tree-parent-upgrade.ts +353 -0
  65. package/src/fanout-tree.ts +4467 -2204
  66. package/src/index.ts +392 -56
  67. package/src/topic-root-control-plane.ts +48 -0
@@ -0,0 +1,712 @@
1
+ /**
2
+ * A/B evidence harness for proactive FanoutTree parent upgrades.
3
+ *
4
+ * This intentionally runs the same scenario and seed twice: first with the
5
+ * default stability-first behavior, then with bounded parent upgrades enabled.
6
+ */
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { dirname } from "node:path";
9
+ import { DEFAULT_PARENT_UPGRADE_SEEDS, PARENT_UPGRADE_HELP_TEXT, avgFinite, defaultEvidenceLimitsForPreset, fmt, maxFinite, parentUpgradeRuntimeOptions, parseBool01, parseCsvNumbers, parseParentUpgradePresetConfig, ratioLimit, } from "./fanout-tree-parent-upgrade-preset.js";
10
+ import { formatFanoutTreeSimResult, runFanoutTreeSim, } from "./fanout-tree-sim-lib.js";
11
+ const quantile = (values, q) => {
12
+ if (values.length === 0)
13
+ return NaN;
14
+ const pos = Math.min(values.length - 1, Math.max(0, Math.ceil(values.length * q) - 1));
15
+ return values[pos];
16
+ };
17
+ const HELP_TEXT = [
18
+ "fanout-tree-parent-upgrade-eval.ts",
19
+ "",
20
+ "Args:",
21
+ " --scenario NAME scenario to run (ci-small|ci-loss|ci-constrained|ci-live-stream|ci-idle-upgrade|ci-idle-upgrade-large|all, default: all)",
22
+ " --seeds CSV seeds to run for each scenario (default: 1,2,3)",
23
+ ...PARENT_UPGRADE_HELP_TEXT,
24
+ " --compareModes 0|1 run direct, probe, and shadow against one baseline (default: 0)",
25
+ " --streamRxDelayMs MS override scenario per-chunk inbound delay in shim",
26
+ " --jsonOut FILE write compact JSON evidence summary",
27
+ " --maxCostRatio R max treatment/base ratio for control/tracker/repair bpp (default: 1.15)",
28
+ " --maxFormationScoreDelta N absolute formation score jitter tolerated (default: 0.05)",
29
+ " --maxLiveDeadlinePctDelta N max live-flow deadline pct jitter tolerated (default: 2)",
30
+ " --maxSecondBatchLatencyP95DeltaMs N max idle second-batch p95 latency jitter tolerated (default: 3)",
31
+ " --maxSecondBatchLatencyP95DeltaRatio R max idle second-batch p95 latency relative jitter tolerated (default: 0.15)",
32
+ " --maxDataOverheadRatio R max treatment/base data payload overhead factor ratio (default: 1.05)",
33
+ " --maxProbePerUpgrade N max parent probes per successful proactive upgrade (default: preset-dependent)",
34
+ " --maxRootChildrenDelta N max root child-count increase over baseline (default: 4, default-candidate: 2)",
35
+ " --maxRootUploadPctDelta N max root upload pct-of-cap increase over baseline (default: 1)",
36
+ " --maxReparentsPerMin N max treatment reparent events per minute (default: 500)",
37
+ " --maxReparentsPerPeer N max treatment reparent events for one peer (default: 20)",
38
+ " --maxOrphanAreaRatio R max treatment/base ratio for orphan area (default: 1.15)",
39
+ " --strict 0|1 exit non-zero on evidence failure (default: 0)",
40
+ "",
41
+ "Example:",
42
+ " pnpm -C packages/transport/pubsub run bench -- fanout-tree-parent-upgrade-eval --scenario ci-small --seeds 1,2,3",
43
+ ].join("\n");
44
+ const SCENARIOS = {
45
+ "ci-small": {
46
+ nodes: 25,
47
+ bootstraps: 1,
48
+ subscribers: 20,
49
+ relayFraction: 0.3,
50
+ candidateScoringMode: "weighted",
51
+ messages: 20,
52
+ msgRate: 50,
53
+ msgSize: 64,
54
+ settleMs: 500,
55
+ deadlineMs: 500,
56
+ timeoutMs: 20_000,
57
+ repair: true,
58
+ rootUploadLimitBps: 100_000_000,
59
+ relayUploadLimitBps: 100_000_000,
60
+ rootMaxChildren: 64,
61
+ relayMaxChildren: 32,
62
+ dropDataFrameRate: 0,
63
+ },
64
+ "ci-loss": {
65
+ nodes: 40,
66
+ bootstraps: 1,
67
+ subscribers: 30,
68
+ relayFraction: 0.35,
69
+ messages: 40,
70
+ msgRate: 50,
71
+ msgSize: 64,
72
+ settleMs: 5_000,
73
+ deadlineMs: 500,
74
+ timeoutMs: 40_000,
75
+ repair: true,
76
+ rootUploadLimitBps: 100_000_000,
77
+ relayUploadLimitBps: 100_000_000,
78
+ rootMaxChildren: 64,
79
+ relayMaxChildren: 32,
80
+ neighborRepair: true,
81
+ neighborRepairPeers: 3,
82
+ dropDataFrameRate: 0.1,
83
+ churnEveryMs: 200,
84
+ churnDownMs: 100,
85
+ churnFraction: 0.05,
86
+ },
87
+ "ci-constrained": {
88
+ nodes: 55,
89
+ bootstraps: 1,
90
+ subscribers: 42,
91
+ relayFraction: 0.4,
92
+ candidateScoringMode: "weighted",
93
+ messages: 50,
94
+ msgRate: 60,
95
+ msgSize: 96,
96
+ settleMs: 5_000,
97
+ deadlineMs: 750,
98
+ timeoutMs: 50_000,
99
+ repair: true,
100
+ rootUploadLimitBps: 100_000_000,
101
+ relayUploadLimitBps: 100_000_000,
102
+ rootMaxChildren: 6,
103
+ relayMaxChildren: 4,
104
+ neighborRepair: true,
105
+ neighborRepairPeers: 3,
106
+ dropDataFrameRate: 0.05,
107
+ churnEveryMs: 250,
108
+ churnDownMs: 125,
109
+ churnFraction: 0.05,
110
+ },
111
+ "ci-live-stream": {
112
+ nodes: 60,
113
+ bootstraps: 1,
114
+ subscribers: 48,
115
+ relayFraction: 0.5,
116
+ candidateScoringMode: "weighted",
117
+ joinConcurrency: 1,
118
+ joinPhases: true,
119
+ joinPhaseSettleMs: 500,
120
+ messages: 300,
121
+ msgRate: 60,
122
+ msgSize: 256,
123
+ streamRxDelayMs: 2,
124
+ settleMs: 2_000,
125
+ deadlineMs: 750,
126
+ timeoutMs: 90_000,
127
+ trackerQueryIntervalMs: 1_000,
128
+ repair: true,
129
+ rootUploadLimitBps: 100_000_000,
130
+ relayUploadLimitBps: 100_000_000,
131
+ rootMaxChildren: 3,
132
+ relayMaxChildren: 4,
133
+ neighborRepair: true,
134
+ neighborRepairPeers: 3,
135
+ dropDataFrameRate: 0,
136
+ churnEveryMs: 500,
137
+ churnDownMs: 150,
138
+ churnFraction: 0.03,
139
+ lateRootConnectAfterMs: 1_000,
140
+ lateRootDuringPublish: true,
141
+ lateRootMaxChildren: 16,
142
+ lateRootConnectFraction: 0.5,
143
+ },
144
+ "ci-idle-upgrade": {
145
+ nodes: 45,
146
+ bootstraps: 1,
147
+ subscribers: 36,
148
+ relayFraction: 0.5,
149
+ candidateScoringMode: "weighted",
150
+ joinConcurrency: 1,
151
+ joinPhases: true,
152
+ joinPhaseSettleMs: 500,
153
+ messages: 20,
154
+ secondBatchMessages: 80,
155
+ secondBatchSettleMs: 1_000,
156
+ msgRate: 50,
157
+ msgSize: 64,
158
+ streamRxDelayMs: 3,
159
+ settleMs: 10_000,
160
+ deadlineMs: 500,
161
+ timeoutMs: 60_000,
162
+ trackerQueryIntervalMs: 1_000,
163
+ repair: true,
164
+ rootUploadLimitBps: 100_000_000,
165
+ relayUploadLimitBps: 100_000_000,
166
+ rootMaxChildren: 2,
167
+ relayMaxChildren: 4,
168
+ dropDataFrameRate: 0,
169
+ churnEveryMs: 0,
170
+ lateRootConnectAfterMs: 1_000,
171
+ lateRootMaxChildren: 12,
172
+ lateRootConnectFraction: 0.45,
173
+ },
174
+ "ci-idle-upgrade-large": {
175
+ nodes: 90,
176
+ bootstraps: 1,
177
+ subscribers: 72,
178
+ relayFraction: 0.5,
179
+ candidateScoringMode: "weighted",
180
+ joinConcurrency: 1,
181
+ joinPhases: true,
182
+ joinPhaseSettleMs: 500,
183
+ messages: 20,
184
+ secondBatchMessages: 80,
185
+ secondBatchSettleMs: 1_500,
186
+ msgRate: 50,
187
+ msgSize: 64,
188
+ streamRxDelayMs: 3,
189
+ settleMs: 12_000,
190
+ deadlineMs: 500,
191
+ timeoutMs: 120_000,
192
+ trackerQueryIntervalMs: 1_000,
193
+ repair: true,
194
+ rootUploadLimitBps: 100_000_000,
195
+ relayUploadLimitBps: 100_000_000,
196
+ rootMaxChildren: 3,
197
+ relayMaxChildren: 4,
198
+ dropDataFrameRate: 0,
199
+ churnEveryMs: 0,
200
+ lateRootConnectAfterMs: 1_000,
201
+ lateRootMaxChildren: 24,
202
+ lateRootConnectFraction: 0.4,
203
+ },
204
+ };
205
+ const parseScenarios = (value) => {
206
+ if (!value || value === "all") {
207
+ return ["ci-small", "ci-loss", "ci-constrained", "ci-idle-upgrade"];
208
+ }
209
+ const parsed = value
210
+ .split(",")
211
+ .map((part) => part.trim())
212
+ .filter((part) => part === "ci-small" ||
213
+ part === "ci-loss" ||
214
+ part === "ci-constrained" ||
215
+ part === "ci-live-stream" ||
216
+ part === "ci-idle-upgrade" ||
217
+ part === "ci-idle-upgrade-large");
218
+ if (parsed.length === 0) {
219
+ throw new Error(`Unknown scenario: ${value}`);
220
+ }
221
+ return parsed;
222
+ };
223
+ const parseArgs = (argv) => {
224
+ const get = (key) => {
225
+ const idx = argv.indexOf(key);
226
+ return idx === -1 ? undefined : argv[idx + 1];
227
+ };
228
+ if (argv.includes("--help") || argv.includes("-h")) {
229
+ console.log(HELP_TEXT);
230
+ process.exit(0);
231
+ }
232
+ const parentUpgradeConfig = parseParentUpgradePresetConfig(get);
233
+ const evidenceDefaults = defaultEvidenceLimitsForPreset(parentUpgradeConfig.parentUpgradePreset, "single");
234
+ return {
235
+ scenarios: parseScenarios(get("--scenario")),
236
+ seeds: parseCsvNumbers(get("--seeds"), DEFAULT_PARENT_UPGRADE_SEEDS),
237
+ ...parentUpgradeConfig,
238
+ compareModes: parseBool01(get("--compareModes"), false),
239
+ streamRxDelayMs: get("--streamRxDelayMs") == null
240
+ ? undefined
241
+ : Number(get("--streamRxDelayMs")),
242
+ jsonOut: get("--jsonOut"),
243
+ maxCostRatio: Number(get("--maxCostRatio") ?? 1.15),
244
+ maxFormationScoreDelta: Number(get("--maxFormationScoreDelta") ?? 0.05),
245
+ maxLiveDeadlinePctDelta: Number(get("--maxLiveDeadlinePctDelta") ?? 2),
246
+ maxSecondBatchLatencyP95DeltaMs: Number(get("--maxSecondBatchLatencyP95DeltaMs") ?? 3),
247
+ maxSecondBatchLatencyP95DeltaRatio: Number(get("--maxSecondBatchLatencyP95DeltaRatio") ?? 0.15),
248
+ maxDataOverheadRatio: Number(get("--maxDataOverheadRatio") ?? 1.05),
249
+ maxProbePerUpgrade: Number(get("--maxProbePerUpgrade") ?? evidenceDefaults.maxProbePerUpgrade),
250
+ maxRootChildrenDelta: Number(get("--maxRootChildrenDelta") ?? evidenceDefaults.maxRootChildrenDelta),
251
+ maxRootUploadPctDelta: Number(get("--maxRootUploadPctDelta") ?? evidenceDefaults.maxRootUploadPctDelta),
252
+ maxReparentsPerMin: Number(get("--maxReparentsPerMin") ?? 500),
253
+ maxReparentsPerPeer: Number(get("--maxReparentsPerPeer") ?? 20),
254
+ maxOrphanAreaRatio: Number(get("--maxOrphanAreaRatio") ?? 1.15),
255
+ strict: parseBool01(get("--strict"), false),
256
+ };
257
+ };
258
+ const peerLatencyP95For = (result, hashes) => {
259
+ const values = [];
260
+ for (const hash of hashes) {
261
+ const value = result.secondBatchLatencyP95ByHash[hash];
262
+ if (Number.isFinite(value))
263
+ values.push(value);
264
+ }
265
+ values.sort((a, b) => a - b);
266
+ return quantile(values, 0.95);
267
+ };
268
+ const peerCoveragePct = (result, hashes) => {
269
+ if (result.subscriberCount <= 0)
270
+ return 0;
271
+ return (100 * new Set(hashes).size) / result.subscriberCount;
272
+ };
273
+ const isIdleUpgradeScenario = (scenario) => scenario === "ci-idle-upgrade" || scenario === "ci-idle-upgrade-large";
274
+ const isLiveStreamScenario = (scenario) => scenario === "ci-live-stream";
275
+ const hasLivePublishPhase = (scenario) => scenario === "ci-live-stream";
276
+ const failIfGreater = (failures, metric, baseline, upgrade, limit) => {
277
+ if (!Number.isFinite(baseline) || !Number.isFinite(upgrade))
278
+ return;
279
+ if (upgrade > limit) {
280
+ failures.push({ metric, baseline, upgrade, limit });
281
+ }
282
+ };
283
+ const failIfLess = (failures, metric, baseline, upgrade, limit) => {
284
+ if (!Number.isFinite(baseline) || !Number.isFinite(upgrade))
285
+ return;
286
+ if (upgrade < limit) {
287
+ failures.push({ metric, baseline, upgrade, limit });
288
+ }
289
+ };
290
+ const evaluateRun = (scenario, params, baseline, upgrade, args) => {
291
+ const failures = [];
292
+ const promoted = upgrade.reparentUpgradeTotal > 0;
293
+ const treeLevelP95Gain = Number.isFinite(baseline.treeLevelP95) &&
294
+ Number.isFinite(upgrade.treeLevelP95)
295
+ ? baseline.treeLevelP95 - upgrade.treeLevelP95
296
+ : NaN;
297
+ const treeLevelAvgGain = Number.isFinite(baseline.treeLevelAvg) &&
298
+ Number.isFinite(upgrade.treeLevelAvg)
299
+ ? baseline.treeLevelAvg - upgrade.treeLevelAvg
300
+ : NaN;
301
+ const usefulDepthGain = Math.max(treeLevelP95Gain, treeLevelAvgGain);
302
+ const secondBatchLatencyP95Gain = Number.isFinite(baseline.secondBatchLatencyP95) &&
303
+ Number.isFinite(upgrade.secondBatchLatencyP95)
304
+ ? baseline.secondBatchLatencyP95 - upgrade.secondBatchLatencyP95
305
+ : NaN;
306
+ const secondBatchLatencyP95SlackMs = Number.isFinite(baseline.secondBatchLatencyP95)
307
+ ? Math.max(0, args.maxSecondBatchLatencyP95DeltaMs, baseline.secondBatchLatencyP95 *
308
+ Math.max(0, args.maxSecondBatchLatencyP95DeltaRatio))
309
+ : Math.max(0, args.maxSecondBatchLatencyP95DeltaMs);
310
+ const promotedPeerBaselineSecondBatchLatencyP95 = peerLatencyP95For(baseline, upgrade.upgradedPeerHashes);
311
+ const promotedPeerUpgradeSecondBatchLatencyP95 = peerLatencyP95For(upgrade, upgrade.upgradedPeerHashes);
312
+ const promotedBranchBaselineSecondBatchLatencyP95 = peerLatencyP95For(baseline, upgrade.upgradedBranchPeerHashes);
313
+ const promotedBranchUpgradeSecondBatchLatencyP95 = peerLatencyP95For(upgrade, upgrade.upgradedBranchPeerHashes);
314
+ const promotedBranchSecondBatchLatencyP95Gain = Number.isFinite(promotedBranchBaselineSecondBatchLatencyP95) &&
315
+ Number.isFinite(promotedBranchUpgradeSecondBatchLatencyP95)
316
+ ? promotedBranchBaselineSecondBatchLatencyP95 -
317
+ promotedBranchUpgradeSecondBatchLatencyP95
318
+ : NaN;
319
+ const usefulIdleGain = Math.max(usefulDepthGain, secondBatchLatencyP95Gain, promotedBranchSecondBatchLatencyP95Gain);
320
+ const usefulPromotions = upgrade.reparentUpgradeTotal > 0 &&
321
+ (isIdleUpgradeScenario(scenario)
322
+ ? usefulIdleGain >= 1
323
+ : usefulDepthGain > 0.05)
324
+ ? upgrade.reparentUpgradeTotal
325
+ : 0;
326
+ const upgradeActivity = upgrade.reparentUpgradeTotal > 0 ||
327
+ upgrade.parentProbeReqSentTotal > 0 ||
328
+ upgrade.parentShadowStartTotal > 0;
329
+ if (hasLivePublishPhase(scenario)) {
330
+ failIfGreater(failures, "liveActiveParentProbeReqSent", 0, upgrade.publishActiveParentProbeReqSentTotal, 0);
331
+ failIfGreater(failures, "liveActiveParentShadowStart", 0, upgrade.publishActiveParentShadowStartTotal, 0);
332
+ failIfGreater(failures, "liveActiveParentShadowPromote", 0, upgrade.publishActiveParentShadowPromoteTotal, 0);
333
+ failIfGreater(failures, "liveActiveReparentUpgrade", 0, upgrade.publishActiveReparentUpgradeTotal, 0);
334
+ failIfLess(failures, "liveActiveDataGuardSkips", 0, upgrade.publishActiveReparentUpgradeSkipDataTotal, 1);
335
+ }
336
+ if (isLiveStreamScenario(scenario)) {
337
+ failIfGreater(failures, "liveTotalParentProbeReqSent", 0, upgrade.parentProbeReqSentTotal, 0);
338
+ failIfGreater(failures, "liveTotalParentShadowStart", 0, upgrade.parentShadowStartTotal, 0);
339
+ failIfGreater(failures, "liveTotalReparentUpgrade", 0, upgrade.reparentUpgradeTotal, 0);
340
+ // Live-stream default-candidate runs are no-proactive-work safety gates.
341
+ // Baseline and treatment are independent async simulations, so delivery
342
+ // and repair timing jitter is observability unless the policy actually
343
+ // sends parent-upgrade traffic.
344
+ if (!upgradeActivity) {
345
+ return failures;
346
+ }
347
+ }
348
+ if (isIdleUpgradeScenario(scenario)) {
349
+ failIfLess(failures, "idlePromotions", 0, upgrade.reparentUpgradeTotal, 1);
350
+ failIfLess(failures, "idleUsefulPromotions", 0, usefulPromotions, 1);
351
+ failIfLess(failures, "idleUsefulGain", 0, usefulIdleGain, 1);
352
+ if (upgradeActivity) {
353
+ failIfGreater(failures, "idleProbePerUpgrade", 0, upgrade.reparentUpgradeTotal > 0
354
+ ? upgrade.parentProbeReqSentTotal / upgrade.reparentUpgradeTotal
355
+ : Number.POSITIVE_INFINITY, args.maxProbePerUpgrade);
356
+ failIfGreater(failures, "idleMaxReparentsPerPeer", baseline.maintMaxReparentsPerPeer, upgrade.maintMaxReparentsPerPeer, 1);
357
+ failIfLess(failures, "idleSecondBatchDeadlinePct", baseline.secondBatchDeliveredWithinDeadlinePct, upgrade.secondBatchDeliveredWithinDeadlinePct, baseline.secondBatchDeliveredWithinDeadlinePct);
358
+ failIfGreater(failures, "idleSecondBatchLatencyP95", baseline.secondBatchLatencyP95, upgrade.secondBatchLatencyP95, baseline.secondBatchLatencyP95 + secondBatchLatencyP95SlackMs);
359
+ failIfLess(failures, "idleSecondBatchLatencyP95OrBranchGain", 0, Math.max(secondBatchLatencyP95Gain, promotedBranchSecondBatchLatencyP95Gain), 1);
360
+ failIfGreater(failures, "idlePromotedPeerSecondBatchLatencyP95", promotedPeerBaselineSecondBatchLatencyP95, promotedPeerUpgradeSecondBatchLatencyP95, promotedPeerBaselineSecondBatchLatencyP95);
361
+ failIfGreater(failures, "idlePromotedBranchSecondBatchLatencyP95", promotedBranchBaselineSecondBatchLatencyP95, promotedBranchUpgradeSecondBatchLatencyP95, promotedBranchBaselineSecondBatchLatencyP95);
362
+ }
363
+ }
364
+ if (!isIdleUpgradeScenario(scenario) &&
365
+ !isLiveStreamScenario(scenario) &&
366
+ upgrade.reparentUpgradeTotal === 0 &&
367
+ upgrade.parentProbeReqSentTotal === 0 &&
368
+ upgrade.parentShadowStartTotal === 0) {
369
+ return failures;
370
+ }
371
+ if (promoted) {
372
+ if (!isIdleUpgradeScenario(scenario)) {
373
+ failIfGreater(failures, "formationScore", baseline.formationScore, upgrade.formationScore, baseline.formationScore + args.maxFormationScoreDelta);
374
+ }
375
+ failIfGreater(failures, "treeLevelP95", baseline.treeLevelP95, upgrade.treeLevelP95, baseline.treeLevelP95);
376
+ failIfGreater(failures, "formationStretchP95", baseline.formationStretchP95, upgrade.formationStretchP95, baseline.formationStretchP95);
377
+ }
378
+ failIfLess(failures, "deliveredWithinDeadlinePct", baseline.deliveredWithinDeadlinePct, upgrade.deliveredWithinDeadlinePct, hasLivePublishPhase(scenario)
379
+ ? baseline.deliveredWithinDeadlinePct -
380
+ Math.max(0, args.maxLiveDeadlinePctDelta)
381
+ : baseline.deliveredWithinDeadlinePct);
382
+ failIfGreater(failures, "dataOverheadFactor", baseline.overheadFactorData, upgrade.overheadFactorData, ratioLimit(baseline.overheadFactorData, args.maxDataOverheadRatio, 0.01));
383
+ failIfGreater(failures, "controlBpp", baseline.controlBpp, upgrade.controlBpp, ratioLimit(baseline.controlBpp, args.maxCostRatio, 0.001));
384
+ failIfGreater(failures, "trackerBpp", baseline.trackerBpp, upgrade.trackerBpp, ratioLimit(baseline.trackerBpp, args.maxCostRatio, 0.001));
385
+ failIfGreater(failures, "repairBpp", baseline.repairBpp, upgrade.repairBpp, ratioLimit(baseline.repairBpp, args.maxCostRatio, 0.001));
386
+ failIfGreater(failures, "maintReparentsPerMin", baseline.maintReparentsPerMin, upgrade.maintReparentsPerMin, args.maxReparentsPerMin);
387
+ failIfGreater(failures, "maintMaxReparentsPerPeer", baseline.maintMaxReparentsPerPeer, upgrade.maintMaxReparentsPerPeer, args.maxReparentsPerPeer);
388
+ failIfGreater(failures, "maintOrphanArea", baseline.maintOrphanArea, upgrade.maintOrphanArea, ratioLimit(baseline.maintOrphanArea, args.maxOrphanAreaRatio, 1));
389
+ failIfGreater(failures, "treeRootChildren", baseline.treeRootChildren, upgrade.treeRootChildren, params.lateRootMaxChildren > 0
390
+ ? params.lateRootMaxChildren
391
+ : params.rootMaxChildren);
392
+ if (upgradeActivity) {
393
+ failIfGreater(failures, "rootChildrenDelta", baseline.treeRootChildren, upgrade.treeRootChildren, baseline.treeRootChildren + Math.max(0, args.maxRootChildrenDelta));
394
+ failIfGreater(failures, "rootUploadPctDelta", baseline.rootUploadFracPct, upgrade.rootUploadFracPct, baseline.rootUploadFracPct + Math.max(0, args.maxRootUploadPctDelta));
395
+ }
396
+ return failures;
397
+ };
398
+ const classifyEffect = (upgrade, failures) => {
399
+ if (upgrade.reparentUpgradeTotal === 0 &&
400
+ upgrade.parentProbeReqSentTotal === 0 &&
401
+ upgrade.parentShadowStartTotal === 0) {
402
+ return "no-op";
403
+ }
404
+ if (failures.length > 0)
405
+ return "regressed";
406
+ if (upgrade.reparentUpgradeTotal > 0)
407
+ return "promoted";
408
+ return "guarded";
409
+ };
410
+ const printComparison = (scenario, seed, mode, baseline, upgrade, failures) => {
411
+ const delta = (after, before) => Number.isFinite(after) && Number.isFinite(before) ? after - before : NaN;
412
+ const effect = classifyEffect(upgrade, failures);
413
+ const treeLevelP95Gain = delta(baseline.treeLevelP95, upgrade.treeLevelP95);
414
+ const treeLevelAvgGain = delta(baseline.treeLevelAvg, upgrade.treeLevelAvg);
415
+ const promotedPeerBaselineSecondBatchLatencyP95 = peerLatencyP95For(baseline, upgrade.upgradedPeerHashes);
416
+ const promotedPeerUpgradeSecondBatchLatencyP95 = peerLatencyP95For(upgrade, upgrade.upgradedPeerHashes);
417
+ const promotedBranchBaselineSecondBatchLatencyP95 = peerLatencyP95For(baseline, upgrade.upgradedBranchPeerHashes);
418
+ const promotedBranchUpgradeSecondBatchLatencyP95 = peerLatencyP95For(upgrade, upgrade.upgradedBranchPeerHashes);
419
+ const promotedBranchCoveragePct = peerCoveragePct(upgrade, upgrade.upgradedBranchPeerHashes);
420
+ const secondBatchLatencyP95Gain = baseline.secondBatchLatencyP95 - upgrade.secondBatchLatencyP95;
421
+ const promotedBranchSecondBatchLatencyP95Gain = promotedBranchBaselineSecondBatchLatencyP95 -
422
+ promotedBranchUpgradeSecondBatchLatencyP95;
423
+ const usefulPromotions = upgrade.reparentUpgradeTotal > 0 &&
424
+ (isIdleUpgradeScenario(scenario)
425
+ ? Math.max(treeLevelP95Gain, treeLevelAvgGain, secondBatchLatencyP95Gain, promotedBranchSecondBatchLatencyP95Gain) >= 1
426
+ : treeLevelP95Gain > 0 || treeLevelAvgGain > 0.05)
427
+ ? upgrade.reparentUpgradeTotal
428
+ : 0;
429
+ console.log([
430
+ `parent-upgrade-eval scenario=${scenario} seed=${seed} mode=${mode} viable=${failures.length === 0} effect=${effect}`,
431
+ ` formationScore ${baseline.formationScore.toFixed(2)} -> ${upgrade.formationScore.toFixed(2)} delta=${delta(upgrade.formationScore, baseline.formationScore).toFixed(2)}`,
432
+ ` treeLevelP95 ${baseline.treeLevelP95.toFixed(1)} -> ${upgrade.treeLevelP95.toFixed(1)} delta=${delta(upgrade.treeLevelP95, baseline.treeLevelP95).toFixed(1)}`,
433
+ ` treeLevelAvg ${baseline.treeLevelAvg.toFixed(2)} -> ${upgrade.treeLevelAvg.toFixed(2)} delta=${delta(upgrade.treeLevelAvg, baseline.treeLevelAvg).toFixed(2)}`,
434
+ ` formationStretchP95 ${baseline.formationStretchP95.toFixed(2)} -> ${upgrade.formationStretchP95.toFixed(2)} delta=${delta(upgrade.formationStretchP95, baseline.formationStretchP95).toFixed(2)}`,
435
+ ` deliveredWithinDeadlinePct ${baseline.deliveredWithinDeadlinePct.toFixed(2)} -> ${upgrade.deliveredWithinDeadlinePct.toFixed(2)} delta=${delta(upgrade.deliveredWithinDeadlinePct, baseline.deliveredWithinDeadlinePct).toFixed(2)}`,
436
+ ...(baseline.secondBatchExpected > 0 || upgrade.secondBatchExpected > 0
437
+ ? [
438
+ ` secondBatchDeadlinePct ${baseline.secondBatchDeliveredWithinDeadlinePct.toFixed(2)} -> ${upgrade.secondBatchDeliveredWithinDeadlinePct.toFixed(2)} delta=${delta(upgrade.secondBatchDeliveredWithinDeadlinePct, baseline.secondBatchDeliveredWithinDeadlinePct).toFixed(2)}`,
439
+ ` secondBatchLatencyP95 ${baseline.secondBatchLatencyP95.toFixed(1)} -> ${upgrade.secondBatchLatencyP95.toFixed(1)} delta=${delta(upgrade.secondBatchLatencyP95, baseline.secondBatchLatencyP95).toFixed(1)}`,
440
+ ` promotedPeerSecondBatchLatencyP95 ${promotedPeerBaselineSecondBatchLatencyP95.toFixed(1)} -> ${promotedPeerUpgradeSecondBatchLatencyP95.toFixed(1)} peers=${upgrade.upgradedPeerHashes.length}`,
441
+ ` promotedBranchSecondBatchLatencyP95 ${promotedBranchBaselineSecondBatchLatencyP95.toFixed(1)} -> ${promotedBranchUpgradeSecondBatchLatencyP95.toFixed(1)} peers=${upgrade.upgradedBranchPeerHashes.length} coverage=${promotedBranchCoveragePct.toFixed(1)}%`,
442
+ ]
443
+ : []),
444
+ ` redundancy dataFactor ${baseline.overheadFactorData.toFixed(3)} -> ${upgrade.overheadFactorData.toFixed(3)} dup ${baseline.duplicates} -> ${upgrade.duplicates}`,
445
+ ` bpp control ${baseline.controlBpp.toFixed(4)} -> ${upgrade.controlBpp.toFixed(4)} tracker ${baseline.trackerBpp.toFixed(4)} -> ${upgrade.trackerBpp.toFixed(4)} repair ${baseline.repairBpp.toFixed(4)} -> ${upgrade.repairBpp.toFixed(4)}`,
446
+ ` maintenance reparentsPerMin ${baseline.maintReparentsPerMin.toFixed(2)} -> ${upgrade.maintReparentsPerMin.toFixed(2)} maxReparentsPerPeer ${baseline.maintMaxReparentsPerPeer} -> ${upgrade.maintMaxReparentsPerPeer} orphanArea ${baseline.maintOrphanArea.toFixed(1)} -> ${upgrade.maintOrphanArea.toFixed(1)}`,
447
+ ` rootChildren ${baseline.treeRootChildren} -> ${upgrade.treeRootChildren} rootUploadPct ${baseline.rootUploadFracPct.toFixed(2)} -> ${upgrade.rootUploadFracPct.toFixed(2)} proactiveUpgrades=${upgrade.reparentUpgradeTotal} usefulPromotions=${usefulPromotions} treeLevelP95Gain=${treeLevelP95Gain.toFixed(1)} treeLevelAvgGain=${treeLevelAvgGain.toFixed(2)}`,
448
+ ...(hasLivePublishPhase(scenario)
449
+ ? [
450
+ ` publishActive upgrade=${upgrade.publishActiveReparentUpgradeTotal} dataSkips=${upgrade.publishActiveReparentUpgradeSkipDataTotal} repairSkips=${upgrade.publishActiveReparentUpgradeSkipRepairTotal} quietSkips=${upgrade.publishActiveReparentUpgradeSkipQuietTotal} probes=${upgrade.publishActiveParentProbeReqSentTotal} shadowStart=${upgrade.publishActiveParentShadowStartTotal} shadowPromote=${upgrade.publishActiveParentShadowPromoteTotal}`,
451
+ ]
452
+ : []),
453
+ ` skipped leaf=${upgrade.reparentUpgradeSkipLeafTotal} repair=${upgrade.reparentUpgradeSkipRepairTotal} data=${upgrade.reparentUpgradeSkipDataTotal} cooldown=${upgrade.reparentUpgradeSkipCooldownTotal} quiet=${upgrade.reparentUpgradeSkipQuietTotal} budget=${upgrade.reparentUpgradeSkipBudgetTotal} candidateLevel=${upgrade.reparentUpgradeSkipCandidateLevelTotal} candidateSlots=${upgrade.reparentUpgradeSkipCandidateSlotsTotal} candidatePressure=${upgrade.reparentUpgradeSkipCandidatePressureTotal} rootPressure=${upgrade.reparentUpgradeSkipRootPressureTotal}`,
454
+ ` probes req=${upgrade.parentProbeReqSentTotal}/${upgrade.parentProbeReqReceivedTotal} reply=${upgrade.parentProbeReplySentTotal}/${upgrade.parentProbeReplyReceivedTotal} noReply=${upgrade.reparentUpgradeSkipProbeNoReplyTotal} notRooted=${upgrade.reparentUpgradeSkipProbeNotRootedTotal} repair=${upgrade.reparentUpgradeSkipProbeRepairTotal} lag=${upgrade.reparentUpgradeSkipProbeLagTotal} overloaded=${upgrade.reparentUpgradeSkipProbeOverloadedTotal} cooldown=${upgrade.reparentUpgradeSkipProbeCooldownTotal}`,
455
+ ` root reservations created=${upgrade.parentUpgradeRootReservationCreatedTotal} consumed=${upgrade.parentUpgradeRootReservationConsumedTotal} rejected=${upgrade.parentUpgradeRootReservationRejectedTotal} marginRejected=${upgrade.parentUpgradeRootReservationMarginRejectedTotal} blocked=${upgrade.parentUpgradeRootReservationBlockedTotal} expired=${upgrade.parentUpgradeRootReservationExpiredTotal}`,
456
+ ` shadow start=${upgrade.parentShadowStartTotal} observe=${upgrade.parentShadowObserveTotal} promote=${upgrade.parentShadowPromoteTotal} reset=${upgrade.parentShadowResetTotal} reject noReply=${upgrade.parentShadowRejectNoReplyTotal} notRooted=${upgrade.parentShadowRejectNotRootedTotal} capacity=${upgrade.parentShadowRejectCapacityTotal} repair=${upgrade.parentShadowRejectRepairTotal} lag=${upgrade.parentShadowRejectLagTotal} overloaded=${upgrade.parentShadowRejectOverloadedTotal} level=${upgrade.parentShadowRejectLevelTotal}`,
457
+ ...(failures.length > 0
458
+ ? failures.map((f) => ` FAIL ${f.metric}: baseline=${f.baseline.toFixed(4)} upgrade=${f.upgrade.toFixed(4)} limit=${f.limit.toFixed(4)}`)
459
+ : []),
460
+ ].join("\n"));
461
+ };
462
+ const formatDelta = (after, before, digits = 2) => Number.isFinite(after) && Number.isFinite(before)
463
+ ? (after - before).toFixed(digits)
464
+ : "NaN";
465
+ const printModeTable = (scenario, seed, baseline, rows) => {
466
+ const lines = [
467
+ `parent-upgrade-mode-table scenario=${scenario} seed=${seed}`,
468
+ "mode viable effect upgrades probes shadowPromote treeP95Delta treeAvgDelta secondBatchP95Delta promotedBranchGain promotedBranchCoverage rootUploadPctDelta deadlineDelta dataFactorDelta controlBppDelta repairBppDelta orphanAreaDelta maxReparents",
469
+ ];
470
+ for (const row of rows) {
471
+ const r = row.result;
472
+ const promotedBranchBaselineSecondBatchLatencyP95 = peerLatencyP95For(baseline, r.upgradedBranchPeerHashes);
473
+ const promotedBranchUpgradeSecondBatchLatencyP95 = peerLatencyP95For(r, r.upgradedBranchPeerHashes);
474
+ lines.push([
475
+ row.mode,
476
+ row.failures.length === 0 ? "yes" : "no",
477
+ classifyEffect(r, row.failures),
478
+ r.reparentUpgradeTotal,
479
+ r.parentProbeReqSentTotal,
480
+ r.parentShadowPromoteTotal,
481
+ formatDelta(r.treeLevelP95, baseline.treeLevelP95, 1),
482
+ formatDelta(r.treeLevelAvg, baseline.treeLevelAvg, 2),
483
+ formatDelta(r.secondBatchLatencyP95, baseline.secondBatchLatencyP95, 1),
484
+ fmt(promotedBranchBaselineSecondBatchLatencyP95 -
485
+ promotedBranchUpgradeSecondBatchLatencyP95, 1),
486
+ fmt(peerCoveragePct(r, r.upgradedBranchPeerHashes), 1),
487
+ formatDelta(r.rootUploadFracPct, baseline.rootUploadFracPct, 2),
488
+ formatDelta(r.deliveredWithinDeadlinePct, baseline.deliveredWithinDeadlinePct, 2),
489
+ formatDelta(r.overheadFactorData, baseline.overheadFactorData, 3),
490
+ formatDelta(r.controlBpp, baseline.controlBpp, 4),
491
+ formatDelta(r.repairBpp, baseline.repairBpp, 4),
492
+ formatDelta(r.maintOrphanArea, baseline.maintOrphanArea, 1),
493
+ r.maintMaxReparentsPerPeer,
494
+ ].join(" "));
495
+ }
496
+ console.log(lines.join("\n"));
497
+ };
498
+ const printAggregateSummary = (samples) => {
499
+ if (samples.length === 0)
500
+ return;
501
+ const groups = new Map();
502
+ for (const sample of samples) {
503
+ const key = `${sample.scenario}:${sample.mode}`;
504
+ const group = groups.get(key) ?? [];
505
+ group.push(sample);
506
+ groups.set(key, group);
507
+ }
508
+ const lines = [
509
+ "",
510
+ "parent-upgrade-summary",
511
+ "scenario mode seeds viable effects upgrades probes activeUpgrades activeProbes activeGuardSkips treeAvgGainAvg secondBatchP95DeltaAvg/Max promotedBranchGainAvg promotedBranchCoverageAvg dataFactorDeltaPctAvg controlBppDeltaPctAvg rootChildrenDeltaMax rootUploadPctDeltaMax maxReparents failures",
512
+ ];
513
+ for (const group of groups.values()) {
514
+ const first = group[0];
515
+ const effects = {
516
+ "no-op": 0,
517
+ guarded: 0,
518
+ promoted: 0,
519
+ regressed: 0,
520
+ };
521
+ for (const sample of group) {
522
+ effects[classifyEffect(sample.upgrade, sample.failures)] += 1;
523
+ }
524
+ const treeAvgGains = group.map((sample) => sample.baseline.treeLevelAvg - sample.upgrade.treeLevelAvg);
525
+ const secondBatchP95Deltas = group.map((sample) => sample.baseline.secondBatchExpected > 0 ||
526
+ sample.upgrade.secondBatchExpected > 0
527
+ ? sample.upgrade.secondBatchLatencyP95 -
528
+ sample.baseline.secondBatchLatencyP95
529
+ : NaN);
530
+ const branchGains = group.map((sample) => {
531
+ const baseline = peerLatencyP95For(sample.baseline, sample.upgrade.upgradedBranchPeerHashes);
532
+ const upgrade = peerLatencyP95For(sample.upgrade, sample.upgrade.upgradedBranchPeerHashes);
533
+ return baseline - upgrade;
534
+ });
535
+ const branchCoverages = group.map((sample) => sample.upgrade.upgradedBranchPeerHashes.length > 0
536
+ ? peerCoveragePct(sample.upgrade, sample.upgrade.upgradedBranchPeerHashes)
537
+ : NaN);
538
+ const controlBppDeltaPct = group.map((sample) => sample.baseline.controlBpp > 0
539
+ ? (100 * (sample.upgrade.controlBpp - sample.baseline.controlBpp)) /
540
+ sample.baseline.controlBpp
541
+ : NaN);
542
+ const dataFactorDeltaPct = group.map((sample) => sample.baseline.overheadFactorData > 0
543
+ ? (100 *
544
+ (sample.upgrade.overheadFactorData -
545
+ sample.baseline.overheadFactorData)) /
546
+ sample.baseline.overheadFactorData
547
+ : NaN);
548
+ const rootChildrenDeltas = group.map((sample) => sample.upgrade.treeRootChildren - sample.baseline.treeRootChildren);
549
+ const rootUploadPctDeltas = group.map((sample) => sample.upgrade.rootUploadFracPct - sample.baseline.rootUploadFracPct);
550
+ const failureTotal = group.reduce((sum, sample) => sum + sample.failures.length, 0);
551
+ const effectText = [
552
+ `promoted=${effects.promoted}`,
553
+ `guarded=${effects.guarded}`,
554
+ `no-op=${effects["no-op"]}`,
555
+ `regressed=${effects.regressed}`,
556
+ ].join(",");
557
+ lines.push([
558
+ first.scenario,
559
+ first.mode,
560
+ group.length,
561
+ `${group.filter((sample) => sample.failures.length === 0).length}/${group.length}`,
562
+ effectText,
563
+ group.reduce((sum, sample) => sum + sample.upgrade.reparentUpgradeTotal, 0),
564
+ group.reduce((sum, sample) => sum + sample.upgrade.parentProbeReqSentTotal, 0),
565
+ group.reduce((sum, sample) => sum + sample.upgrade.publishActiveReparentUpgradeTotal, 0),
566
+ group.reduce((sum, sample) => sum + sample.upgrade.publishActiveParentProbeReqSentTotal, 0),
567
+ group.reduce((sum, sample) => sum +
568
+ sample.upgrade.publishActiveReparentUpgradeSkipDataTotal +
569
+ sample.upgrade.publishActiveReparentUpgradeSkipRepairTotal +
570
+ sample.upgrade.publishActiveReparentUpgradeSkipQuietTotal, 0),
571
+ fmt(avgFinite(treeAvgGains), 2),
572
+ `${fmt(avgFinite(secondBatchP95Deltas), 1)}/${fmt(maxFinite(secondBatchP95Deltas), 1)}`,
573
+ fmt(avgFinite(branchGains), 1),
574
+ fmt(avgFinite(branchCoverages), 1),
575
+ fmt(avgFinite(dataFactorDeltaPct), 1),
576
+ fmt(avgFinite(controlBppDeltaPct), 1),
577
+ fmt(maxFinite(rootChildrenDeltas), 0),
578
+ fmt(maxFinite(rootUploadPctDeltas), 2),
579
+ Math.max(...group.map((sample) => sample.upgrade.maintMaxReparentsPerPeer)),
580
+ failureTotal,
581
+ ].join(" "));
582
+ }
583
+ console.log(lines.join("\n"));
584
+ };
585
+ const compactResult = (result) => ({
586
+ params: {
587
+ seed: result.params.seed,
588
+ nodes: result.params.nodes,
589
+ subscribers: result.params.subscribers,
590
+ streamRxDelayMs: result.params.streamRxDelayMs,
591
+ deadlineMs: result.params.deadlineMs,
592
+ },
593
+ joinedCount: result.joinedCount,
594
+ subscriberCount: result.subscriberCount,
595
+ expected: result.expected,
596
+ delivered: result.delivered,
597
+ deliveredPct: result.deliveredPct,
598
+ deliveredWithinDeadlinePct: result.deliveredWithinDeadlinePct,
599
+ secondBatchExpected: result.secondBatchExpected,
600
+ secondBatchDeliveredWithinDeadlinePct: result.secondBatchDeliveredWithinDeadlinePct,
601
+ secondBatchLatencyP95: result.secondBatchLatencyP95,
602
+ formationScore: result.formationScore,
603
+ treeLevelAvg: result.treeLevelAvg,
604
+ treeLevelP95: result.treeLevelP95,
605
+ formationStretchP95: result.formationStretchP95,
606
+ treeRootChildren: result.treeRootChildren,
607
+ rootUploadFracPct: result.rootUploadFracPct,
608
+ reparentUpgradeTotal: result.reparentUpgradeTotal,
609
+ parentProbeReqSentTotal: result.parentProbeReqSentTotal,
610
+ parentShadowStartTotal: result.parentShadowStartTotal,
611
+ parentShadowPromoteTotal: result.parentShadowPromoteTotal,
612
+ publishActiveReparentUpgradeTotal: result.publishActiveReparentUpgradeTotal,
613
+ publishActiveParentProbeReqSentTotal: result.publishActiveParentProbeReqSentTotal,
614
+ publishActiveGuardSkipsTotal: result.publishActiveReparentUpgradeSkipDataTotal +
615
+ result.publishActiveReparentUpgradeSkipRepairTotal +
616
+ result.publishActiveReparentUpgradeSkipQuietTotal,
617
+ maintReparentsPerMin: result.maintReparentsPerMin,
618
+ maintMaxReparentsPerPeer: result.maintMaxReparentsPerPeer,
619
+ maintOrphanArea: result.maintOrphanArea,
620
+ duplicates: result.duplicates,
621
+ dataOverheadFactor: result.overheadFactorData,
622
+ controlBpp: result.controlBpp,
623
+ trackerBpp: result.trackerBpp,
624
+ repairBpp: result.repairBpp,
625
+ });
626
+ const writeJsonSummary = async (args, samples) => {
627
+ if (!args.jsonOut)
628
+ return;
629
+ await mkdir(dirname(args.jsonOut), { recursive: true });
630
+ await writeFile(args.jsonOut, JSON.stringify({
631
+ kind: "fanout-tree-parent-upgrade-eval",
632
+ generatedAt: new Date().toISOString(),
633
+ args,
634
+ samples: samples.map((sample) => ({
635
+ scenario: sample.scenario,
636
+ mode: sample.mode,
637
+ seed: sample.seed,
638
+ viable: sample.failures.length === 0,
639
+ failures: sample.failures,
640
+ baseline: compactResult(sample.baseline),
641
+ upgrade: compactResult(sample.upgrade),
642
+ })),
643
+ }, null, 2) + "\n");
644
+ };
645
+ const main = async () => {
646
+ const args = parseArgs(process.argv.slice(2));
647
+ let failureCount = 0;
648
+ const summarySamples = [];
649
+ const modes = args.compareModes
650
+ ? ["direct", "probe", "shadow"]
651
+ : [args.parentUpgradeMode];
652
+ for (const scenario of args.scenarios) {
653
+ for (const seed of args.seeds) {
654
+ const baseParams = {
655
+ ...SCENARIOS[scenario],
656
+ seed,
657
+ parentUpgradeIntervalMs: 0,
658
+ ...(args.streamRxDelayMs == null
659
+ ? {}
660
+ : { streamRxDelayMs: args.streamRxDelayMs }),
661
+ };
662
+ const upgradeParams = {
663
+ ...SCENARIOS[scenario],
664
+ seed,
665
+ ...(args.streamRxDelayMs == null
666
+ ? {}
667
+ : { streamRxDelayMs: args.streamRxDelayMs }),
668
+ ...parentUpgradeRuntimeOptions(args),
669
+ };
670
+ console.log(`\n[baseline] scenario=${scenario} seed=${seed}`);
671
+ const baseline = await runFanoutTreeSim(baseParams);
672
+ console.log(formatFanoutTreeSimResult(baseline));
673
+ const rows = [{ mode: "off", result: baseline, failures: [] }];
674
+ for (const mode of modes) {
675
+ console.log(`\n[parent-upgrade:${mode}] scenario=${scenario} seed=${seed}`);
676
+ const upgrade = await runFanoutTreeSim({
677
+ ...upgradeParams,
678
+ parentUpgradeMode: mode,
679
+ });
680
+ console.log(formatFanoutTreeSimResult(upgrade));
681
+ const failures = evaluateRun(scenario, upgrade.params, baseline, upgrade, args);
682
+ printComparison(scenario, seed, mode, baseline, upgrade, failures);
683
+ rows.push({ mode, result: upgrade, failures });
684
+ summarySamples.push({
685
+ scenario,
686
+ mode,
687
+ seed,
688
+ baseline,
689
+ upgrade,
690
+ failures,
691
+ });
692
+ failureCount += failures.length;
693
+ }
694
+ if (args.compareModes) {
695
+ printModeTable(scenario, seed, baseline, rows);
696
+ }
697
+ }
698
+ }
699
+ printAggregateSummary(summarySamples);
700
+ await writeJsonSummary(args, summarySamples);
701
+ if (args.strict && failureCount > 0) {
702
+ process.exit(2);
703
+ }
704
+ };
705
+ try {
706
+ await main();
707
+ }
708
+ catch (err) {
709
+ console.error(err?.message ?? String(err));
710
+ process.exit(1);
711
+ }
712
+ //# sourceMappingURL=fanout-tree-parent-upgrade-eval.js.map