@fedify/cli 2.3.0-dev.1361 → 2.3.0-dev.1367
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/bench/action.js +189 -29
- package/dist/bench/command.js +43 -13
- package/dist/bench/compare/schema.js +16 -0
- package/dist/bench/compare.js +667 -0
- package/dist/bench/load/clock.js +20 -2
- package/dist/bench/load/generator.js +42 -9
- package/dist/bench/metrics/stats-client.js +65 -3
- package/dist/bench/mod.js +9 -2
- package/dist/bench/render/markdown.js +1 -0
- package/dist/bench/render/text.js +1 -0
- package/dist/bench/result/build.js +133 -10
- package/dist/bench/result/expect/evaluate.js +1 -1
- package/dist/bench/result/schema.js +353 -3
- package/dist/bench/safety/gate.js +4 -2
- package/dist/bench/scenario/normalize.js +1 -2
- package/dist/bench/scenario/schema.js +50 -9
- package/dist/bench/scenario/validate.js +2 -2
- package/dist/bench/scenarios/actor.js +38 -0
- package/dist/bench/scenarios/failure.js +363 -0
- package/dist/bench/scenarios/fanout.js +261 -0
- package/dist/bench/scenarios/inbox.js +4 -12
- package/dist/bench/scenarios/mixed.js +244 -0
- package/dist/bench/scenarios/object-discovery.js +211 -0
- package/dist/bench/scenarios/object.js +54 -0
- package/dist/bench/scenarios/read.js +108 -0
- package/dist/bench/scenarios/registry.js +19 -1
- package/dist/bench/scenarios/runner.js +21 -1
- package/dist/bench/scenarios/webfinger.js +1 -1
- package/dist/cache.js +1 -1
- package/dist/commands.js +110 -0
- package/dist/config.js +1 -1
- package/dist/deno.js +1 -1
- package/dist/docloader.js +1 -1
- package/dist/generate-vocab/action.js +1 -1
- package/dist/generate-vocab/command.js +5 -3
- package/dist/imagerenderer.js +2 -2
- package/dist/inbox/command.js +6 -4
- package/dist/inbox.js +4 -4
- package/dist/log.js +2 -2
- package/dist/lookup/command.js +121 -0
- package/dist/lookup.js +12 -123
- package/dist/mod.js +2 -23
- package/dist/nodeinfo.js +11 -9
- package/dist/options.js +1 -1
- package/dist/relay/command.js +6 -4
- package/dist/relay.js +2 -2
- package/dist/runner.js +69 -46
- package/dist/tempserver.js +1 -1
- package/dist/tunnel.js +6 -4
- package/dist/utils.js +5 -4
- package/dist/webfinger/action.js +1 -1
- package/dist/webfinger/command.js +6 -4
- package/dist/webfinger/lib.js +1 -1
- package/package.json +15 -14
- package/dist/generate-vocab/mod.js +0 -4
- package/dist/init/mod.js +0 -3
- package/dist/webfinger/mod.js +0 -4
|
@@ -9,10 +9,10 @@ import { systemClock } from "./clock.js";
|
|
|
9
9
|
* @param clock The clock (overridable for tests); defaults to the system clock.
|
|
10
10
|
* @returns The recorded samples and run metadata.
|
|
11
11
|
*/
|
|
12
|
-
function runLoad(plan, send, clock = systemClock()) {
|
|
13
|
-
return plan.load.kind === "open" ? runOpenLoop(plan, plan.load, send, clock) : runClosedLoop(plan, plan.load, send, clock);
|
|
12
|
+
function runLoad(plan, send, clock = systemClock(), signal) {
|
|
13
|
+
return plan.load.kind === "open" ? runOpenLoop(plan, plan.load, send, clock, signal) : runClosedLoop(plan, plan.load, send, clock, signal);
|
|
14
14
|
}
|
|
15
|
-
async function runOpenLoop(plan, load, send, clock) {
|
|
15
|
+
async function runOpenLoop(plan, load, send, clock, signal) {
|
|
16
16
|
const arrivals = scheduleArrivals({
|
|
17
17
|
ratePerSec: load.ratePerSec,
|
|
18
18
|
durationMs: plan.durationMs,
|
|
@@ -25,8 +25,13 @@ async function runOpenLoop(plan, load, send, clock) {
|
|
|
25
25
|
const start = clock.now();
|
|
26
26
|
const active = /* @__PURE__ */ new Set();
|
|
27
27
|
for (const offset of arrivals) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
throwIfAborted(signal);
|
|
29
|
+
await clock.sleepUntil(start + offset, signal);
|
|
30
|
+
if (await slots.acquire(signal)) saturated = true;
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
slots.release();
|
|
33
|
+
throw abortReason(signal);
|
|
34
|
+
}
|
|
30
35
|
const dispatched = dispatch(send, offset, start, plan.warmupMs, clock, samples).finally(() => {
|
|
31
36
|
slots.release();
|
|
32
37
|
active.delete(dispatched);
|
|
@@ -40,7 +45,7 @@ async function runOpenLoop(plan, load, send, clock) {
|
|
|
40
45
|
wallDurationMs: clock.now() - start
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
|
-
async function runClosedLoop(plan, load, send, clock) {
|
|
48
|
+
async function runClosedLoop(plan, load, send, clock, signal) {
|
|
44
49
|
const samples = [];
|
|
45
50
|
const slots = createSemaphore(load.maxInFlight);
|
|
46
51
|
let saturated = false;
|
|
@@ -48,7 +53,12 @@ async function runClosedLoop(plan, load, send, clock) {
|
|
|
48
53
|
const deadline = start + plan.durationMs;
|
|
49
54
|
async function worker() {
|
|
50
55
|
while (clock.now() < deadline) {
|
|
51
|
-
|
|
56
|
+
throwIfAborted(signal);
|
|
57
|
+
if (await slots.acquire(signal)) saturated = true;
|
|
58
|
+
if (signal?.aborted) {
|
|
59
|
+
slots.release();
|
|
60
|
+
throw abortReason(signal);
|
|
61
|
+
}
|
|
52
62
|
if (clock.now() >= deadline) {
|
|
53
63
|
slots.release();
|
|
54
64
|
break;
|
|
@@ -94,12 +104,29 @@ function createSemaphore(max) {
|
|
|
94
104
|
let count = 0;
|
|
95
105
|
const queue = [];
|
|
96
106
|
return {
|
|
97
|
-
acquire() {
|
|
107
|
+
acquire(signal) {
|
|
108
|
+
throwIfAborted(signal);
|
|
98
109
|
if (count < max) {
|
|
99
110
|
count++;
|
|
100
111
|
return Promise.resolve(false);
|
|
101
112
|
}
|
|
102
|
-
return new Promise((resolve
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const waiter = () => {
|
|
115
|
+
cleanup();
|
|
116
|
+
resolve(true);
|
|
117
|
+
};
|
|
118
|
+
const onAbort = () => {
|
|
119
|
+
const index = queue.indexOf(waiter);
|
|
120
|
+
if (index >= 0) queue.splice(index, 1);
|
|
121
|
+
cleanup();
|
|
122
|
+
reject(abortReason(signal));
|
|
123
|
+
};
|
|
124
|
+
const cleanup = () => {
|
|
125
|
+
signal?.removeEventListener("abort", onAbort);
|
|
126
|
+
};
|
|
127
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
queue.push(waiter);
|
|
129
|
+
});
|
|
103
130
|
},
|
|
104
131
|
release() {
|
|
105
132
|
const next = queue.shift();
|
|
@@ -108,5 +135,11 @@ function createSemaphore(max) {
|
|
|
108
135
|
}
|
|
109
136
|
};
|
|
110
137
|
}
|
|
138
|
+
function throwIfAborted(signal) {
|
|
139
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
140
|
+
}
|
|
141
|
+
function abortReason(signal) {
|
|
142
|
+
return signal.reason ?? /* @__PURE__ */ new Error("Benchmark load aborted.");
|
|
143
|
+
}
|
|
111
144
|
//#endregion
|
|
112
145
|
export { runLoad };
|
|
@@ -36,9 +36,13 @@ function parseServerSnapshot(snapshot) {
|
|
|
36
36
|
const values = depth.dataPoints.map((p) => p.value).filter(isFiniteNumber);
|
|
37
37
|
if (values.length > 0) queueDepthMax = Math.max(...values);
|
|
38
38
|
}
|
|
39
|
+
const queueTasks = parseQueueTasks(metrics);
|
|
40
|
+
const deliveryPermanentFailures = sumMetric(metrics, "activitypub.delivery.permanent_failure");
|
|
39
41
|
return {
|
|
40
42
|
signature,
|
|
41
|
-
queueDepthMax
|
|
43
|
+
queueDepthMax,
|
|
44
|
+
...queueTasks == null ? {} : { queueTasks },
|
|
45
|
+
...deliveryPermanentFailures == null ? {} : { deliveryPermanentFailures }
|
|
42
46
|
};
|
|
43
47
|
} catch {
|
|
44
48
|
return null;
|
|
@@ -56,9 +60,13 @@ function parseServerSnapshot(snapshot) {
|
|
|
56
60
|
* @returns The windowed snapshot.
|
|
57
61
|
*/
|
|
58
62
|
function diffSnapshots(baseline, end) {
|
|
63
|
+
const queueTasks = diffQueueTasks(baseline.queueTasks ?? null, end.queueTasks ?? null);
|
|
64
|
+
const deliveryPermanentFailures = diffCounter(baseline.deliveryPermanentFailures ?? null, end.deliveryPermanentFailures ?? null);
|
|
59
65
|
return {
|
|
60
66
|
signature: diffHistogram(baseline.signature, end.signature),
|
|
61
|
-
queueDepthMax: end.queueDepthMax
|
|
67
|
+
queueDepthMax: end.queueDepthMax,
|
|
68
|
+
...queueTasks == null ? {} : { queueTasks },
|
|
69
|
+
...deliveryPermanentFailures == null ? {} : { deliveryPermanentFailures }
|
|
62
70
|
};
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
@@ -95,6 +103,21 @@ async function fetchServerSnapshot(target, fetchImpl = fetch) {
|
|
|
95
103
|
return null;
|
|
96
104
|
}
|
|
97
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the remaining queue task backlog represented by a diffed snapshot.
|
|
108
|
+
* @param snapshot The server snapshot to inspect, usually already diffed
|
|
109
|
+
* against a baseline.
|
|
110
|
+
* @param baselineRemaining Queue tasks that were already outstanding when the
|
|
111
|
+
* diff baseline was taken. These must drain before diffed completions can be
|
|
112
|
+
* attributed to newly enqueued tasks.
|
|
113
|
+
* @returns `Math.max(0, baselineRemaining + enqueued - completed - failed)`,
|
|
114
|
+
* or `null` when the snapshot has no queue task counters.
|
|
115
|
+
*/
|
|
116
|
+
function queueTaskRemaining(snapshot, baselineRemaining = 0) {
|
|
117
|
+
if (snapshot?.queueTasks == null) return null;
|
|
118
|
+
const { enqueued, completed, failed } = snapshot.queueTasks;
|
|
119
|
+
return Math.max(0, baselineRemaining + enqueued - completed - failed);
|
|
120
|
+
}
|
|
98
121
|
function isFiniteNumber(value) {
|
|
99
122
|
return typeof value === "number" && Number.isFinite(value);
|
|
100
123
|
}
|
|
@@ -125,6 +148,28 @@ function mergeHistogram(dataPoints) {
|
|
|
125
148
|
counts
|
|
126
149
|
} : null;
|
|
127
150
|
}
|
|
151
|
+
function parseQueueTasks(metrics) {
|
|
152
|
+
const enqueued = sumMetric(metrics, "fedify.queue.task.enqueued");
|
|
153
|
+
const completed = sumMetric(metrics, "fedify.queue.task.completed");
|
|
154
|
+
const failed = sumMetric(metrics, "fedify.queue.task.failed");
|
|
155
|
+
return enqueued == null && completed == null && failed == null ? null : {
|
|
156
|
+
enqueued: enqueued ?? 0,
|
|
157
|
+
completed: completed ?? 0,
|
|
158
|
+
failed: failed ?? 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function sumMetric(metrics, name) {
|
|
162
|
+
let total = 0;
|
|
163
|
+
let found = false;
|
|
164
|
+
for (const metric of metrics) {
|
|
165
|
+
if (metric.name !== name || !Array.isArray(metric.dataPoints)) continue;
|
|
166
|
+
for (const point of metric.dataPoints) if (isRecord(point) && isFiniteNumber(point["value"])) {
|
|
167
|
+
total += point["value"];
|
|
168
|
+
found = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return found ? total : null;
|
|
172
|
+
}
|
|
128
173
|
function diffHistogram(baseline, end) {
|
|
129
174
|
if (end == null) return null;
|
|
130
175
|
if (baseline == null) return end;
|
|
@@ -135,9 +180,26 @@ function diffHistogram(baseline, end) {
|
|
|
135
180
|
counts
|
|
136
181
|
};
|
|
137
182
|
}
|
|
183
|
+
function diffQueueTasks(baseline, end) {
|
|
184
|
+
if (end == null) return null;
|
|
185
|
+
if (baseline == null) return end;
|
|
186
|
+
return {
|
|
187
|
+
enqueued: Math.max(0, end.enqueued - baseline.enqueued),
|
|
188
|
+
completed: Math.max(0, end.completed - baseline.completed),
|
|
189
|
+
failed: Math.max(0, end.failed - baseline.failed)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function diffCounter(baseline, end) {
|
|
193
|
+
if (end == null) return null;
|
|
194
|
+
if (baseline == null) return end;
|
|
195
|
+
return Math.max(0, end - baseline);
|
|
196
|
+
}
|
|
138
197
|
function histogramsCompatible(a, b) {
|
|
139
198
|
return a.boundaries.length === b.boundaries.length && a.counts.length === b.counts.length && a.boundaries.every((boundary, i) => boundary === b.boundaries[i]);
|
|
140
199
|
}
|
|
200
|
+
function isRecord(value) {
|
|
201
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
202
|
+
}
|
|
141
203
|
function histogramPercentile(histogram, p) {
|
|
142
204
|
const { boundaries, counts } = histogram;
|
|
143
205
|
const total = counts.reduce((sum, n) => sum + n, 0);
|
|
@@ -151,4 +213,4 @@ function histogramPercentile(histogram, p) {
|
|
|
151
213
|
return boundaries[boundaries.length - 1] ?? 0;
|
|
152
214
|
}
|
|
153
215
|
//#endregion
|
|
154
|
-
export { diffSnapshots, fetchServerSnapshot, snapshotToMetrics };
|
|
216
|
+
export { diffSnapshots, fetchServerSnapshot, queueTaskRemaining, snapshotToMetrics };
|
package/dist/bench/mod.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import "@js-temporal/polyfill";
|
|
2
|
-
import "./action.js";
|
|
3
2
|
import "./command.js";
|
|
4
|
-
|
|
3
|
+
import runBench$1 from "./action.js";
|
|
4
|
+
import { runBenchCompare } from "./compare.js";
|
|
5
|
+
//#region src/bench/mod.ts
|
|
6
|
+
/** Runs a parsed benchmark command. */
|
|
7
|
+
function runBench(command) {
|
|
8
|
+
return command.mode === "compare" ? runBenchCompare(command) : runBench$1(command);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { runBench };
|
|
@@ -32,6 +32,7 @@ function renderScenario(scenario) {
|
|
|
32
32
|
lines.push(`| Requests | ${formatNumber(r.total)} |`);
|
|
33
33
|
lines.push(`| Success rate | ${formatPercent(r.successRate)} |`);
|
|
34
34
|
lines.push(`| Throughput | ${formatNumber(scenario.throughputPerSec)}/s |`);
|
|
35
|
+
if (scenario.deliveryThroughputPerSec != null) lines.push(`| Delivery throughput | ${formatNumber(scenario.deliveryThroughputPerSec)}/s |`);
|
|
35
36
|
const l = scenario.client.latencyMs;
|
|
36
37
|
lines.push(`| Latency p50 | ${formatNumber(l.p50)}ms |`);
|
|
37
38
|
lines.push(`| Latency p95 | ${formatNumber(l.p95)}ms |`);
|
|
@@ -28,6 +28,7 @@ function renderScenario(scenario) {
|
|
|
28
28
|
const r = scenario.requests;
|
|
29
29
|
lines.push(` Requests: ${formatNumber(r.total)} (ok ${formatNumber(r.ok)}, failed ${formatNumber(r.failed)}, success ${formatPercent(r.successRate)})`);
|
|
30
30
|
lines.push(` Throughput: ${formatNumber(scenario.throughputPerSec)} req/s`);
|
|
31
|
+
if (scenario.deliveryThroughputPerSec != null) lines.push(` Delivery throughput: ${formatNumber(scenario.deliveryThroughputPerSec)} deliveries/s`);
|
|
31
32
|
const l = scenario.client.latencyMs;
|
|
32
33
|
lines.push(` Client latency (ms): p50 ${formatNumber(l.p50)} p95 ${formatNumber(l.p95)} p99 ${formatNumber(l.p99)} mean ${formatNumber(l.mean)} max ${formatNumber(l.max)}`);
|
|
33
34
|
if (scenario.server?.signatureVerificationMs != null) lines.push(` Server signature verification (ms): ${describePartial(scenario.server.signatureVerificationMs.overall)}`);
|
|
@@ -3,8 +3,8 @@ import { version } from "../../deno.js";
|
|
|
3
3
|
import { evaluateExpect } from "./expect/evaluate.js";
|
|
4
4
|
import { REPORT_SCHEMA_ID } from "./schema.js";
|
|
5
5
|
import process from "node:process";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
6
|
import { cpus } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
8
|
//#region src/bench/result/build.ts
|
|
9
9
|
/**
|
|
10
10
|
* Assembly of the canonical benchmark report from measured scenario data.
|
|
@@ -23,19 +23,25 @@ import { cpus } from "node:os";
|
|
|
23
23
|
* @returns The assembled scenario result.
|
|
24
24
|
*/
|
|
25
25
|
function buildScenarioResult(scenario, measurement) {
|
|
26
|
-
const
|
|
26
|
+
const measurements = Array.isArray(measurement) ? measurement : [measurement];
|
|
27
|
+
if (measurements.length < 1) throw new RangeError("At least one scenario measurement is required.");
|
|
28
|
+
const aggregate = measurements.length === 1 ? measurements[0] : aggregateMeasurements(measurements);
|
|
29
|
+
const { results, passed } = evaluateExpect(scenario.expect, aggregate);
|
|
27
30
|
return {
|
|
28
31
|
name: scenario.name,
|
|
29
32
|
type: scenario.type,
|
|
30
33
|
load: loadSummary(scenario),
|
|
31
|
-
requests:
|
|
32
|
-
throughputPerSec:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
requests: aggregate.requests,
|
|
35
|
+
throughputPerSec: aggregate.throughputPerSec,
|
|
36
|
+
...aggregate.deliveryThroughputPerSec == null ? {} : { deliveryThroughputPerSec: aggregate.deliveryThroughputPerSec },
|
|
37
|
+
client: aggregate.client,
|
|
38
|
+
server: aggregate.server,
|
|
39
|
+
errors: aggregate.errors,
|
|
36
40
|
expectations: results,
|
|
37
|
-
passed: passed &&
|
|
38
|
-
|
|
41
|
+
passed: passed && measurements.every((m) => m.requests.total > 0),
|
|
42
|
+
runCount: measurements.length,
|
|
43
|
+
...measurements.length > 1 ? { runs: measurements.map((m, index) => runResult(index + 1, m)) } : {},
|
|
44
|
+
...aggregate.histogram ? { histogram: aggregate.histogram } : {}
|
|
39
45
|
};
|
|
40
46
|
}
|
|
41
47
|
/**
|
|
@@ -47,7 +53,7 @@ function buildScenarioResult(scenario, measurement) {
|
|
|
47
53
|
function buildReport(input) {
|
|
48
54
|
return {
|
|
49
55
|
$schema: REPORT_SCHEMA_ID,
|
|
50
|
-
schemaVersion:
|
|
56
|
+
schemaVersion: 3,
|
|
51
57
|
tool: {
|
|
52
58
|
name: "@fedify/cli",
|
|
53
59
|
version
|
|
@@ -61,6 +67,123 @@ function buildReport(input) {
|
|
|
61
67
|
scenarios: input.scenarios
|
|
62
68
|
};
|
|
63
69
|
}
|
|
70
|
+
function aggregateMeasurements(measurements) {
|
|
71
|
+
const errors = sumErrorBuckets(measurements.flatMap((m) => m.errors));
|
|
72
|
+
const total = sum(measurements.map((m) => m.requests.total));
|
|
73
|
+
const ok = sum(measurements.map((m) => m.requests.ok));
|
|
74
|
+
const failed = sum(measurements.map((m) => m.requests.failed));
|
|
75
|
+
const delivery = medianPresent(measurements.map((m) => m.deliveryThroughputPerSec));
|
|
76
|
+
return {
|
|
77
|
+
requests: {
|
|
78
|
+
total,
|
|
79
|
+
ok,
|
|
80
|
+
failed,
|
|
81
|
+
successRate: Math.min(...measurements.map((m) => m.requests.successRate))
|
|
82
|
+
},
|
|
83
|
+
throughputPerSec: median(measurements.map((m) => m.throughputPerSec)),
|
|
84
|
+
...delivery == null ? {} : { deliveryThroughputPerSec: delivery },
|
|
85
|
+
client: { latencyMs: {
|
|
86
|
+
p50: median(measurements.map((m) => m.client.latencyMs.p50)),
|
|
87
|
+
p95: median(measurements.map((m) => m.client.latencyMs.p95)),
|
|
88
|
+
p99: median(measurements.map((m) => m.client.latencyMs.p99)),
|
|
89
|
+
mean: median(measurements.map((m) => m.client.latencyMs.mean)),
|
|
90
|
+
max: median(measurements.map((m) => m.client.latencyMs.max))
|
|
91
|
+
} },
|
|
92
|
+
server: aggregateServer(measurements.map((m) => m.server)),
|
|
93
|
+
errors
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function runResult(run, measurement) {
|
|
97
|
+
return {
|
|
98
|
+
run,
|
|
99
|
+
requests: measurement.requests,
|
|
100
|
+
throughputPerSec: measurement.throughputPerSec,
|
|
101
|
+
...measurement.deliveryThroughputPerSec == null ? {} : { deliveryThroughputPerSec: measurement.deliveryThroughputPerSec },
|
|
102
|
+
client: measurement.client,
|
|
103
|
+
server: measurement.server,
|
|
104
|
+
errors: measurement.errors,
|
|
105
|
+
...measurement.histogram ? { histogram: measurement.histogram } : {}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function aggregateServer(servers) {
|
|
109
|
+
const present = servers.filter((s) => s != null);
|
|
110
|
+
if (present.length !== servers.length) return null;
|
|
111
|
+
const signature = aggregateSignatureVerification(present);
|
|
112
|
+
const queue = aggregateQueue(present);
|
|
113
|
+
return {
|
|
114
|
+
...signature == null ? {} : { signatureVerificationMs: signature },
|
|
115
|
+
...queue == null ? {} : { queue }
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function aggregateSignatureVerification(servers) {
|
|
119
|
+
const values = servers.map((s) => s.signatureVerificationMs).filter((s) => s != null);
|
|
120
|
+
if (values.length !== servers.length) return null;
|
|
121
|
+
const standards = /* @__PURE__ */ new Set();
|
|
122
|
+
for (const value of values) for (const key of Object.keys(value.byStandard ?? {})) standards.add(key);
|
|
123
|
+
const byStandard = {};
|
|
124
|
+
for (const standard of standards) byStandard[standard] = aggregatePartial(values.map((v) => v.byStandard?.[standard]), "present");
|
|
125
|
+
return {
|
|
126
|
+
overall: aggregatePartial(values.map((v) => v.overall)),
|
|
127
|
+
...Object.keys(byStandard).length < 1 ? {} : { byStandard }
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function aggregateQueue(servers) {
|
|
131
|
+
const values = servers.map((s) => s.queue).filter((q) => q != null);
|
|
132
|
+
if (values.length !== servers.length) return null;
|
|
133
|
+
const drainMs = aggregatePartial(values.map((v) => v.drainMs));
|
|
134
|
+
const depths = values.map((v) => v.depthMax);
|
|
135
|
+
return {
|
|
136
|
+
...hasPartial(drainMs) ? { drainMs } : {},
|
|
137
|
+
...depths.every(isNumber) ? { depthMax: Math.max(...depths) } : {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function aggregatePartial(values, mode = "complete") {
|
|
141
|
+
return {
|
|
142
|
+
...partialField(values, "p50", mode),
|
|
143
|
+
...partialField(values, "p95", mode),
|
|
144
|
+
...partialField(values, "p99", mode)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function partialField(values, key, mode) {
|
|
148
|
+
const fieldValues = values.map((v) => v?.[key]);
|
|
149
|
+
if (mode === "present") {
|
|
150
|
+
const present = fieldValues.filter(isNumber);
|
|
151
|
+
return present.length < 1 ? {} : { [key]: median(present) };
|
|
152
|
+
}
|
|
153
|
+
return fieldValues.every(isNumber) ? { [key]: median(fieldValues) } : {};
|
|
154
|
+
}
|
|
155
|
+
function hasPartial(value) {
|
|
156
|
+
return value.p50 != null || value.p95 != null || value.p99 != null;
|
|
157
|
+
}
|
|
158
|
+
function sumErrorBuckets(errors) {
|
|
159
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const error of errors) {
|
|
161
|
+
const key = `${error.kind}|${error.status ?? ""}|${error.reason}`;
|
|
162
|
+
const previous = buckets.get(key);
|
|
163
|
+
buckets.set(key, {
|
|
164
|
+
...error,
|
|
165
|
+
count: (previous?.count ?? 0) + error.count
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return [...buckets.values()].sort((a, b) => b.count - a.count);
|
|
169
|
+
}
|
|
170
|
+
function medianPresent(values) {
|
|
171
|
+
const present = values.filter(isNumber);
|
|
172
|
+
return present.length < 1 ? null : median(present);
|
|
173
|
+
}
|
|
174
|
+
function median(values) {
|
|
175
|
+
if (values.length < 1) return 0;
|
|
176
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
177
|
+
const middle = Math.floor(sorted.length / 2);
|
|
178
|
+
if (sorted.length % 2 === 1) return sorted[middle];
|
|
179
|
+
return (sorted[middle - 1] + sorted[middle]) / 2;
|
|
180
|
+
}
|
|
181
|
+
function sum(values) {
|
|
182
|
+
return values.reduce((a, b) => a + b, 0);
|
|
183
|
+
}
|
|
184
|
+
function isNumber(value) {
|
|
185
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
186
|
+
}
|
|
64
187
|
/** Detects the current runtime environment for reproducibility metadata. */
|
|
65
188
|
function detectEnvironment() {
|
|
66
189
|
const g = globalThis;
|
|
@@ -78,7 +78,7 @@ function lookupValue(metrics, metric) {
|
|
|
78
78
|
switch (metric) {
|
|
79
79
|
case "successRate": return metrics.requests.successRate;
|
|
80
80
|
case "throughputPerSec": return metrics.throughputPerSec;
|
|
81
|
-
case "deliveryThroughput": return null;
|
|
81
|
+
case "deliveryThroughput": return metrics.deliveryThroughputPerSec ?? null;
|
|
82
82
|
case "errors.total": return sumErrors(metrics.errors);
|
|
83
83
|
case "errors.4xx": return sumErrors(metrics.errors, {
|
|
84
84
|
min: 400,
|