@fedify/cli 2.3.0-dev.1347 → 2.3.0-dev.1361
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 +29 -189
- package/dist/bench/command.js +13 -43
- package/dist/bench/load/clock.js +2 -20
- package/dist/bench/load/generator.js +9 -42
- package/dist/bench/metrics/stats-client.js +3 -65
- package/dist/bench/mod.js +2 -9
- package/dist/bench/render/markdown.js +0 -1
- package/dist/bench/render/text.js +0 -1
- package/dist/bench/result/build.js +10 -133
- package/dist/bench/result/expect/evaluate.js +1 -1
- package/dist/bench/result/schema.js +3 -353
- package/dist/bench/safety/gate.js +2 -4
- package/dist/bench/scenario/normalize.js +2 -1
- package/dist/bench/scenario/schema.js +9 -50
- package/dist/bench/scenario/validate.js +2 -2
- package/dist/bench/scenarios/inbox.js +12 -4
- package/dist/bench/scenarios/registry.js +1 -19
- package/dist/bench/scenarios/runner.js +1 -21
- package/dist/bench/scenarios/webfinger.js +1 -1
- package/dist/cache.js +1 -1
- 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 +3 -5
- package/dist/generate-vocab/mod.js +4 -0
- package/dist/imagerenderer.js +2 -2
- package/dist/inbox/command.js +4 -6
- package/dist/inbox.js +4 -4
- package/dist/init/mod.js +3 -0
- package/dist/log.js +2 -2
- package/dist/lookup.js +123 -12
- package/dist/mod.js +23 -2
- package/dist/nodeinfo.js +9 -11
- package/dist/options.js +1 -1
- package/dist/relay/command.js +4 -6
- package/dist/relay.js +2 -2
- package/dist/runner.js +46 -69
- package/dist/tempserver.js +1 -1
- package/dist/tunnel.js +4 -6
- package/dist/utils.js +4 -5
- package/dist/webfinger/action.js +1 -1
- package/dist/webfinger/command.js +4 -6
- package/dist/webfinger/lib.js +1 -1
- package/dist/webfinger/mod.js +4 -0
- package/package.json +12 -13
- package/dist/bench/compare/schema.js +0 -16
- package/dist/bench/compare.js +0 -667
- package/dist/bench/scenarios/actor.js +0 -38
- package/dist/bench/scenarios/failure.js +0 -363
- package/dist/bench/scenarios/fanout.js +0 -261
- package/dist/bench/scenarios/mixed.js +0 -244
- package/dist/bench/scenarios/object-discovery.js +0 -211
- package/dist/bench/scenarios/object.js +0 -54
- package/dist/bench/scenarios/read.js +0 -108
- package/dist/commands.js +0 -110
- package/dist/lookup/command.js +0 -121
package/dist/bench/compare.js
DELETED
|
@@ -1,667 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import { describeError } from "../utils.js";
|
|
3
|
-
import { metricUnit } from "./result/expect/metrics.js";
|
|
4
|
-
import { parseDuration } from "./scenario/units.js";
|
|
5
|
-
import runBench from "./action.js";
|
|
6
|
-
import { COMPARE_REPORT_SCHEMA_ID } from "./compare/schema.js";
|
|
7
|
-
import process from "node:process";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
12
|
-
//#region src/bench/compare.ts
|
|
13
|
-
const ZERO_BASE_LATENCY_ALLOWANCE_MS = 1;
|
|
14
|
-
/** Runs `fedify bench compare`. */
|
|
15
|
-
async function runBenchCompare(command, deps = {}) {
|
|
16
|
-
const exit = deps.exit ?? ((code) => {
|
|
17
|
-
process.exitCode = code;
|
|
18
|
-
});
|
|
19
|
-
const writeOutput = deps.writeOutput ?? defaultWriteOutput;
|
|
20
|
-
const log = deps.log ?? ((message) => process.stderr.write(`${message}\n`));
|
|
21
|
-
const createWorktree = deps.createWorktree ?? defaultCreateWorktree;
|
|
22
|
-
const removeWorktree = deps.removeWorktree ?? defaultRemoveWorktree;
|
|
23
|
-
const startTarget = deps.startTarget ?? defaultStartTarget;
|
|
24
|
-
const waitReady = deps.waitReady ?? defaultWaitReady;
|
|
25
|
-
const runBenchInWorktree = deps.runBenchInWorktree ?? ((input) => defaultRunBenchInWorktree(input, deps.benchDeps));
|
|
26
|
-
const signalTarget = deps.signalTarget ?? process;
|
|
27
|
-
let readyUrl;
|
|
28
|
-
let readyTimeoutMs;
|
|
29
|
-
let maxRegression;
|
|
30
|
-
try {
|
|
31
|
-
readyUrl = new URL(command.readyUrl);
|
|
32
|
-
readyTimeoutMs = parseDuration(command.readyTimeout);
|
|
33
|
-
maxRegression = parseRegressionTolerance(command.maxRegression);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
log(describeError(error));
|
|
36
|
-
exit(2);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const target = command.target ?? new URL("/", readyUrl).origin;
|
|
40
|
-
const worktrees = [];
|
|
41
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
42
|
-
let activeTarget;
|
|
43
|
-
let interruptError;
|
|
44
|
-
const interruptController = new AbortController();
|
|
45
|
-
const interruptSignal = interruptController.signal;
|
|
46
|
-
let rejectInterrupt;
|
|
47
|
-
const interruptPromise = new Promise((_resolve, reject) => {
|
|
48
|
-
rejectInterrupt = reject;
|
|
49
|
-
});
|
|
50
|
-
interruptPromise.catch(() => {});
|
|
51
|
-
let interrupted = false;
|
|
52
|
-
const onSignal = (signal) => {
|
|
53
|
-
if (interrupted) return;
|
|
54
|
-
interrupted = true;
|
|
55
|
-
interruptError = new BenchmarkInterrupted(signal);
|
|
56
|
-
interruptController.abort(interruptError);
|
|
57
|
-
rejectInterrupt(interruptError);
|
|
58
|
-
};
|
|
59
|
-
signalTarget.on("SIGINT", onSignal);
|
|
60
|
-
signalTarget.on("SIGTERM", onSignal);
|
|
61
|
-
try {
|
|
62
|
-
const baseReport = await runSide("base", command.base);
|
|
63
|
-
throwIfInterrupted();
|
|
64
|
-
const headReport = await runSide("head", command.head);
|
|
65
|
-
throwIfInterrupted();
|
|
66
|
-
const report = buildCompareReport({
|
|
67
|
-
baseRef: command.base,
|
|
68
|
-
headRef: command.head,
|
|
69
|
-
baseReport,
|
|
70
|
-
headReport,
|
|
71
|
-
maxRegression,
|
|
72
|
-
startedAt,
|
|
73
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
74
|
-
});
|
|
75
|
-
throwIfInterrupted();
|
|
76
|
-
await withInterrupt(writeOutput(renderCompareReport(report, command.format), command.output));
|
|
77
|
-
throwIfInterrupted();
|
|
78
|
-
exit(report.passed ? 0 : 1);
|
|
79
|
-
return;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if (error instanceof BenchmarkInterrupted) {
|
|
82
|
-
exit(error.exitCode);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
log(describeError(error));
|
|
86
|
-
exit(2);
|
|
87
|
-
return;
|
|
88
|
-
} finally {
|
|
89
|
-
if (activeTarget != null) try {
|
|
90
|
-
await activeTarget.stop();
|
|
91
|
-
} catch (error) {
|
|
92
|
-
log(`Failed to stop benchmark target: ${describeError(error)}`);
|
|
93
|
-
} finally {
|
|
94
|
-
activeTarget = void 0;
|
|
95
|
-
}
|
|
96
|
-
for (let i = worktrees.length - 1; i >= 0; i--) {
|
|
97
|
-
const path = worktrees[i];
|
|
98
|
-
try {
|
|
99
|
-
await removeWorktree(path);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
log(`Failed to remove benchmark worktree ${path}: ${describeError(error)}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
signalTarget.off("SIGINT", onSignal);
|
|
105
|
-
signalTarget.off("SIGTERM", onSignal);
|
|
106
|
-
}
|
|
107
|
-
async function runSide(label, ref) {
|
|
108
|
-
log(`Checking out ${label} benchmark ref ${ref}…`);
|
|
109
|
-
const cwd = await createWorktree(ref, label);
|
|
110
|
-
worktrees.push(cwd);
|
|
111
|
-
throwIfInterrupted();
|
|
112
|
-
const targetProcess = await startTarget(cwd, command.startCommand);
|
|
113
|
-
activeTarget = targetProcess;
|
|
114
|
-
let stoppingTarget = false;
|
|
115
|
-
let targetExitError;
|
|
116
|
-
const targetExit = targetProcessExited(targetProcess).catch((error) => {
|
|
117
|
-
targetExitError = error;
|
|
118
|
-
if (!stoppingTarget && !interruptSignal.aborted) {
|
|
119
|
-
interruptController.abort(error);
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
return new Promise(() => {});
|
|
123
|
-
});
|
|
124
|
-
const throwIfTargetExited = () => {
|
|
125
|
-
if (targetExitError != null) throw targetExitError;
|
|
126
|
-
};
|
|
127
|
-
try {
|
|
128
|
-
throwIfInterrupted();
|
|
129
|
-
await withInterrupt(Promise.race([targetExit, waitReady(readyUrl, readyTimeoutMs, interruptSignal)]));
|
|
130
|
-
await Promise.resolve();
|
|
131
|
-
throwIfTargetExited();
|
|
132
|
-
return await withInterrupt(Promise.race([targetExit, runBenchInWorktree({
|
|
133
|
-
cwd,
|
|
134
|
-
command,
|
|
135
|
-
target,
|
|
136
|
-
signal: interruptSignal
|
|
137
|
-
})]));
|
|
138
|
-
} finally {
|
|
139
|
-
try {
|
|
140
|
-
stoppingTarget = true;
|
|
141
|
-
await targetProcess.stop();
|
|
142
|
-
} finally {
|
|
143
|
-
if (activeTarget === targetProcess) activeTarget = void 0;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function withInterrupt(promise) {
|
|
148
|
-
return Promise.race([interruptPromise, promise]);
|
|
149
|
-
}
|
|
150
|
-
function throwIfInterrupted() {
|
|
151
|
-
if (interruptError != null) throw interruptError;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
var BenchmarkInterrupted = class extends Error {
|
|
155
|
-
signal;
|
|
156
|
-
constructor(signal) {
|
|
157
|
-
super(`Interrupted by ${signal}.`);
|
|
158
|
-
this.signal = signal;
|
|
159
|
-
}
|
|
160
|
-
get exitCode() {
|
|
161
|
-
return this.signal === "SIGINT" ? 130 : 143;
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
function targetProcessExited(target) {
|
|
165
|
-
return target.exited ?? new Promise(() => {});
|
|
166
|
-
}
|
|
167
|
-
/** Parses `--max-regression`, accepting ratios or percentages. */
|
|
168
|
-
function parseRegressionTolerance(value) {
|
|
169
|
-
const trimmed = value.trim();
|
|
170
|
-
const match = /^(\d+(?:\.\d+)?|\.\d+)(%)?$/.exec(trimmed);
|
|
171
|
-
const numeric = match == null ? NaN : Number(match[1]);
|
|
172
|
-
if (!Number.isFinite(numeric) || numeric < 0) throw new RangeError(`Invalid --max-regression value: ${JSON.stringify(value)}.`);
|
|
173
|
-
if (match?.[2] == null && numeric > 1) throw new RangeError(`Invalid --max-regression value: ${JSON.stringify(value)}; use a ratio between 0 and 1 or an explicit percentage.`);
|
|
174
|
-
return match?.[2] === "%" ? numeric / 100 : numeric;
|
|
175
|
-
}
|
|
176
|
-
/** Builds a compare report from two benchmark reports. */
|
|
177
|
-
function buildCompareReport(input) {
|
|
178
|
-
const comparisons = compareReports(input.baseReport, input.headReport, input.maxRegression);
|
|
179
|
-
return {
|
|
180
|
-
$schema: COMPARE_REPORT_SCHEMA_ID,
|
|
181
|
-
schemaVersion: 1,
|
|
182
|
-
tool: input.headReport.tool,
|
|
183
|
-
environment: input.headReport.environment,
|
|
184
|
-
startedAt: input.startedAt,
|
|
185
|
-
finishedAt: input.finishedAt,
|
|
186
|
-
suite: input.headReport.suite,
|
|
187
|
-
maxRegression: input.maxRegression,
|
|
188
|
-
base: {
|
|
189
|
-
ref: input.baseRef,
|
|
190
|
-
report: input.baseReport
|
|
191
|
-
},
|
|
192
|
-
head: {
|
|
193
|
-
ref: input.headRef,
|
|
194
|
-
report: input.headReport
|
|
195
|
-
},
|
|
196
|
-
comparisons,
|
|
197
|
-
passed: input.headReport.passed && comparisons.every((c) => c.pass)
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
function compareReports(base, head, maxRegression) {
|
|
201
|
-
const results = [];
|
|
202
|
-
const baseByScenario = /* @__PURE__ */ new Map();
|
|
203
|
-
for (const baseScenario of base.scenarios) {
|
|
204
|
-
const key = comparisonScenarioKey(baseScenario);
|
|
205
|
-
const scenarios = baseByScenario.get(key);
|
|
206
|
-
if (scenarios == null) baseByScenario.set(key, [baseScenario]);
|
|
207
|
-
else scenarios.push(baseScenario);
|
|
208
|
-
}
|
|
209
|
-
const headCounts = /* @__PURE__ */ new Map();
|
|
210
|
-
for (const headScenario of head.scenarios) {
|
|
211
|
-
const key = comparisonScenarioKey(headScenario);
|
|
212
|
-
const occurrence = headCounts.get(key) ?? 0;
|
|
213
|
-
headCounts.set(key, occurrence + 1);
|
|
214
|
-
const baseScenario = baseByScenario.get(key)?.[occurrence];
|
|
215
|
-
if (baseScenario == null) {
|
|
216
|
-
results.push(newScenario(headScenario.name, maxRegression));
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
for (const metric of comparisonMetrics(headScenario)) results.push(compareMetric(baseScenario, headScenario, metric, maxRegression));
|
|
220
|
-
}
|
|
221
|
-
return results;
|
|
222
|
-
}
|
|
223
|
-
function comparisonScenarioKey(scenario) {
|
|
224
|
-
return `${scenario.name}\0${scenario.type}`;
|
|
225
|
-
}
|
|
226
|
-
function comparisonMetrics(scenario) {
|
|
227
|
-
const fromExpect = scenario.expectations.map((e) => e.metric).filter(isPerformanceMetric);
|
|
228
|
-
return [...new Set(fromExpect.length < 1 ? ["latency.p95", "throughputPerSec"] : fromExpect)];
|
|
229
|
-
}
|
|
230
|
-
function isPerformanceMetric(metric) {
|
|
231
|
-
const unit = metricUnit(metric);
|
|
232
|
-
return unit === "ms" || unit === "rate";
|
|
233
|
-
}
|
|
234
|
-
function compareMetric(baseScenario, headScenario, metric, maxRegression) {
|
|
235
|
-
const unit = metricUnit(metric);
|
|
236
|
-
const direction = unit === "rate" ? "higher-is-better" : "lower-is-better";
|
|
237
|
-
const base = metricValue(baseScenario, metric);
|
|
238
|
-
const head = metricValue(headScenario, metric);
|
|
239
|
-
const noiseBand = Math.max(relativeNoise(baseScenario, metric), relativeNoise(headScenario, metric));
|
|
240
|
-
const regression = base == null || head == null ? null : regressionRatio(base, head, direction, unit);
|
|
241
|
-
const allowedRegression = maxRegression + noiseBand;
|
|
242
|
-
return {
|
|
243
|
-
scenario: headScenario.name,
|
|
244
|
-
metric,
|
|
245
|
-
direction,
|
|
246
|
-
base,
|
|
247
|
-
head,
|
|
248
|
-
regression,
|
|
249
|
-
noiseBand,
|
|
250
|
-
allowedRegression,
|
|
251
|
-
pass: base == null && head != null || regression != null && regression <= allowedRegression
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function newScenario(scenario, maxRegression) {
|
|
255
|
-
return {
|
|
256
|
-
scenario,
|
|
257
|
-
metric: "scenario",
|
|
258
|
-
direction: "lower-is-better",
|
|
259
|
-
base: null,
|
|
260
|
-
head: null,
|
|
261
|
-
regression: null,
|
|
262
|
-
noiseBand: 0,
|
|
263
|
-
allowedRegression: maxRegression,
|
|
264
|
-
pass: true
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function metricValue(scenario, metric) {
|
|
268
|
-
switch (metric) {
|
|
269
|
-
case "throughputPerSec": return scenario.throughputPerSec;
|
|
270
|
-
case "deliveryThroughput": return scenario.deliveryThroughputPerSec ?? null;
|
|
271
|
-
}
|
|
272
|
-
if (metric.startsWith("latency.")) {
|
|
273
|
-
const latency = scenario.client?.latencyMs;
|
|
274
|
-
return latency == null ? null : latencyValue(latency, metric.slice(8));
|
|
275
|
-
}
|
|
276
|
-
if (metric.startsWith("signatureVerification.")) return partialValue(scenario.server?.signatureVerificationMs?.overall, metric.slice(22));
|
|
277
|
-
if (metric.startsWith("queueDrain.")) return partialValue(scenario.server?.queue?.drainMs, metric.slice(11));
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
function latencyValue(latency, field) {
|
|
281
|
-
switch (field) {
|
|
282
|
-
case "p50": return latency.p50;
|
|
283
|
-
case "p95": return latency.p95;
|
|
284
|
-
case "p99": return latency.p99;
|
|
285
|
-
case "mean": return latency.mean;
|
|
286
|
-
case "max": return latency.max;
|
|
287
|
-
default: return null;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function partialValue(latency, field) {
|
|
291
|
-
switch (field) {
|
|
292
|
-
case "p50": return latency?.p50 ?? null;
|
|
293
|
-
case "p95": return latency?.p95 ?? null;
|
|
294
|
-
case "p99": return latency?.p99 ?? null;
|
|
295
|
-
default: return null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
function regressionRatio(base, head, direction, unit) {
|
|
299
|
-
if (!Number.isFinite(base) || !Number.isFinite(head)) return null;
|
|
300
|
-
if (base < 0) return base === head ? 0 : null;
|
|
301
|
-
if (base === 0) {
|
|
302
|
-
if (base === head) return 0;
|
|
303
|
-
if (direction === "lower-is-better" && unit === "ms" && head <= ZERO_BASE_LATENCY_ALLOWANCE_MS) return 0;
|
|
304
|
-
return direction === "higher-is-better" && head > base ? 0 : null;
|
|
305
|
-
}
|
|
306
|
-
return direction === "higher-is-better" ? (base - head) / base : (head - base) / base;
|
|
307
|
-
}
|
|
308
|
-
function relativeNoise(scenario, metric) {
|
|
309
|
-
const values = (scenario.runs ?? []).map((run) => metricValue(run, metric)).filter((value) => value != null && Number.isFinite(value));
|
|
310
|
-
if (values.length < 2) return 0;
|
|
311
|
-
const medianValue = median(values);
|
|
312
|
-
if (medianValue <= 0) return 0;
|
|
313
|
-
return (Math.max(...values) - Math.min(...values)) / (2 * medianValue);
|
|
314
|
-
}
|
|
315
|
-
function median(values) {
|
|
316
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
317
|
-
const middle = Math.floor(sorted.length / 2);
|
|
318
|
-
if (sorted.length % 2 === 1) return sorted[middle];
|
|
319
|
-
return (sorted[middle - 1] + sorted[middle]) / 2;
|
|
320
|
-
}
|
|
321
|
-
function renderCompareReport(report, format) {
|
|
322
|
-
switch (format) {
|
|
323
|
-
case "json": return `${JSON.stringify(report, null, 2)}\n`;
|
|
324
|
-
case "markdown": return renderCompareMarkdown(report);
|
|
325
|
-
case "text": return renderCompareText(report);
|
|
326
|
-
}
|
|
327
|
-
throw new RangeError(`Unsupported benchmark report format: ${format}.`);
|
|
328
|
-
}
|
|
329
|
-
function renderCompareText(report) {
|
|
330
|
-
const lines = [
|
|
331
|
-
"Fedify benchmark comparison",
|
|
332
|
-
"",
|
|
333
|
-
`Base: ${report.base.ref}`,
|
|
334
|
-
`Head: ${report.head.ref}`,
|
|
335
|
-
`Maximum regression: ${formatPercent(report.maxRegression)}`,
|
|
336
|
-
""
|
|
337
|
-
];
|
|
338
|
-
for (const comparison of report.comparisons) lines.push(`[${comparison.pass ? "PASS" : "FAIL"}] ${comparison.scenario} ${comparison.metric}: base ${formatNumberOrNull(comparison.base)}, head ${formatNumberOrNull(comparison.head)}, regression ${formatNumberOrNull(comparison.regression, formatPercent)}, noise ${formatPercent(comparison.noiseBand)}`);
|
|
339
|
-
lines.push("", `Overall: ${report.passed ? "PASS" : "FAIL"}`);
|
|
340
|
-
return `${lines.join("\n")}\n`;
|
|
341
|
-
}
|
|
342
|
-
function renderCompareMarkdown(report) {
|
|
343
|
-
const lines = [
|
|
344
|
-
"# Fedify benchmark comparison",
|
|
345
|
-
"",
|
|
346
|
-
`**Result:** ${report.passed ? "PASS" : "FAIL"}`,
|
|
347
|
-
"",
|
|
348
|
-
`- **Base:** \`${report.base.ref}\``,
|
|
349
|
-
`- **Head:** \`${report.head.ref}\``,
|
|
350
|
-
`- **Maximum regression:** ${formatPercent(report.maxRegression)}`,
|
|
351
|
-
"",
|
|
352
|
-
"| Scenario | Metric | Base | Head | Regression | Noise | Result |",
|
|
353
|
-
"| --- | --- | --- | --- | --- | --- | --- |"
|
|
354
|
-
];
|
|
355
|
-
for (const comparison of report.comparisons) lines.push(`| ${comparison.scenario} | \`${comparison.metric}\` | ${formatNumberOrNull(comparison.base)} | ${formatNumberOrNull(comparison.head)} | ${formatNumberOrNull(comparison.regression, formatPercent)} | ${formatPercent(comparison.noiseBand)} | ${comparison.pass ? "PASS" : "FAIL"} |`);
|
|
356
|
-
return `${lines.join("\n")}\n`;
|
|
357
|
-
}
|
|
358
|
-
function formatNumberOrNull(value, formatter = formatNumber) {
|
|
359
|
-
return value == null ? "n/a" : formatter(value);
|
|
360
|
-
}
|
|
361
|
-
function formatNumber(value) {
|
|
362
|
-
if (!Number.isFinite(value)) return String(value);
|
|
363
|
-
return Number.isInteger(value) ? String(value) : value.toFixed(3);
|
|
364
|
-
}
|
|
365
|
-
function formatPercent(value) {
|
|
366
|
-
if (!Number.isFinite(value)) return String(value);
|
|
367
|
-
return `${(value * 100).toFixed(1)}%`;
|
|
368
|
-
}
|
|
369
|
-
async function defaultRunBenchInWorktree(input, benchDeps = {}) {
|
|
370
|
-
let output = "";
|
|
371
|
-
let exitCode = 0;
|
|
372
|
-
await runBench({
|
|
373
|
-
command: "bench",
|
|
374
|
-
mode: "run",
|
|
375
|
-
scenario: input.command.file,
|
|
376
|
-
target: input.target,
|
|
377
|
-
format: "json",
|
|
378
|
-
output: void 0,
|
|
379
|
-
dryRun: false,
|
|
380
|
-
advertiseHost: input.command.advertiseHost,
|
|
381
|
-
allowUnsafeTarget: input.command.allowUnsafeTarget,
|
|
382
|
-
userAgent: input.command.userAgent,
|
|
383
|
-
explicitCliTarget: input.command.target != null
|
|
384
|
-
}, {
|
|
385
|
-
...benchDeps,
|
|
386
|
-
signal: input.signal,
|
|
387
|
-
exit: (code) => {
|
|
388
|
-
exitCode = code;
|
|
389
|
-
},
|
|
390
|
-
writeOutput: (content) => {
|
|
391
|
-
output = content;
|
|
392
|
-
return Promise.resolve();
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
if (exitCode === 2 || output.trim() === "") throw new Error(`Benchmark run failed for ${input.cwd}.`);
|
|
396
|
-
return JSON.parse(output);
|
|
397
|
-
}
|
|
398
|
-
function defaultCreateWorktree(ref, label) {
|
|
399
|
-
return createBenchmarkWorktree(ref, label);
|
|
400
|
-
}
|
|
401
|
-
/** Creates a detached Git worktree for one side of a benchmark comparison. */
|
|
402
|
-
async function createBenchmarkWorktree(ref, label, deps = {}) {
|
|
403
|
-
const createTempDir = deps.createTempDir ?? mkdtemp;
|
|
404
|
-
const removePath = deps.removePath ?? rm;
|
|
405
|
-
const run = deps.runGit ?? runGit;
|
|
406
|
-
const path = await createTempDir(join(tmpdir(), `fedify-bench-${label}-`));
|
|
407
|
-
try {
|
|
408
|
-
await run([
|
|
409
|
-
"worktree",
|
|
410
|
-
"add",
|
|
411
|
-
"--detach",
|
|
412
|
-
path,
|
|
413
|
-
ref
|
|
414
|
-
]);
|
|
415
|
-
} catch (error) {
|
|
416
|
-
try {
|
|
417
|
-
await run([
|
|
418
|
-
"worktree",
|
|
419
|
-
"remove",
|
|
420
|
-
"--force",
|
|
421
|
-
path
|
|
422
|
-
]);
|
|
423
|
-
} catch {}
|
|
424
|
-
try {
|
|
425
|
-
await removePath(path, {
|
|
426
|
-
recursive: true,
|
|
427
|
-
force: true
|
|
428
|
-
});
|
|
429
|
-
} catch {}
|
|
430
|
-
throw error;
|
|
431
|
-
}
|
|
432
|
-
return path;
|
|
433
|
-
}
|
|
434
|
-
async function defaultRemoveWorktree(path) {
|
|
435
|
-
await runGit([
|
|
436
|
-
"worktree",
|
|
437
|
-
"remove",
|
|
438
|
-
"--force",
|
|
439
|
-
path
|
|
440
|
-
]);
|
|
441
|
-
}
|
|
442
|
-
function runGit(args) {
|
|
443
|
-
return new Promise((resolve, reject) => {
|
|
444
|
-
const child = spawn("git", args, { stdio: "ignore" });
|
|
445
|
-
child.on("error", reject);
|
|
446
|
-
child.on("close", (code) => {
|
|
447
|
-
if (code === 0) resolve();
|
|
448
|
-
else reject(/* @__PURE__ */ new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
function defaultStartTarget(cwd, startCommand) {
|
|
453
|
-
return Promise.resolve(startBenchmarkTarget(cwd, startCommand));
|
|
454
|
-
}
|
|
455
|
-
/** Starts a benchmark target process. */
|
|
456
|
-
function startBenchmarkTarget(cwd, startCommand, options = {}) {
|
|
457
|
-
const platform = options.platform ?? process.platform;
|
|
458
|
-
const spawnTarget = options.spawn ?? spawn;
|
|
459
|
-
const stderr = options.stderr ?? process.stderr;
|
|
460
|
-
const child = spawnTarget(startCommand, {
|
|
461
|
-
cwd,
|
|
462
|
-
detached: platform !== "win32",
|
|
463
|
-
shell: true,
|
|
464
|
-
stdio: [
|
|
465
|
-
"ignore",
|
|
466
|
-
"pipe",
|
|
467
|
-
"pipe"
|
|
468
|
-
],
|
|
469
|
-
env: process.env
|
|
470
|
-
});
|
|
471
|
-
forwardTargetOutput(child, stderr);
|
|
472
|
-
const exited = createTargetExitPromise(child);
|
|
473
|
-
exited.catch(() => {});
|
|
474
|
-
return {
|
|
475
|
-
exited,
|
|
476
|
-
stop: () => stopTargetProcess(child, { platform })
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
function forwardTargetOutput(child, stderr) {
|
|
480
|
-
child.stdout?.on("data", (chunk) => {
|
|
481
|
-
stderr.write(chunk);
|
|
482
|
-
});
|
|
483
|
-
child.stderr?.on("data", (chunk) => {
|
|
484
|
-
stderr.write(chunk);
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
function createTargetExitPromise(child) {
|
|
488
|
-
return new Promise((_resolve, reject) => {
|
|
489
|
-
const onError = (error) => {
|
|
490
|
-
child.removeListener("exit", onExit);
|
|
491
|
-
reject(error);
|
|
492
|
-
};
|
|
493
|
-
const onExit = (code, signal) => {
|
|
494
|
-
child.removeListener("error", onError);
|
|
495
|
-
const suffix = signal == null ? ` with code ${code ?? "<unknown>"}` : ` from ${signal}`;
|
|
496
|
-
reject(/* @__PURE__ */ new Error(`Benchmark target process ${child.pid ?? "<unknown>"} exited${suffix} before benchmark completion.`));
|
|
497
|
-
};
|
|
498
|
-
child.once("error", onError);
|
|
499
|
-
child.once("exit", onExit);
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
/** Stops a benchmark target process. */
|
|
503
|
-
function stopTargetProcess(child, options = {}) {
|
|
504
|
-
const platform = options.platform ?? process.platform;
|
|
505
|
-
const killWindowsProcessTree = options.killWindowsProcessTree ?? defaultKillWindowsProcessTree;
|
|
506
|
-
const killProcessGroup = options.killProcessGroup ?? ((pid, signal) => process.kill(pid, signal));
|
|
507
|
-
const forceTimeoutMs = options.forceTimeoutMs ?? 5e3;
|
|
508
|
-
const forceKillTimeoutMs = options.forceKillTimeoutMs ?? forceTimeoutMs;
|
|
509
|
-
return new Promise((resolve, reject) => {
|
|
510
|
-
if (child.pid == null || child.exitCode != null || child.signalCode != null) {
|
|
511
|
-
resolve();
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
let settled = false;
|
|
515
|
-
let forceKillTimer;
|
|
516
|
-
const clearTimers = () => {
|
|
517
|
-
clearTimeout(forceTimer);
|
|
518
|
-
if (forceKillTimer != null) clearTimeout(forceKillTimer);
|
|
519
|
-
};
|
|
520
|
-
const onExit = () => {
|
|
521
|
-
if (settled) return;
|
|
522
|
-
settled = true;
|
|
523
|
-
clearTimers();
|
|
524
|
-
resolve();
|
|
525
|
-
};
|
|
526
|
-
const rejectStop = (error) => {
|
|
527
|
-
if (settled) return;
|
|
528
|
-
settled = true;
|
|
529
|
-
clearTimers();
|
|
530
|
-
child.removeListener("exit", onExit);
|
|
531
|
-
reject(error);
|
|
532
|
-
};
|
|
533
|
-
const forceTimer = setTimeout(() => {
|
|
534
|
-
try {
|
|
535
|
-
killTargetProcess(child, "SIGKILL", {
|
|
536
|
-
platform,
|
|
537
|
-
killWindowsProcessTree,
|
|
538
|
-
killProcessGroup
|
|
539
|
-
});
|
|
540
|
-
} catch (error) {
|
|
541
|
-
rejectStop(error);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
forceKillTimer = setTimeout(() => {
|
|
545
|
-
rejectStop(/* @__PURE__ */ new Error(`Benchmark target process ${child.pid ?? "<unknown>"} did not exit after SIGKILL.`));
|
|
546
|
-
}, forceKillTimeoutMs);
|
|
547
|
-
}, forceTimeoutMs);
|
|
548
|
-
child.once("exit", onExit);
|
|
549
|
-
try {
|
|
550
|
-
killTargetProcess(child, "SIGTERM", {
|
|
551
|
-
platform,
|
|
552
|
-
killWindowsProcessTree,
|
|
553
|
-
killProcessGroup
|
|
554
|
-
});
|
|
555
|
-
} catch (error) {
|
|
556
|
-
rejectStop(error);
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
function killTargetProcess(child, signal, options) {
|
|
561
|
-
if (child.pid == null) {
|
|
562
|
-
child.kill(signal);
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (options.platform === "win32") {
|
|
566
|
-
options.killWindowsProcessTree(child.pid, signal);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
try {
|
|
570
|
-
options.killProcessGroup(-child.pid, signal);
|
|
571
|
-
} catch {
|
|
572
|
-
child.kill(signal);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function defaultKillWindowsProcessTree(pid, signal) {
|
|
576
|
-
spawn("taskkill", windowsTaskkillArgs(pid, signal), {
|
|
577
|
-
stdio: "ignore",
|
|
578
|
-
windowsHide: true
|
|
579
|
-
}).on("error", () => {});
|
|
580
|
-
}
|
|
581
|
-
/** Builds the Windows `taskkill` arguments used for target cleanup. */
|
|
582
|
-
function windowsTaskkillArgs(pid, signal) {
|
|
583
|
-
const args = [
|
|
584
|
-
"/pid",
|
|
585
|
-
String(pid),
|
|
586
|
-
"/T"
|
|
587
|
-
];
|
|
588
|
-
if (signal === "SIGKILL") args.push("/F");
|
|
589
|
-
return args;
|
|
590
|
-
}
|
|
591
|
-
async function defaultWaitReady(url, timeoutMs, signal) {
|
|
592
|
-
return await waitReadyUrl(url, timeoutMs, { signal });
|
|
593
|
-
}
|
|
594
|
-
/** Waits until a benchmark target readiness URL responds successfully. */
|
|
595
|
-
async function waitReadyUrl(url, timeoutMs, deps = {}) {
|
|
596
|
-
const fetchReady = deps.fetch ?? fetch;
|
|
597
|
-
const sleep = deps.sleep ?? ((ms, signal) => abortableSleep(ms, signal));
|
|
598
|
-
const signal = deps.signal;
|
|
599
|
-
const deadline = Date.now() + timeoutMs;
|
|
600
|
-
let lastError;
|
|
601
|
-
while (Date.now() <= deadline) {
|
|
602
|
-
throwIfAborted(signal);
|
|
603
|
-
const remainingMs = deadline - Date.now();
|
|
604
|
-
if (remainingMs <= 0) break;
|
|
605
|
-
const controller = new AbortController();
|
|
606
|
-
const onAbort = () => {
|
|
607
|
-
controller.abort(abortReason(signal));
|
|
608
|
-
};
|
|
609
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
610
|
-
const timer = setTimeout(() => {
|
|
611
|
-
controller.abort(/* @__PURE__ */ new Error(`ready URL timed out after ${timeoutMs}ms`));
|
|
612
|
-
}, remainingMs);
|
|
613
|
-
try {
|
|
614
|
-
const response = await fetchReady(url, { signal: controller.signal });
|
|
615
|
-
response.body?.cancel?.().catch(() => {});
|
|
616
|
-
if (response.status >= 200 && response.status < 400) return;
|
|
617
|
-
lastError = /* @__PURE__ */ new Error(`ready URL returned ${response.status}`);
|
|
618
|
-
} catch (error) {
|
|
619
|
-
if (signal?.aborted) throw abortReason(signal);
|
|
620
|
-
if (controller.signal.aborted) {
|
|
621
|
-
lastError = controller.signal.reason ?? error;
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
lastError = error;
|
|
625
|
-
} finally {
|
|
626
|
-
signal?.removeEventListener("abort", onAbort);
|
|
627
|
-
clearTimeout(timer);
|
|
628
|
-
}
|
|
629
|
-
const delayMs = Math.min(250, deadline - Date.now());
|
|
630
|
-
if (delayMs > 0) await sleep(delayMs, signal);
|
|
631
|
-
}
|
|
632
|
-
throw new Error(`Timed out waiting for ${url.href}: ${describeError(lastError)}.`);
|
|
633
|
-
}
|
|
634
|
-
function abortableSleep(ms, signal) {
|
|
635
|
-
if (signal?.aborted) return Promise.reject(abortReason(signal));
|
|
636
|
-
if (ms <= 0) return Promise.resolve();
|
|
637
|
-
return new Promise((resolve, reject) => {
|
|
638
|
-
const timer = setTimeout(() => {
|
|
639
|
-
cleanup();
|
|
640
|
-
resolve();
|
|
641
|
-
}, ms);
|
|
642
|
-
const onAbort = () => {
|
|
643
|
-
clearTimeout(timer);
|
|
644
|
-
cleanup();
|
|
645
|
-
reject(abortReason(signal));
|
|
646
|
-
};
|
|
647
|
-
const cleanup = () => {
|
|
648
|
-
signal?.removeEventListener("abort", onAbort);
|
|
649
|
-
};
|
|
650
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
function throwIfAborted(signal) {
|
|
654
|
-
if (signal?.aborted) throw abortReason(signal);
|
|
655
|
-
}
|
|
656
|
-
function abortReason(signal) {
|
|
657
|
-
return signal.reason ?? /* @__PURE__ */ new Error("Benchmark comparison aborted.");
|
|
658
|
-
}
|
|
659
|
-
async function defaultWriteOutput(content, outputPath) {
|
|
660
|
-
if (outputPath == null) {
|
|
661
|
-
process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
await writeFile(outputPath, content, { encoding: "utf-8" });
|
|
665
|
-
}
|
|
666
|
-
//#endregion
|
|
667
|
-
export { runBenchCompare };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import "@js-temporal/polyfill";
|
|
2
|
-
import { convertUrlIfHandle } from "../../webfinger/lib.js";
|
|
3
|
-
import { actorUrlsFromRecipients } from "./object-discovery.js";
|
|
4
|
-
import { isBareHttpUrl } from "./runner.js";
|
|
5
|
-
import { runReadLoad } from "./read.js";
|
|
6
|
-
//#region src/bench/scenarios/actor.ts
|
|
7
|
-
/**
|
|
8
|
-
* The `actor` scenario runner.
|
|
9
|
-
* @since 2.3.0
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
/** The `actor` scenario runner. */
|
|
13
|
-
const actorRunner = {
|
|
14
|
-
validate(scenario) {
|
|
15
|
-
if (scenario.recipients.length < 1) throw new Error("The actor scenario requires a recipient.");
|
|
16
|
-
for (const recipient of scenario.recipients) {
|
|
17
|
-
let url;
|
|
18
|
-
try {
|
|
19
|
-
url = convertUrlIfHandle(recipient);
|
|
20
|
-
} catch {
|
|
21
|
-
throw new Error(`Scenario "${scenario.name}": invalid actor recipient ${JSON.stringify(recipient)}.`);
|
|
22
|
-
}
|
|
23
|
-
if (url.protocol !== "acct:" && !isBareHttpUrl(url)) throw new Error(`Scenario "${scenario.name}": actor recipient must be an acct: handle or a bare http(s) URL with a host and no credentials; got ${JSON.stringify(url.href)}.`);
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
async run(context) {
|
|
27
|
-
this.validate?.(context.scenario);
|
|
28
|
-
return await runReadLoad(context, {
|
|
29
|
-
urls: await actorUrlsFromRecipients(context.scenario.recipients, {
|
|
30
|
-
target: context.target,
|
|
31
|
-
fetch: context.fetch
|
|
32
|
-
}),
|
|
33
|
-
authenticated: context.scenario.authenticated
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
//#endregion
|
|
38
|
-
export { actorRunner };
|