@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.
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
- package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
- package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
- package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
- package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
- package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
- package/dist/benchmark/fanout-tree-sim.js +369 -28
- package/dist/benchmark/fanout-tree-sim.js.map +1 -1
- package/dist/benchmark/index.js +20 -0
- package/dist/benchmark/index.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
- package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
- package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
- package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
- package/dist/benchmark/pubsub-topic-sim.js +18 -0
- package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
- package/dist/src/debounced-set.d.ts +1 -1
- package/dist/src/debounced-set.d.ts.map +1 -1
- package/dist/src/debounced-set.js +5 -1
- package/dist/src/debounced-set.js.map +1 -1
- package/dist/src/fanout-tree-codec.d.ts +330 -0
- package/dist/src/fanout-tree-codec.d.ts.map +1 -0
- package/dist/src/fanout-tree-codec.js +1209 -0
- package/dist/src/fanout-tree-codec.js.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
- package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
- package/dist/src/fanout-tree-parent-upgrade.js +135 -0
- package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
- package/dist/src/fanout-tree.d.ts +241 -73
- package/dist/src/fanout-tree.d.ts.map +1 -1
- package/dist/src/fanout-tree.js +2224 -1050
- package/dist/src/fanout-tree.js.map +1 -1
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +293 -41
- package/dist/src/index.js.map +1 -1
- package/dist/src/topic-root-control-plane.d.ts +13 -0
- package/dist/src/topic-root-control-plane.d.ts.map +1 -1
- package/dist/src/topic-root-control-plane.js +45 -0
- package/dist/src/topic-root-control-plane.js.map +1 -1
- package/package.json +6 -6
- package/src/debounced-set.ts +5 -0
- package/src/fanout-tree-codec.ts +1580 -0
- package/src/fanout-tree-parent-upgrade.ts +353 -0
- package/src/fanout-tree.ts +4467 -2204
- package/src/index.ts +392 -56
- 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
|