@cosyte/bench 0.0.0-stage → 0.1.1
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/dist/child-entry.d.ts +2 -0
- package/dist/child-entry.mjs +480 -0
- package/dist/child-entry.mjs.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1441 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/import-entry.d.ts +2 -0
- package/dist/import-entry.mjs +42 -0
- package/dist/import-entry.mjs.map +1 -0
- package/dist/index.d.ts +1043 -0
- package/dist/index.mjs +555 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -3
- package/schema/results-v1.schema.json +747 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { performance } from 'perf_hooks';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { GCProfiler, getHeapStatistics } from 'v8';
|
|
5
|
+
|
|
6
|
+
// src/child.ts
|
|
7
|
+
|
|
8
|
+
// src/contract.ts
|
|
9
|
+
var BENCH_CONTRACT = Object.freeze({
|
|
10
|
+
SAMPLES: 5,
|
|
11
|
+
SAMPLE_MIN_MS: 50,
|
|
12
|
+
ROUNDS: 12,
|
|
13
|
+
ALPHA: 0.05,
|
|
14
|
+
THRESHOLD: 0.03,
|
|
15
|
+
LATENCY_BUDGET_MS: 2e3,
|
|
16
|
+
LATENCY_MAX_CALLS: 5e3,
|
|
17
|
+
LATENCY_MIN_CALLS: 20,
|
|
18
|
+
LATENCY_TAIL_CALLS: 10,
|
|
19
|
+
LATENCY_TIMER_MULTIPLE: 20,
|
|
20
|
+
ALLOC_WINDOW_OPS: 256,
|
|
21
|
+
ALLOC_WINDOW_MS: 100,
|
|
22
|
+
ALLOC_SEMI_SPACE_MIB: 64,
|
|
23
|
+
PEAK_CALLS: 20,
|
|
24
|
+
LONG_RUN_ROUNDS: 30,
|
|
25
|
+
LONG_RUN_SETTLE_ROUNDS: 5,
|
|
26
|
+
LONG_RUN_ROUND_MS: 100,
|
|
27
|
+
LONG_RUN_GROWTH_BYTES: 64 * 1024,
|
|
28
|
+
GC_SETTLE_BYTES: 64 * 1024,
|
|
29
|
+
GC_SETTLE_ROUNDS: 2,
|
|
30
|
+
GC_MAX_ROUNDS: 7,
|
|
31
|
+
IMPORT_RUNS: 20,
|
|
32
|
+
REQUIRED_FALSE_ALARM_RATE: 0.01,
|
|
33
|
+
REQUIRED_CONFIDENCE: 0.95
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/stats.ts
|
|
37
|
+
function sortedCopy(xs) {
|
|
38
|
+
return [...xs].sort((a, b) => a - b);
|
|
39
|
+
}
|
|
40
|
+
function median(xs) {
|
|
41
|
+
const s = sortedCopy(xs);
|
|
42
|
+
const mid = Math.floor(s.length / 2);
|
|
43
|
+
return s.length % 2 === 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
44
|
+
}
|
|
45
|
+
function nearestRank(xs, q) {
|
|
46
|
+
if (xs.length === 0) return Number.NaN;
|
|
47
|
+
const s = sortedCopy(xs);
|
|
48
|
+
const rank = Math.max(1, Math.ceil(q * s.length));
|
|
49
|
+
return s[rank - 1];
|
|
50
|
+
}
|
|
51
|
+
function theilSenSlope(xs, ys) {
|
|
52
|
+
const slopes = [];
|
|
53
|
+
xs.forEach((xi, i) => {
|
|
54
|
+
for (let j = i + 1; j < xs.length; j++) {
|
|
55
|
+
const dx = xs[j] - xi;
|
|
56
|
+
if (dx !== 0) slopes.push((ys[j] - ys[i]) / dx);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return median(slopes);
|
|
60
|
+
}
|
|
61
|
+
function round(n, places = 4) {
|
|
62
|
+
const f = 10 ** places;
|
|
63
|
+
return Math.round(n * f) / f;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/memory.ts
|
|
67
|
+
function realProbe() {
|
|
68
|
+
const gc = globalThis.gc;
|
|
69
|
+
if (typeof gc !== "function") return null;
|
|
70
|
+
return {
|
|
71
|
+
gc: () => {
|
|
72
|
+
gc();
|
|
73
|
+
},
|
|
74
|
+
usedHeap: () => getHeapStatistics().used_heap_size,
|
|
75
|
+
external: () => process.memoryUsage().external,
|
|
76
|
+
profile: (fn) => {
|
|
77
|
+
const profiler = new GCProfiler();
|
|
78
|
+
profiler.start();
|
|
79
|
+
fn();
|
|
80
|
+
const stats = profiler.stop().statistics;
|
|
81
|
+
let peak = 0;
|
|
82
|
+
for (const s of stats) peak = Math.max(peak, s.beforeGC.heapStatistics.usedHeapSize);
|
|
83
|
+
return { gcs: stats.length, peakBeforeGc: peak };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function settle(probe) {
|
|
88
|
+
let quiet = 0;
|
|
89
|
+
let before = probe.usedHeap();
|
|
90
|
+
for (let round2 = 1; round2 <= BENCH_CONTRACT.GC_MAX_ROUNDS; round2++) {
|
|
91
|
+
probe.gc();
|
|
92
|
+
const after = probe.usedHeap();
|
|
93
|
+
quiet = before - after < BENCH_CONTRACT.GC_SETTLE_BYTES ? quiet + 1 : 0;
|
|
94
|
+
before = after;
|
|
95
|
+
if (quiet >= BENCH_CONTRACT.GC_SETTLE_ROUNDS)
|
|
96
|
+
return { settled: true, rounds: round2, heap: after };
|
|
97
|
+
}
|
|
98
|
+
return { settled: false, rounds: BENCH_CONTRACT.GC_MAX_ROUNDS, heap: before };
|
|
99
|
+
}
|
|
100
|
+
function measureAllocations(call, probe, msPerOp2) {
|
|
101
|
+
const window = (ops) => {
|
|
102
|
+
const s = settle(probe);
|
|
103
|
+
const h0 = probe.usedHeap();
|
|
104
|
+
const e0 = probe.external();
|
|
105
|
+
const { gcs } = probe.profile(() => {
|
|
106
|
+
for (let i = 0; i < ops; i++) call(i);
|
|
107
|
+
});
|
|
108
|
+
return { settled: s.settled, gcs, heap: probe.usedHeap() - h0, ext: probe.external() - e0 };
|
|
109
|
+
};
|
|
110
|
+
const paced = Math.floor(BENCH_CONTRACT.ALLOC_WINDOW_MS / msPerOp2);
|
|
111
|
+
const first = Math.max(1, Math.min(BENCH_CONTRACT.ALLOC_WINDOW_OPS, paced));
|
|
112
|
+
for (let n = first; n >= 1; n = Math.floor(n / 2)) {
|
|
113
|
+
const a = window(n);
|
|
114
|
+
const b = window(2 * n);
|
|
115
|
+
if (!a.settled || !b.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
116
|
+
if (a.gcs === 0 && b.gcs === 0) {
|
|
117
|
+
return {
|
|
118
|
+
status: "measured",
|
|
119
|
+
windowOps: n,
|
|
120
|
+
heapBytesPerOp: round((b.heap - a.heap) / n, 1),
|
|
121
|
+
externalBytesPerOp: round((b.ext - a.ext) / n, 1)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { status: "skipped", reason: "allocation-window-gc" };
|
|
126
|
+
}
|
|
127
|
+
function measurePeak(call, probe) {
|
|
128
|
+
const peaks = [];
|
|
129
|
+
for (let i = 0; i < BENCH_CONTRACT.PEAK_CALLS; i++) {
|
|
130
|
+
const s = settle(probe);
|
|
131
|
+
if (!s.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
132
|
+
const { peakBeforeGc } = probe.profile(() => {
|
|
133
|
+
call(i);
|
|
134
|
+
});
|
|
135
|
+
peaks.push(Math.max(peakBeforeGc, probe.usedHeap()) - s.heap);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
status: "measured",
|
|
139
|
+
calls: peaks.length,
|
|
140
|
+
peakHeapBytes: Math.max(...peaks),
|
|
141
|
+
medianPeakHeapBytes: median(peaks)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function measureLongRun(pass, opsPerPass, probe, clock) {
|
|
145
|
+
const t0 = clock();
|
|
146
|
+
let passesPerRound = 0;
|
|
147
|
+
do {
|
|
148
|
+
pass();
|
|
149
|
+
passesPerRound++;
|
|
150
|
+
} while (clock() - t0 < BENCH_CONTRACT.LONG_RUN_ROUND_MS);
|
|
151
|
+
const opsPerRound = passesPerRound * opsPerPass;
|
|
152
|
+
const rounds = [];
|
|
153
|
+
let ops = opsPerRound;
|
|
154
|
+
for (; ; ) {
|
|
155
|
+
const s = settle(probe);
|
|
156
|
+
if (!s.settled) return { status: "skipped", reason: "gc-unsettled" };
|
|
157
|
+
rounds.push({ ops, heap: s.heap });
|
|
158
|
+
if (rounds.length === BENCH_CONTRACT.LONG_RUN_ROUNDS) break;
|
|
159
|
+
for (let p = 0; p < passesPerRound; p++) pass();
|
|
160
|
+
ops += opsPerRound;
|
|
161
|
+
}
|
|
162
|
+
const skip = BENCH_CONTRACT.LONG_RUN_SETTLE_ROUNDS;
|
|
163
|
+
const fitted = rounds.slice(skip);
|
|
164
|
+
const slope = theilSenSlope(
|
|
165
|
+
fitted.map((r) => r.ops),
|
|
166
|
+
fitted.map((r) => r.heap)
|
|
167
|
+
);
|
|
168
|
+
const span = (BENCH_CONTRACT.LONG_RUN_ROUNDS - 1 - skip) * opsPerRound;
|
|
169
|
+
const growth = slope * span;
|
|
170
|
+
return {
|
|
171
|
+
status: "measured",
|
|
172
|
+
// Stryker disable next-line EqualityOperator: growth is a fitted slope times 24 rounds, so exactly 65,536 bytes is not reached in floating point (tried in memory.test.ts history).
|
|
173
|
+
verdict: growth >= BENCH_CONTRACT.LONG_RUN_GROWTH_BYTES ? "growing" : "flat",
|
|
174
|
+
ops: BENCH_CONTRACT.LONG_RUN_ROUNDS * opsPerRound,
|
|
175
|
+
opsPerRound,
|
|
176
|
+
settledHeap: rounds.map((r) => r.heap),
|
|
177
|
+
slopeBytesPerOp: round(slope, 4),
|
|
178
|
+
growthBytes: Math.round(growth),
|
|
179
|
+
detectableBytesPerOp: round(BENCH_CONTRACT.LONG_RUN_GROWTH_BYTES / span, 4)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../test-utils/src/perf/contract.ts
|
|
184
|
+
var PERF_CONTRACT = Object.freeze({
|
|
185
|
+
RATIO_CEILING: 8,
|
|
186
|
+
RATIO_FLOOR: 1.5,
|
|
187
|
+
REPS: 5,
|
|
188
|
+
SCALE_STEP: 4,
|
|
189
|
+
MIN_PHASE_MS: 4,
|
|
190
|
+
WARMUP_MIN_MS: 500,
|
|
191
|
+
WARMUP_BATCH_MS: 50,
|
|
192
|
+
WARMUP_MAX_MS: 5e3,
|
|
193
|
+
WARMUP_STABLE_TOL: 0.05,
|
|
194
|
+
WARMUP_STABLE_BATCHES: 3
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// src/measure.ts
|
|
198
|
+
var WARMUP = Object.freeze({
|
|
199
|
+
MIN_MS: PERF_CONTRACT.WARMUP_MIN_MS,
|
|
200
|
+
BATCH_MS: PERF_CONTRACT.WARMUP_BATCH_MS,
|
|
201
|
+
MAX_MS: PERF_CONTRACT.WARMUP_MAX_MS,
|
|
202
|
+
STABLE_TOL: PERF_CONTRACT.WARMUP_STABLE_TOL,
|
|
203
|
+
STABLE_BATCHES: PERF_CONTRACT.WARMUP_STABLE_BATCHES
|
|
204
|
+
});
|
|
205
|
+
function batchesAreStable(batches) {
|
|
206
|
+
if (batches.length < WARMUP.STABLE_BATCHES) return false;
|
|
207
|
+
const window = batches.slice(-WARMUP.STABLE_BATCHES);
|
|
208
|
+
const mid = median(window);
|
|
209
|
+
return window.every((b) => Math.abs(b - mid) / mid <= WARMUP.STABLE_TOL);
|
|
210
|
+
}
|
|
211
|
+
function warmUp(pass, clock) {
|
|
212
|
+
const start = clock();
|
|
213
|
+
const batches = [];
|
|
214
|
+
let passes = 0;
|
|
215
|
+
for (; ; ) {
|
|
216
|
+
const b0 = clock();
|
|
217
|
+
let inBatch = 0;
|
|
218
|
+
let batchMs;
|
|
219
|
+
do {
|
|
220
|
+
pass();
|
|
221
|
+
inBatch++;
|
|
222
|
+
batchMs = clock() - b0;
|
|
223
|
+
} while (batchMs < WARMUP.BATCH_MS);
|
|
224
|
+
passes += inBatch;
|
|
225
|
+
batches.push(batchMs / inBatch);
|
|
226
|
+
const elapsedMs = clock() - start;
|
|
227
|
+
if (elapsedMs >= WARMUP.MIN_MS && batchesAreStable(batches)) {
|
|
228
|
+
return { stable: true, elapsedMs, passes, batches };
|
|
229
|
+
}
|
|
230
|
+
if (elapsedMs >= WARMUP.MAX_MS) return { stable: false, elapsedMs, passes, batches };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function sampleThroughput(pass, opsPerPass, clock, inject = 0) {
|
|
234
|
+
const samples = [];
|
|
235
|
+
for (let s = 0; s < BENCH_CONTRACT.SAMPLES; s++) {
|
|
236
|
+
const t0 = clock();
|
|
237
|
+
let t = t0;
|
|
238
|
+
let passes = 0;
|
|
239
|
+
do {
|
|
240
|
+
const p0 = t;
|
|
241
|
+
pass();
|
|
242
|
+
t = clock();
|
|
243
|
+
const until = t + inject * (t - p0);
|
|
244
|
+
while (t < until) t = clock();
|
|
245
|
+
passes++;
|
|
246
|
+
} while (t - t0 < BENCH_CONTRACT.SAMPLE_MIN_MS);
|
|
247
|
+
samples.push((t - t0) / (passes * opsPerPass));
|
|
248
|
+
}
|
|
249
|
+
return samples;
|
|
250
|
+
}
|
|
251
|
+
function timerOverheadNs(ns, reads = 1e3) {
|
|
252
|
+
const gaps = [];
|
|
253
|
+
for (let i = 0; i < reads; i++) {
|
|
254
|
+
const a = ns();
|
|
255
|
+
const b = ns();
|
|
256
|
+
gaps.push(Number(b - a));
|
|
257
|
+
}
|
|
258
|
+
return median(gaps);
|
|
259
|
+
}
|
|
260
|
+
function tailPercentile(times, q) {
|
|
261
|
+
return times.length * (1 - q) >= BENCH_CONTRACT.LATENCY_TAIL_CALLS ? nearestRank(times, q) : null;
|
|
262
|
+
}
|
|
263
|
+
function measureLatency(call, ns) {
|
|
264
|
+
const overhead = timerOverheadNs(ns);
|
|
265
|
+
const budgetNs = BigInt(BENCH_CONTRACT.LATENCY_BUDGET_MS) * 1000000n;
|
|
266
|
+
const times = [];
|
|
267
|
+
const start = ns();
|
|
268
|
+
let now = start;
|
|
269
|
+
while (times.length < BENCH_CONTRACT.LATENCY_MAX_CALLS && (times.length < BENCH_CONTRACT.LATENCY_MIN_CALLS || now - start < budgetNs)) {
|
|
270
|
+
const a = ns();
|
|
271
|
+
call(times.length);
|
|
272
|
+
now = ns();
|
|
273
|
+
times.push(Number(now - a));
|
|
274
|
+
}
|
|
275
|
+
const p50 = nearestRank(times, 0.5);
|
|
276
|
+
if (p50 < BENCH_CONTRACT.LATENCY_TIMER_MULTIPLE * overhead) {
|
|
277
|
+
return {
|
|
278
|
+
status: "skipped",
|
|
279
|
+
reason: "latency-below-timer-resolution",
|
|
280
|
+
calls: times.length,
|
|
281
|
+
timerOverheadNs: round(overhead, 1),
|
|
282
|
+
p50Ns: p50
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
status: "measured",
|
|
287
|
+
calls: times.length,
|
|
288
|
+
timerOverheadNs: round(overhead, 1),
|
|
289
|
+
p50Ns: tailPercentile(times, 0.5),
|
|
290
|
+
p95Ns: tailPercentile(times, 0.95),
|
|
291
|
+
p99Ns: tailPercentile(times, 0.99),
|
|
292
|
+
maxNs: Math.max(...times)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/suite.ts
|
|
297
|
+
function fail(message) {
|
|
298
|
+
throw new TypeError(`@cosyte/bench: ${message}`);
|
|
299
|
+
}
|
|
300
|
+
function isRecord(value) {
|
|
301
|
+
return typeof value === "object" && value !== null;
|
|
302
|
+
}
|
|
303
|
+
function checkName(value, what) {
|
|
304
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${what} must be a non-empty string`);
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
function checkSuite(value) {
|
|
308
|
+
if (!isRecord(value)) fail("a suite must be an object made by defineSuite");
|
|
309
|
+
const name = checkName(value["name"], "the suite name");
|
|
310
|
+
const benchmarks = value["benchmarks"];
|
|
311
|
+
if (!Array.isArray(benchmarks) || benchmarks.length === 0) {
|
|
312
|
+
fail(`suite "${name}" must have at least one benchmark`);
|
|
313
|
+
}
|
|
314
|
+
const seen = /* @__PURE__ */ new Set();
|
|
315
|
+
for (const b of benchmarks) {
|
|
316
|
+
if (!isRecord(b)) fail(`suite "${name}" holds a benchmark that is not an object`);
|
|
317
|
+
const bName = checkName(b["name"], `a benchmark name in suite "${name}"`);
|
|
318
|
+
if (seen.has(bName)) fail(`suite "${name}" has two benchmarks named "${bName}"`);
|
|
319
|
+
seen.add(bName);
|
|
320
|
+
if (typeof b["bind"] !== "function") fail(`benchmark "${bName}" has no bind function`);
|
|
321
|
+
if (b["weigh"] !== void 0 && typeof b["weigh"] !== "function") {
|
|
322
|
+
fail(`benchmark "${bName}" has a weigh that is not a function`);
|
|
323
|
+
}
|
|
324
|
+
const sizes = b["sizes"];
|
|
325
|
+
if (!Array.isArray(sizes) || sizes.length === 0) fail(`benchmark "${bName}" has no sizes`);
|
|
326
|
+
const labels = /* @__PURE__ */ new Set();
|
|
327
|
+
for (const s of sizes) {
|
|
328
|
+
if (!isRecord(s)) fail(`benchmark "${bName}" holds a size that is not an object`);
|
|
329
|
+
const label = checkName(s["label"], `a size label in benchmark "${bName}"`);
|
|
330
|
+
if (labels.has(label)) fail(`benchmark "${bName}" has two sizes labelled "${label}"`);
|
|
331
|
+
labels.add(label);
|
|
332
|
+
if (typeof s["build"] !== "function")
|
|
333
|
+
fail(`size "${label}" of "${bName}" has no build function`);
|
|
334
|
+
if (s["compare"] !== void 0 && typeof s["compare"] !== "boolean") {
|
|
335
|
+
fail(`size "${label}" of "${bName}" has a compare that is not true or false`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (b["longRun"] !== void 0 && !labels.has(b["longRun"])) {
|
|
339
|
+
fail(`benchmark "${bName}" names a longRun size it does not have`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return Object.freeze(value);
|
|
343
|
+
}
|
|
344
|
+
function caseId(c) {
|
|
345
|
+
return `${c.benchmark} @ ${c.size}`;
|
|
346
|
+
}
|
|
347
|
+
function findCase(suite, c) {
|
|
348
|
+
const benchmark = suite.benchmarks.find((b) => b.name === c.benchmark);
|
|
349
|
+
const size = benchmark?.sizes.find((s) => s.label === c.size);
|
|
350
|
+
if (size === void 0) fail(`suite "${suite.name}" has no case "${caseId(c)}"`);
|
|
351
|
+
return { benchmark, size };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/child.ts
|
|
355
|
+
var realDeps = () => ({
|
|
356
|
+
clock: () => performance.now(),
|
|
357
|
+
ns: () => process.hrtime.bigint(),
|
|
358
|
+
probe: realProbe()
|
|
359
|
+
});
|
|
360
|
+
var defaultWeigh = (result) => result === void 0 || result === null ? 0 : 1;
|
|
361
|
+
var SKIPPED_NO_GC = { status: "skipped", reason: "gc-unavailable" };
|
|
362
|
+
function guard(c, phase, fn) {
|
|
363
|
+
try {
|
|
364
|
+
return fn();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const kind = error instanceof Error ? error.name : typeof error;
|
|
367
|
+
throw new Error(
|
|
368
|
+
`@cosyte/bench: case "${c.benchmark} @ ${c.size}": the suite's ${phase} threw (${kind}). Its message is not printed, because it may quote an input; run the case directly to read it.`,
|
|
369
|
+
{ cause: error }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function msPerOp(warmup, shape) {
|
|
374
|
+
return (warmup.batches[warmup.batches.length - 1] ?? 0) / shape.inputsPerPass;
|
|
375
|
+
}
|
|
376
|
+
function measureJob(job, shape, warmup, pass, call, deps) {
|
|
377
|
+
const base = { case: job.case, ...shape };
|
|
378
|
+
if (job.mode === "timing") {
|
|
379
|
+
if (!warmup.stable) {
|
|
380
|
+
return { mode: "timing", ...base, warmup, status: "skipped", reason: "warmup-unstable" };
|
|
381
|
+
}
|
|
382
|
+
const samples = sampleThroughput(pass, shape.inputsPerPass, deps.clock, job.inject);
|
|
383
|
+
return {
|
|
384
|
+
mode: "timing",
|
|
385
|
+
...base,
|
|
386
|
+
warmup,
|
|
387
|
+
status: "measured",
|
|
388
|
+
samplesMsPerOp: samples,
|
|
389
|
+
medianMsPerOp: median(samples),
|
|
390
|
+
latency: job.latency ? measureLatency(call, deps.ns) : null
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const probe = deps.probe;
|
|
394
|
+
if (job.mode === "alloc") {
|
|
395
|
+
return {
|
|
396
|
+
mode: "alloc",
|
|
397
|
+
...base,
|
|
398
|
+
allocations: probe === null ? SKIPPED_NO_GC : measureAllocations(call, probe, msPerOp(warmup, shape)),
|
|
399
|
+
peak: null,
|
|
400
|
+
longRun: null
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
let longRun = null;
|
|
404
|
+
if (job.longRun) {
|
|
405
|
+
longRun = probe === null ? SKIPPED_NO_GC : measureLongRun(pass, shape.inputsPerPass, probe, deps.clock);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
mode: "heap",
|
|
409
|
+
...base,
|
|
410
|
+
allocations: null,
|
|
411
|
+
peak: probe === null ? SKIPPED_NO_GC : measurePeak(call, probe),
|
|
412
|
+
longRun
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function measuredSomething(result) {
|
|
416
|
+
if (result.mode === "timing") return result.status === "measured";
|
|
417
|
+
return [result.allocations, result.peak, result.longRun].some((r) => r?.status === "measured");
|
|
418
|
+
}
|
|
419
|
+
async function runJob(job, deps = realDeps()) {
|
|
420
|
+
const suiteModule = await import(pathToFileURL(job.suite).href);
|
|
421
|
+
const suite = checkSuite(suiteModule.default);
|
|
422
|
+
const target = await import(pathToFileURL(job.target).href);
|
|
423
|
+
const { benchmark, size } = findCase(suite, job.case);
|
|
424
|
+
const corpus = guard(job.case, "build", () => size.build());
|
|
425
|
+
try {
|
|
426
|
+
const inputs = corpus.inputs;
|
|
427
|
+
if (!Array.isArray(inputs) || inputs.length === 0) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`@cosyte/bench: case "${job.case.benchmark} @ ${job.case.size}" built an empty corpus`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
const fn = guard(job.case, "bind", () => benchmark.bind(target));
|
|
433
|
+
const weigh = benchmark.weigh ?? defaultWeigh;
|
|
434
|
+
const sink = { value: 0 };
|
|
435
|
+
const pass = () => {
|
|
436
|
+
let local = 0;
|
|
437
|
+
for (const input of inputs) local += weigh(fn(input));
|
|
438
|
+
sink.value += local;
|
|
439
|
+
};
|
|
440
|
+
const call = (i) => {
|
|
441
|
+
sink.value += weigh(fn(inputs[i % inputs.length]));
|
|
442
|
+
};
|
|
443
|
+
const shape = {
|
|
444
|
+
inputsPerPass: inputs.length,
|
|
445
|
+
bytesPerPass: typeof corpus.bytes === "number" ? corpus.bytes : null
|
|
446
|
+
};
|
|
447
|
+
const warmup = guard(job.case, "measured function", () => warmUp(pass, deps.clock));
|
|
448
|
+
const before = sink.value;
|
|
449
|
+
const result = guard(
|
|
450
|
+
job.case,
|
|
451
|
+
"measured function",
|
|
452
|
+
() => measureJob(job, shape, warmup, pass, call, deps)
|
|
453
|
+
);
|
|
454
|
+
if (!(sink.value > before) && measuredSomething(result)) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`@cosyte/bench: case "${job.case.benchmark} @ ${job.case.size}": the sink did not grow, so the measured loop may have been deleted by the compiler, or \`weigh\` gave nothing positive. Supply a \`weigh\` that returns a positive number for a result.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return result;
|
|
460
|
+
} finally {
|
|
461
|
+
corpus.teardown?.();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function childMain(jobFile, outFile, deps) {
|
|
465
|
+
try {
|
|
466
|
+
const job = JSON.parse(readFileSync(jobFile, "utf8"));
|
|
467
|
+
const result = await runJob(job, deps);
|
|
468
|
+
writeFileSync(outFile, JSON.stringify({ ok: true, result }));
|
|
469
|
+
return 0;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
472
|
+
writeFileSync(outFile, JSON.stringify({ ok: false, error: message }));
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/child-entry.ts
|
|
478
|
+
process.exitCode = await childMain(process.argv[2] ?? "", process.argv[3] ?? "");
|
|
479
|
+
//# sourceMappingURL=child-entry.mjs.map
|
|
480
|
+
//# sourceMappingURL=child-entry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contract.ts","../src/stats.ts","../src/memory.ts","../../test-utils/src/perf/contract.ts","../src/measure.ts","../src/suite.ts","../src/child.ts","../src/child-entry.ts"],"names":["round","msPerOp"],"mappings":"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","file":"child-entry.mjs","sourcesContent":["/**\n * The benchmark constants, frozen.\n *\n * Every value here is fixed by the performance standard,\n * `documentation/decisions/0005-the-performance-standard.md` in the `config` repo, which says for\n * each one whether it was measured or chosen, and on what basis. The warmup rule is not here: it\n * is `PERF_CONTRACT` in `@cosyte/test-utils/perf` (ADR 0001 section 2), which `measure.ts` imports\n * and the build bundles, so the scaling gate and the benchmarks warm up by one rule kept in one\n * place.\n *\n * They are not tuning knobs and cannot be overridden per package. A comparison's false alarm rate\n * was measured with these values; change one and the measurement no longer describes the tool.\n *\n * @packageDocumentation\n */\n\n/** The benchmark constants. See {@link BENCH_CONTRACT}. */\nexport interface BenchContractShape {\n /** Timed samples per case per process. Each sample is whole passes over the corpus. */\n readonly SAMPLES: number;\n /** Minimum timed length of one sample, in milliseconds. A sample runs whole passes until it is reached. */\n readonly SAMPLE_MIN_MS: number;\n /** Rounds in one base against head comparison. Each round runs both sides once, in fresh processes. */\n readonly ROUNDS: number;\n /** One-sided error allowed at each end of the comparison's interval on the median ratio. */\n readonly ALPHA: number;\n /** The smallest slowdown a comparison will call a regression, as a fraction: 0.03 is 3%. */\n readonly THRESHOLD: number;\n /** Time budget for the latency calls of one case, in milliseconds. */\n readonly LATENCY_BUDGET_MS: number;\n /** Most latency calls taken for one case. */\n readonly LATENCY_MAX_CALLS: number;\n /** Fewest latency calls taken for one case, whatever the budget says. */\n readonly LATENCY_MIN_CALLS: number;\n /** A percentile q is reported only when at least this many calls lie above it: n(1 - q) >= 10. */\n readonly LATENCY_TAIL_CALLS: number;\n /** The median call must be at least this many timer reads long, or latency is skipped. */\n readonly LATENCY_TIMER_MULTIPLE: number;\n /** Most operations in the first allocation window. Halved while a garbage collection lands in a window. */\n readonly ALLOC_WINDOW_OPS: number;\n /** The first allocation window is cut to about this many milliseconds of operations, from the warmup's pace. */\n readonly ALLOC_WINDOW_MS: number;\n /** The young generation size the allocation process runs with, in MiB, so a window rarely collects. */\n readonly ALLOC_SEMI_SPACE_MIB: number;\n /** Calls measured one by one for the peak heap. */\n readonly PEAK_CALLS: number;\n /** Rounds in the long run that checks the heap stays flat. */\n readonly LONG_RUN_ROUNDS: number;\n /** Rounds at the start of the long run left out of the slope, while caches and compiled code settle. */\n readonly LONG_RUN_SETTLE_ROUNDS: number;\n /** Minimum length of one long run round, in milliseconds, fixed as a number of operations by the first. */\n readonly LONG_RUN_ROUND_MS: number;\n /** A long run whose fitted growth over the measured rounds reaches this many bytes is reported as growing. */\n readonly LONG_RUN_GROWTH_BYTES: number;\n /** A garbage collection round that reclaims less than this, in bytes, counts as settled (ADR 0001 section 8). */\n readonly GC_SETTLE_BYTES: number;\n /** Consecutive settled rounds needed (ADR 0001 section 8). */\n readonly GC_SETTLE_ROUNDS: number;\n /** Most garbage collection rounds before giving up (ADR 0001 section 8). */\n readonly GC_MAX_ROUNDS: number;\n /** Fresh processes timed for the cold import, for the target and for an empty module each. */\n readonly IMPORT_RUNS: number;\n /** The timing comparison may become a required check when A/A false alarms are under this rate. */\n readonly REQUIRED_FALSE_ALARM_RATE: number;\n /** The confidence that the false alarm rate is under the line, for the rule above. */\n readonly REQUIRED_CONFIDENCE: number;\n}\n\n/**\n * The frozen benchmark constants.\n *\n * @example\n * ```ts\n * import { BENCH_CONTRACT } from \"@cosyte/bench\";\n *\n * BENCH_CONTRACT.ROUNDS; // => 12\n * BENCH_CONTRACT.THRESHOLD; // => 0.03\n * ```\n */\nexport const BENCH_CONTRACT: BenchContractShape = Object.freeze({\n SAMPLES: 5,\n SAMPLE_MIN_MS: 50,\n ROUNDS: 12,\n ALPHA: 0.05,\n THRESHOLD: 0.03,\n LATENCY_BUDGET_MS: 2_000,\n LATENCY_MAX_CALLS: 5_000,\n LATENCY_MIN_CALLS: 20,\n LATENCY_TAIL_CALLS: 10,\n LATENCY_TIMER_MULTIPLE: 20,\n ALLOC_WINDOW_OPS: 256,\n ALLOC_WINDOW_MS: 100,\n ALLOC_SEMI_SPACE_MIB: 64,\n PEAK_CALLS: 20,\n LONG_RUN_ROUNDS: 30,\n LONG_RUN_SETTLE_ROUNDS: 5,\n LONG_RUN_ROUND_MS: 100,\n LONG_RUN_GROWTH_BYTES: 64 * 1024,\n GC_SETTLE_BYTES: 64 * 1024,\n GC_SETTLE_ROUNDS: 2,\n GC_MAX_ROUNDS: 7,\n IMPORT_RUNS: 20,\n REQUIRED_FALSE_ALARM_RATE: 0.01,\n REQUIRED_CONFIDENCE: 0.95,\n});\n","/**\n * The statistics the benchmarks report and judge by. Plain arithmetic with no dependencies, each\n * function small enough to check by hand against the worked numbers in its tests.\n *\n * @packageDocumentation\n */\n\n/**\n * A sorted copy, ascending. The input is not touched.\n *\n * @internal\n */\nexport function sortedCopy(xs: readonly number[]): number[] {\n return [...xs].sort((a, b) => a - b);\n}\n\n/**\n * The median: the middle value, or the mean of the two middle values. `NaN` for an empty vector,\n * so an empty measurement can never read as a fast one.\n *\n * @internal\n */\nexport function median(xs: readonly number[]): number {\n const s = sortedCopy(xs);\n const mid = Math.floor(s.length / 2);\n // An even length of at least 2 has both middle values. An empty vector takes the even branch and\n // reads two missing values, which is `NaN`: the empty answer, by arithmetic rather than a check.\n return s.length % 2 === 1\n ? (s[mid] as number)\n : ((s[mid - 1] as number) + (s[mid] as number)) / 2;\n}\n\n/**\n * A percentile by nearest rank: the smallest value with at least a fraction `q` of the values at or\n * below it. No interpolation, so every reported percentile is a value that was observed.\n *\n * @internal\n */\nexport function nearestRank(xs: readonly number[], q: number): number {\n if (xs.length === 0) return Number.NaN;\n const s = sortedCopy(xs);\n const rank = Math.max(1, Math.ceil(q * s.length));\n return s[rank - 1] as number;\n}\n\n/**\n * `P(X <= k)` for `X ~ Binomial(n, p)`, summed in log space so it stays finite for large `n`, for\n * `0 < p < 1` (every caller's range). A negative `k` sums nothing and is 0; a `k` past `n` sums all\n * of it and is 1.\n *\n * @internal\n */\nexport function binomialCdf(k: number, n: number, p: number): number {\n const logP = Math.log(p);\n const logQ = Math.log1p(-p);\n let logCoef = 0;\n let total = 0;\n for (let i = 0; i <= Math.min(k, n); i++) {\n if (i > 0) logCoef += Math.log(n - i + 1) - Math.log(i);\n total += Math.exp(logCoef + i * logP + (n - i) * logQ);\n }\n return Math.min(1, total);\n}\n\n/**\n * The sign test's rank for an interval on the median of `n` values: the largest `k` such that the\n * `k`-th smallest value lies below the true median with probability at most `alpha`, that is\n * `P(Binomial(n, 1/2) <= k - 1) <= alpha`. `0` when no rank qualifies (too few values).\n *\n * @internal\n */\nexport function signTestRank(n: number, alpha: number): number {\n let k = 0;\n while (k < n && binomialCdf(k, n, 0.5) <= alpha) k++;\n return k;\n}\n\n/** An interval on a median, from the sign test. */\nexport interface MedianInterval {\n /** The `rank`-th smallest value. */\n readonly low: number;\n /** The `rank`-th largest value. */\n readonly high: number;\n /** Which order statistics bound it. */\n readonly rank: number;\n /** The chance that each end alone misses the median, exactly: `P(Binomial(n, 1/2) <= rank - 1)`. */\n readonly tail: number;\n}\n\n/**\n * The distribution-free interval on the median of independent values: the `k`-th smallest and the\n * `k`-th largest, with `k` from {@link signTestRank}. It assumes nothing about the shape of the\n * noise, which on a shared runner is one-sided and heavy-tailed. `null` when there are too few\n * values for any rank to meet `alpha`.\n *\n * @internal\n */\nexport function signTestInterval(xs: readonly number[], alpha: number): MedianInterval | null {\n const rank = signTestRank(xs.length, alpha);\n if (rank === 0) return null;\n const s = sortedCopy(xs);\n // `rank` is at least 1 and at most `n`, so both order statistics exist.\n return {\n low: s[rank - 1] as number,\n high: s[s.length - rank] as number,\n rank,\n tail: binomialCdf(rank - 1, xs.length, 0.5),\n };\n}\n\n/**\n * The exact (Clopper-Pearson) one-sided upper confidence bound on a rate, from `k` events in `n`\n * trials: the largest rate `p` at which seeing `k` or fewer is still at least `1 - confidence`\n * likely. With no events this is `1 - (1 - confidence)^(1/n)`, about `3 / n` at 95%.\n *\n * @internal\n */\nexport function rateUpperBound(k: number, n: number, confidence: number): number {\n // No trials say nothing. Every trial an event bisects to 1 on its own.\n if (n === 0) return 1;\n const alpha = 1 - confidence;\n let lo = k / n;\n let hi = 1;\n // Stryker disable next-line EqualityOperator: a 101st halving moves the bound by under 2^-100, below a double's resolution.\n for (let i = 0; i < 100; i++) {\n const mid = (lo + hi) / 2;\n // Stryker disable next-line EqualityOperator: `>` and `>=` differ only where the tail equals alpha exactly, which bisection over doubles does not land on.\n if (binomialCdf(k, n, mid) > alpha) lo = mid;\n else hi = mid;\n }\n return hi;\n}\n\n/**\n * The fewest trials, all without an event, whose {@link rateUpperBound} is under `rate`: how long an\n * A/A calibration has to run before a clean record can show a false alarm rate under `rate`.\n *\n * @internal\n */\nexport function trialsForZeroEvents(rate: number, confidence: number): number {\n return Math.ceil(Math.log(1 - confidence) / Math.log1p(-rate));\n}\n\n/**\n * The Theil-Sen slope: the median of the slopes between every pair of points with distinct `x`.\n * One wild point moves it little, which matters for a heap reading that a late collection can\n * spike. `NaN` with fewer than two distinct `x`.\n *\n * @internal\n */\nexport function theilSenSlope(xs: readonly number[], ys: readonly number[]): number {\n // Pairs only exist where both vectors do; a caller passes vectors of one length.\n const slopes: number[] = [];\n xs.forEach((xi, i) => {\n for (let j = i + 1; j < xs.length; j++) {\n const dx = (xs[j] as number) - xi;\n if (dx !== 0) slopes.push(((ys[j] as number) - (ys[i] as number)) / dx);\n }\n });\n return median(slopes);\n}\n\n/**\n * Round to a number of decimal places, for readable reports. The full values stay in\n * the sample vectors.\n *\n * @internal\n */\nexport function round(n: number, places = 4): number {\n const f = 10 ** places;\n return Math.round(n * f) / f;\n}\n","/**\n * The memory measurements: allocations per operation, peak heap per call, and whether the heap stays\n * flat over a long run. Each runs in a measuring process started with `--expose-gc`, one case per\n * process.\n *\n * They read the heap through a {@link HeapProbe}, so their tests can drive them with a scripted heap.\n * The real probe follows ADR 0001 section 8: a zero-argument `gc()` only, settled by rule rather than\n * by a count.\n *\n * @packageDocumentation\n */\n\nimport { getHeapStatistics, GCProfiler } from \"node:v8\";\n\nimport { BENCH_CONTRACT } from \"./contract.js\";\nimport type { Clock } from \"./measure.js\";\nimport { median, round, theilSenSlope } from \"./stats.js\";\n\n/**\n * What the memory measurements read the heap with.\n *\n * @internal\n */\nexport interface HeapProbe {\n /** A full collection. The real probe calls `gc()` with no argument, the only form ADR 0001 allows. */\n readonly gc: () => void;\n /** Bytes in use on the V8 heap. */\n readonly usedHeap: () => number;\n /** Bytes held outside the V8 heap by JavaScript objects (Buffers, ArrayBuffers). */\n readonly external: () => number;\n /**\n * Run `fn` and report the collections that ran during it, and the most heap in use just before any\n * of them. The heap only shrinks at a collection, so its high point is either there or at the end.\n */\n readonly profile: (fn: () => void) => { readonly gcs: number; readonly peakBeforeGc: number };\n}\n\n/**\n * The real probe, or `null` when the process was not started with `--expose-gc`.\n *\n * @internal\n */\nexport function realProbe(): HeapProbe | null {\n const gc: unknown = (globalThis as { gc?: unknown }).gc;\n if (typeof gc !== \"function\") return null;\n return {\n gc: () => {\n (gc as () => void)();\n },\n usedHeap: () => getHeapStatistics().used_heap_size,\n external: () => process.memoryUsage().external,\n profile: (fn) => {\n const profiler = new GCProfiler();\n profiler.start();\n fn();\n const stats = profiler.stop().statistics;\n let peak = 0;\n for (const s of stats) peak = Math.max(peak, s.beforeGC.heapStatistics.usedHeapSize);\n return { gcs: stats.length, peakBeforeGc: peak };\n },\n };\n}\n\n/** The settled heap, or the news that it would not settle. */\nexport interface SettleResult {\n readonly settled: boolean;\n readonly rounds: number;\n readonly heap: number;\n}\n\n/**\n * Collect until two rounds in a row each reclaim under `GC_SETTLE_BYTES`, at most `GC_MAX_ROUNDS`\n * rounds (ADR 0001 section 8).\n *\n * @internal\n */\nexport function settle(probe: HeapProbe): SettleResult {\n let quiet = 0;\n let before = probe.usedHeap();\n for (let round = 1; round <= BENCH_CONTRACT.GC_MAX_ROUNDS; round++) {\n probe.gc();\n const after = probe.usedHeap();\n quiet = before - after < BENCH_CONTRACT.GC_SETTLE_BYTES ? quiet + 1 : 0;\n before = after;\n if (quiet >= BENCH_CONTRACT.GC_SETTLE_ROUNDS)\n return { settled: true, rounds: round, heap: after };\n }\n return { settled: false, rounds: BENCH_CONTRACT.GC_MAX_ROUNDS, heap: before };\n}\n\n/** Why a memory measurement has no number. */\nexport type MemorySkipReason = \"gc-unavailable\" | \"gc-unsettled\" | \"allocation-window-gc\";\n\n/** Allocations per operation. */\nexport type AllocationReport =\n | {\n readonly status: \"measured\";\n /** The smaller of the two windows, in operations. */\n readonly windowOps: number;\n /** Bytes allocated on the V8 heap per operation. */\n readonly heapBytesPerOp: number;\n /** Bytes allocated outside it (Buffers, ArrayBuffers) per operation. */\n readonly externalBytesPerOp: number;\n }\n | { readonly status: \"skipped\"; readonly reason: MemorySkipReason };\n\n/**\n * Allocations per operation, from the heap's growth across two windows of `n` and `2n` operations\n * in which no collection ran: `(growth(2n) - growth(n)) / n`, so the fixed cost of reading the heap\n * cancels. The first `n` is about `ALLOC_WINDOW_MS` of operations at `msPerOp`, at most\n * `ALLOC_WINDOW_OPS`. A collection in either window halves `n` and tries again. The process runs with a large\n * young generation (`ALLOC_SEMI_SPACE_MIB`) so that a window rarely collects.\n *\n * @internal\n */\nexport function measureAllocations(\n call: (i: number) => void,\n probe: HeapProbe,\n msPerOp: number,\n): AllocationReport {\n const window = (ops: number) => {\n const s = settle(probe);\n const h0 = probe.usedHeap();\n const e0 = probe.external();\n const { gcs } = probe.profile(() => {\n for (let i = 0; i < ops; i++) call(i);\n });\n return { settled: s.settled, gcs, heap: probe.usedHeap() - h0, ext: probe.external() - e0 };\n };\n const paced = Math.floor(BENCH_CONTRACT.ALLOC_WINDOW_MS / msPerOp);\n const first = Math.max(1, Math.min(BENCH_CONTRACT.ALLOC_WINDOW_OPS, paced));\n for (let n = first; n >= 1; n = Math.floor(n / 2)) {\n const a = window(n);\n const b = window(2 * n);\n if (!a.settled || !b.settled) return { status: \"skipped\", reason: \"gc-unsettled\" };\n if (a.gcs === 0 && b.gcs === 0) {\n return {\n status: \"measured\",\n windowOps: n,\n heapBytesPerOp: round((b.heap - a.heap) / n, 1),\n externalBytesPerOp: round((b.ext - a.ext) / n, 1),\n };\n }\n }\n return { status: \"skipped\", reason: \"allocation-window-gc\" };\n}\n\n/** Peak heap per call. */\nexport type PeakReport =\n | {\n readonly status: \"measured\";\n readonly calls: number;\n /** The highest peak of any call, in bytes above the settled heap before it. */\n readonly peakHeapBytes: number;\n /** The median call's peak. */\n readonly medianPeakHeapBytes: number;\n }\n | { readonly status: \"skipped\"; readonly reason: MemorySkipReason };\n\n/**\n * The peak heap of single calls: settle, call once, and take the higher of the heap just before any\n * collection during the call and the heap at its end, less the settled heap before it. Run with\n * V8's default heap sizing, so the collections that shape the peak are the ones a user's process has.\n *\n * @internal\n */\nexport function measurePeak(call: (i: number) => void, probe: HeapProbe): PeakReport {\n const peaks: number[] = [];\n for (let i = 0; i < BENCH_CONTRACT.PEAK_CALLS; i++) {\n const s = settle(probe);\n if (!s.settled) return { status: \"skipped\", reason: \"gc-unsettled\" };\n const { peakBeforeGc } = probe.profile(() => {\n call(i);\n });\n peaks.push(Math.max(peakBeforeGc, probe.usedHeap()) - s.heap);\n }\n return {\n status: \"measured\",\n calls: peaks.length,\n peakHeapBytes: Math.max(...peaks),\n medianPeakHeapBytes: median(peaks),\n };\n}\n\n/** Whether the heap stayed flat over a long run. */\nexport type LongRunReport =\n | {\n readonly status: \"measured\";\n /** `growing` when the fitted growth reaches `LONG_RUN_GROWTH_BYTES`. */\n readonly verdict: \"flat\" | \"growing\";\n /** Operations in all. */\n readonly ops: number;\n /** Operations per round, fixed by the first round. */\n readonly opsPerRound: number;\n /** The settled heap after each round, in bytes. */\n readonly settledHeap: readonly number[];\n /** Theil-Sen slope of the settled heap against operations, after the settling rounds. */\n readonly slopeBytesPerOp: number;\n /** The slope times the operations it was fitted over: the growth the verdict reads. */\n readonly growthBytes: number;\n /** The smallest leak, in bytes per operation, this run could call growing: the bound over the span. */\n readonly detectableBytesPerOp: number;\n }\n | { readonly status: \"skipped\"; readonly reason: MemorySkipReason };\n\n/**\n * Run the case for `LONG_RUN_ROUNDS` rounds, settle the heap after each, and fit the settled heap\n * against the operations run so far. A leak of `b` bytes an operation shows as a slope near `b`;\n * a heap that only grows while caches and compiled code settle is left out by\n * `LONG_RUN_SETTLE_ROUNDS`. The verdict reads the growth the slope implies over the fitted rounds,\n * not the slope alone: a settled heap reading moves by about a kilobyte with nothing running\n * (ADR 0001 section 8), so a run of few operations cannot tell a small slope from that.\n *\n * @internal\n */\nexport function measureLongRun(\n pass: () => void,\n opsPerPass: number,\n probe: HeapProbe,\n clock: Clock,\n): LongRunReport {\n const t0 = clock();\n let passesPerRound = 0;\n do {\n pass();\n passesPerRound++;\n } while (clock() - t0 < BENCH_CONTRACT.LONG_RUN_ROUND_MS);\n const opsPerRound = passesPerRound * opsPerPass;\n // Each round is the operations run so far and the heap settled after them. The sizing round\n // above is the first.\n const rounds: { ops: number; heap: number }[] = [];\n let ops = opsPerRound;\n for (;;) {\n const s = settle(probe);\n if (!s.settled) return { status: \"skipped\", reason: \"gc-unsettled\" };\n rounds.push({ ops, heap: s.heap });\n if (rounds.length === BENCH_CONTRACT.LONG_RUN_ROUNDS) break;\n for (let p = 0; p < passesPerRound; p++) pass();\n ops += opsPerRound;\n }\n const skip = BENCH_CONTRACT.LONG_RUN_SETTLE_ROUNDS;\n const fitted = rounds.slice(skip);\n const slope = theilSenSlope(\n fitted.map((r) => r.ops),\n fitted.map((r) => r.heap),\n );\n const span = (BENCH_CONTRACT.LONG_RUN_ROUNDS - 1 - skip) * opsPerRound;\n const growth = slope * span;\n return {\n status: \"measured\",\n // Stryker disable next-line EqualityOperator: growth is a fitted slope times 24 rounds, so exactly 65,536 bytes is not reached in floating point (tried in memory.test.ts history).\n verdict: growth >= BENCH_CONTRACT.LONG_RUN_GROWTH_BYTES ? \"growing\" : \"flat\",\n ops: BENCH_CONTRACT.LONG_RUN_ROUNDS * opsPerRound,\n opsPerRound,\n settledHeap: rounds.map((r) => r.heap),\n slopeBytesPerOp: round(slope, 4),\n growthBytes: Math.round(growth),\n detectableBytesPerOp: round(BENCH_CONTRACT.LONG_RUN_GROWTH_BYTES / span, 4),\n };\n}\n","/**\n * The frozen measurement constants.\n *\n * Every value here is fixed by **ADR 0001: the performance measurement contract**\n * (`documentation/decisions/0001-perf-measurement-contract.md` in the `config` repo), which in turn\n * derives them from the PERF-P0 calibration: 3,200 4N-vs-N ratios on a linear workload, 320 on a\n * deliberately O(n²) one, across two runner classes on Node 22.23.1 / V8 12.4.\n *\n * They are **not tuning knobs and are deliberately not overridable per package.** The ceiling was\n * set from the worst false alarm in that population and the weakest real signal at `hl7`'s own\n * fixture size, so it is conditional on the estimator, the sampling shape *and* the warmup rule:\n * changing any one of them moves the operating point the ceiling was measured against. The ADR's\n * review triggers are the process for changing them.\n *\n * @packageDocumentation\n */\n\n/**\n * The measurement constants, frozen. See {@link PERF_CONTRACT}.\n */\nexport interface PerfContractShape {\n /**\n * Upper bound on `min(scaled) / min(base)`. **8**: above the worst false alarm in 3,200 samples\n * (6.649, `min`, container leg) and below the weakest real O(n²) signal measured at `hl7`'s own\n * fixture size (8.84). Margins 1.20× / 1.10×. Measured.\n */\n readonly RATIO_CEILING: number;\n /**\n * Lower bound on the same ratio. **1.5**: below all 3,200 samples (worst 1.702) and above the ≈1\n * that a same-workload-on-both-phases bug produces. Margin 1.13×, the thinnest in the contract.\n * Measured.\n */\n readonly RATIO_FLOOR: number;\n /**\n * Timed repetitions per phase. **5**, measured by construction: the whole calibration was taken at\n * 5 reps, and `min` over a different rep count is a different statistic.\n */\n readonly REPS: number;\n /**\n * The multiplier between the base phase and the scaled phase. **4×**, measured by construction:\n * every ratio in the dataset is a 4× step, so the ideal linear ratio is 4.\n */\n readonly SCALE_STEP: number;\n /**\n * Floor on the base phase's `min` sample, in milliseconds. **4**: the fastest base phase in all\n * 3,200 samples was 4.14 ms; below that the ceiling is extrapolation, so the gate refuses to\n * answer. Measured.\n */\n readonly MIN_PHASE_MS: number;\n /**\n * Minimum total warmup time before the stability rule may fire, in milliseconds. **500**: one\n * further P0 trial (136 to 294 ms of the same workload) brought rep-to-rep drift from as much as\n * 1.23× inside ±5%; 500 ms is 1.7 to 3.7× that. Measured.\n */\n readonly WARMUP_MIN_MS: number;\n /**\n * Minimum timed duration of one warmup batch, in milliseconds. **50**: judgement forced by\n * measurement: a single ~4 to 9 ms pass is not stable enough to satisfy ±5% three times running\n * (1 to 92% of warm phases do, by axis), and a 50 ms batch aggregates 5 to 12 passes. Aggregation is\n * what shrinks that jitter, so the batch length is load-bearing rather than a free choice.\n */\n readonly WARMUP_BATCH_MS: number;\n /**\n * Hard cap on total warmup, in milliseconds. **5 000**: judgement, no measured basis. Bounds the\n * fleet cost and leaves ~100 batches of headroom, so the cap is reached only by a genuinely\n * unsettled runtime.\n */\n readonly WARMUP_MAX_MS: number;\n /**\n * Relative tolerance for the warmup stability rule, as a fraction. **0.05** (±5%): judgement\n * anchored on P0's rep1/rep5 ratio inside a warm 5-rep phase (0.96 to 1.05× GitHub, 0.97 to 1.02×\n * container), which is a different statistic at a different granularity.\n */\n readonly WARMUP_STABLE_TOL: number;\n /**\n * How many consecutive batches must lie inside the tolerance. **3**: two consecutive can be met\n * by a coincidental pair on a heavy-tailed distribution; a third costs 50 ms against a 5 s cap.\n */\n readonly WARMUP_STABLE_BATCHES: number;\n}\n\n/**\n * The frozen measurement constants, as decided by ADR 0001.\n *\n * Read them, print them in your own diagnostics, assert against them, but do not expect a knob to\n * override them. A package that cannot satisfy the contract at its fixture sizes has a fixture-size\n * problem, and {@link assertScalingGateFires} is what turns that into a build failure.\n *\n * @example\n * ```ts\n * import { PERF_CONTRACT } from \"@cosyte/test-utils/perf\";\n *\n * PERF_CONTRACT.RATIO_CEILING; // => 8\n * PERF_CONTRACT.SCALE_STEP; // => 4\n * ```\n */\nexport const PERF_CONTRACT: PerfContractShape = Object.freeze({\n RATIO_CEILING: 8,\n RATIO_FLOOR: 1.5,\n REPS: 5,\n SCALE_STEP: 4,\n MIN_PHASE_MS: 4,\n WARMUP_MIN_MS: 500,\n WARMUP_BATCH_MS: 50,\n WARMUP_MAX_MS: 5_000,\n WARMUP_STABLE_TOL: 0.05,\n WARMUP_STABLE_BATCHES: 3,\n});\n","/**\n * The timing primitives: the warmup, the throughput samples and the latency calls. They run inside\n * a measuring process, one case per process, and take their clocks as arguments so their tests can\n * drive them with exact arithmetic instead of a machine's noise.\n *\n * @packageDocumentation\n */\n\n// The one source of the warmup rule. A relative import of the scaling gate's own contract file, not\n// of the published `@cosyte/test-utils/perf`, so the build bundles it into `dist` and this package\n// keeps no runtime dependency (that one would bring `fast-check` in as a peer). A change to the rule\n// there changes this package's build, so it needs a changeset that bumps this package too.\nimport { PERF_CONTRACT } from \"../../test-utils/src/perf/contract.js\";\nimport { BENCH_CONTRACT } from \"./contract.js\";\nimport { median, nearestRank, round } from \"./stats.js\";\n\n/**\n * The warmup rule, read from `PERF_CONTRACT` in `@cosyte/test-utils/perf` (ADR 0001 section 2) and\n * renamed, so the scaling gate and the benchmarks warm up by one rule kept in one place.\n *\n * @internal\n */\nexport const WARMUP = Object.freeze({\n MIN_MS: PERF_CONTRACT.WARMUP_MIN_MS,\n BATCH_MS: PERF_CONTRACT.WARMUP_BATCH_MS,\n MAX_MS: PERF_CONTRACT.WARMUP_MAX_MS,\n STABLE_TOL: PERF_CONTRACT.WARMUP_STABLE_TOL,\n STABLE_BATCHES: PERF_CONTRACT.WARMUP_STABLE_BATCHES,\n});\n\n/**\n * A millisecond clock. `performance.now` in a real run.\n *\n * @internal\n */\nexport type Clock = () => number;\n\n/**\n * A nanosecond clock. `process.hrtime.bigint` in a real run.\n *\n * @internal\n */\nexport type NsClock = () => bigint;\n\n/** What the warmup did. */\nexport interface WarmupReport {\n /** Whether three batches in a row came within 5% of their median before 5 s ran out. */\n readonly stable: boolean;\n /** Wall time spent warming, in milliseconds. */\n readonly elapsedMs: number;\n /** Passes over the corpus. */\n readonly passes: number;\n /** Each batch's milliseconds per pass. */\n readonly batches: readonly number[];\n}\n\n/**\n * Whether the last `STABLE_BATCHES` batches all lie within `STABLE_TOL` of their median.\n *\n * @internal\n */\nexport function batchesAreStable(batches: readonly number[]): boolean {\n if (batches.length < WARMUP.STABLE_BATCHES) return false;\n const window = batches.slice(-WARMUP.STABLE_BATCHES);\n const mid = median(window);\n // A zero median makes every relative distance `NaN`, which no tolerance accepts: refused.\n return window.every((b) => Math.abs(b - mid) / mid <= WARMUP.STABLE_TOL);\n}\n\n/**\n * Warm up by time, with a stability rule, never by a fixed count (ADR 0001 section 2): batches of\n * whole passes of at least 50 ms, at least 500 ms in all, until three batches in a row lie within\n * 5% of their median, or 5 s pass without that.\n *\n * @internal\n */\nexport function warmUp(pass: () => void, clock: Clock): WarmupReport {\n const start = clock();\n const batches: number[] = [];\n let passes = 0;\n for (;;) {\n const b0 = clock();\n let inBatch = 0;\n let batchMs: number;\n do {\n pass();\n inBatch++;\n batchMs = clock() - b0;\n } while (batchMs < WARMUP.BATCH_MS);\n passes += inBatch;\n batches.push(batchMs / inBatch);\n const elapsedMs = clock() - start;\n if (elapsedMs >= WARMUP.MIN_MS && batchesAreStable(batches)) {\n return { stable: true, elapsedMs, passes, batches };\n }\n if (elapsedMs >= WARMUP.MAX_MS) return { stable: false, elapsedMs, passes, batches };\n }\n}\n\n/**\n * Take `SAMPLES` throughput samples. A sample runs whole passes until `SAMPLE_MIN_MS` has passed\n * and reports milliseconds per operation.\n *\n * `inject` plants a slowdown, for calibration only: after each pass the loop reads the clock until\n * `inject` times that pass's own measured time has passed, then starts the next pass, so each\n * sample's time includes the spin. That is all it does. It does not make the measured code slower\n * by exactly that fraction: the spin changes the loop the engine compiles and what the processor\n * does between passes, so the ratio a comparison then measures is not `1 + inject` and differs by\n * workload and machine. A calibration reports the ratio it observed for each case\n * (`CalibrationBody.observed`), and a detection rate is read against that, never against `inject`.\n * Zero in every real measurement.\n *\n * @internal\n */\nexport function sampleThroughput(\n pass: () => void,\n opsPerPass: number,\n clock: Clock,\n inject = 0,\n): number[] {\n const samples: number[] = [];\n for (let s = 0; s < BENCH_CONTRACT.SAMPLES; s++) {\n const t0 = clock();\n let t = t0;\n let passes = 0;\n do {\n const p0 = t;\n pass();\n t = clock();\n // Zero in a real measurement, so `until` is `t` and the clock is not read again.\n const until = t + inject * (t - p0);\n while (t < until) t = clock();\n passes++;\n } while (t - t0 < BENCH_CONTRACT.SAMPLE_MIN_MS);\n samples.push((t - t0) / (passes * opsPerPass));\n }\n return samples;\n}\n\n/**\n * The median gap between two back-to-back reads of the nanosecond clock: what one latency reading\n * costs before the call itself.\n *\n * @internal\n */\nexport function timerOverheadNs(ns: NsClock, reads = 1_000): number {\n const gaps: number[] = [];\n for (let i = 0; i < reads; i++) {\n const a = ns();\n const b = ns();\n gaps.push(Number(b - a));\n }\n return median(gaps);\n}\n\n/** Per-call latency, or why there is none. */\nexport type LatencyReport =\n | {\n readonly status: \"measured\";\n readonly calls: number;\n readonly timerOverheadNs: number;\n /** `null` when fewer than `LATENCY_TAIL_CALLS` calls lie above the percentile. */\n readonly p50Ns: number | null;\n readonly p95Ns: number | null;\n readonly p99Ns: number | null;\n readonly maxNs: number;\n }\n | {\n readonly status: \"skipped\";\n readonly reason: \"latency-below-timer-resolution\";\n readonly calls: number;\n readonly timerOverheadNs: number;\n readonly p50Ns: number;\n };\n\n/**\n * A percentile, or `null` when too few calls lie above it to tell it from the maximum.\n *\n * @internal\n */\nexport function tailPercentile(times: readonly number[], q: number): number | null {\n return times.length * (1 - q) >= BENCH_CONTRACT.LATENCY_TAIL_CALLS ? nearestRank(times, q) : null;\n}\n\n/**\n * Time calls one at a time: at least `LATENCY_MIN_CALLS`, then until `LATENCY_BUDGET_MS` or\n * `LATENCY_MAX_CALLS`. Skips, loudly and typed, when the median call is under\n * `LATENCY_TIMER_MULTIPLE` clock reads: then the clock, not the call, is being measured.\n *\n * @internal\n */\nexport function measureLatency(call: (i: number) => void, ns: NsClock): LatencyReport {\n const overhead = timerOverheadNs(ns);\n const budgetNs = BigInt(BENCH_CONTRACT.LATENCY_BUDGET_MS) * 1_000_000n;\n const times: number[] = [];\n const start = ns();\n let now = start;\n while (\n times.length < BENCH_CONTRACT.LATENCY_MAX_CALLS &&\n (times.length < BENCH_CONTRACT.LATENCY_MIN_CALLS || now - start < budgetNs)\n ) {\n const a = ns();\n call(times.length);\n now = ns();\n times.push(Number(now - a));\n }\n const p50 = nearestRank(times, 0.5);\n if (p50 < BENCH_CONTRACT.LATENCY_TIMER_MULTIPLE * overhead) {\n return {\n status: \"skipped\",\n reason: \"latency-below-timer-resolution\",\n calls: times.length,\n timerOverheadNs: round(overhead, 1),\n p50Ns: p50,\n };\n }\n return {\n status: \"measured\",\n calls: times.length,\n timerOverheadNs: round(overhead, 1),\n p50Ns: tailPercentile(times, 0.5),\n p95Ns: tailPercentile(times, 0.95),\n p99Ns: tailPercentile(times, 0.99),\n maxNs: Math.max(...times),\n };\n}\n","/**\n * How a package describes its benchmarks.\n *\n * A suite is a module whose default export is {@link defineSuite}'s result. It is handed the module\n * under test (`bind`) rather than importing it, so one suite file measures the base build and the\n * head build of a comparison. Every corpus is built in code (`build`): the runner never takes a data\n * file, which is how its inputs stay synthetic by construction.\n *\n * @packageDocumentation\n */\n\n/** One corpus: the inputs one pass runs over, built outside every timed region. */\nexport interface BenchCorpus<TInput> {\n /** The inputs. One pass calls the measured function once on each. */\n readonly inputs: readonly TInput[];\n /** Bytes one pass handles, when it means something. Turns operations a second into bytes a second. */\n readonly bytes?: number;\n /** Called once the process has finished with this corpus, for example to remove a temporary directory. */\n readonly teardown?: () => void;\n}\n\n/** One input size of a benchmark: small, large, or anything between. */\nexport interface BenchSize<TInput> {\n /** How a reader names this size, for example `\"1 KiB\"`. Unique within its benchmark. */\n readonly label: string;\n /** Build the corpus. Called once per process, before anything is timed. */\n readonly build: () => BenchCorpus<TInput>;\n /**\n * Whether the base against head comparison runs this size. Defaults to `true`. Set `false` for a\n * size whose processes are too slow for a pull request's time budget; a reading still measures it.\n */\n readonly compare?: boolean;\n}\n\n/** One benchmark: a function measured at one or more input sizes. */\nexport interface Benchmark<TTarget, TInput, TResult> {\n /** Unique within the suite. */\n readonly name: string;\n /** The sizes, smallest first. Each size is measured on its own. */\n readonly sizes: readonly BenchSize<TInput>[];\n /** Take the imported module under test and return the function to measure. */\n readonly bind: (target: TTarget) => (input: TInput) => TResult;\n /**\n * Reduce a result to a number summed into a sink the runner checks, so the compiler cannot delete\n * the measured call. Defaults to 1 for each result that is not `null` or `undefined`; supply it\n * when the function returns nothing. The sum must grow over a measurement, so return a positive\n * number for at least some results; a run whose sink did not grow is refused.\n */\n readonly weigh?: (result: TResult) => number;\n /** The size label the long run (is the heap flat?) uses. Defaults to the first size. */\n readonly longRun?: string;\n}\n\n/** A benchmark with its input and result types erased, as a suite holds it. */\nexport type AnyBenchmark<TTarget> = Benchmark<TTarget, unknown, unknown>;\n\n/** A suite: the benchmarks of one package. */\nexport interface BenchSuite<TTarget> {\n /** Shown in every report, for example `\"@cosyte/script-utils phi-scan\"`. */\n readonly name: string;\n /** At least one. */\n readonly benchmarks: readonly AnyBenchmark<TTarget>[];\n}\n\n/**\n * Type one benchmark. Only a type helper: it returns its argument, with the input and result types\n * erased so benchmarks of different shapes can sit in one suite.\n *\n * @example\n * ```ts\n * import { defineBenchmark } from \"@cosyte/bench\";\n *\n * const b = defineBenchmark({\n * name: \"split\",\n * sizes: [{ label: \"1 KiB\", build: () => ({ inputs: [\"a|b\".repeat(341)] }) }],\n * bind: () => (s: string) => s.split(\"|\"),\n * });\n * b.name; // => \"split\"\n * ```\n */\nexport function defineBenchmark<TTarget, TInput, TResult>(\n benchmark: Benchmark<TTarget, TInput, TResult>,\n): AnyBenchmark<TTarget> {\n return benchmark as unknown as AnyBenchmark<TTarget>;\n}\n\n/**\n * Check a suite and freeze it. Throws a `TypeError` naming the problem: an empty name, no\n * benchmarks, a repeated name or label, a benchmark with no sizes, or a `longRun` naming no size.\n *\n * @example\n * ```ts\n * import { defineBenchmark, defineSuite } from \"@cosyte/bench\";\n *\n * export default defineSuite({\n * name: \"example\",\n * benchmarks: [\n * defineBenchmark({\n * name: \"split\",\n * sizes: [{ label: \"1 KiB\", build: () => ({ inputs: [\"a|b\".repeat(341)] }) }],\n * bind: () => (s: string) => s.split(\"|\"),\n * }),\n * ],\n * });\n * ```\n */\nexport function defineSuite<TTarget>(suite: BenchSuite<TTarget>): BenchSuite<TTarget> {\n return checkSuite(suite);\n}\n\nfunction fail(message: string): never {\n throw new TypeError(`@cosyte/bench: ${message}`);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction checkName(value: unknown, what: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") fail(`${what} must be a non-empty string`);\n return value;\n}\n\n/**\n * Check that a value is a suite, as a suite module's default export must be. The messages name\n * benchmarks and sizes, never an input.\n *\n * @internal\n */\nexport function checkSuite(value: unknown): BenchSuite<unknown> {\n if (!isRecord(value)) fail(\"a suite must be an object made by defineSuite\");\n const name = checkName(value[\"name\"], \"the suite name\");\n const benchmarks = value[\"benchmarks\"];\n if (!Array.isArray(benchmarks) || benchmarks.length === 0) {\n fail(`suite \"${name}\" must have at least one benchmark`);\n }\n const seen = new Set<string>();\n for (const b of benchmarks as unknown[]) {\n if (!isRecord(b)) fail(`suite \"${name}\" holds a benchmark that is not an object`);\n const bName = checkName(b[\"name\"], `a benchmark name in suite \"${name}\"`);\n if (seen.has(bName)) fail(`suite \"${name}\" has two benchmarks named \"${bName}\"`);\n seen.add(bName);\n if (typeof b[\"bind\"] !== \"function\") fail(`benchmark \"${bName}\" has no bind function`);\n if (b[\"weigh\"] !== undefined && typeof b[\"weigh\"] !== \"function\") {\n fail(`benchmark \"${bName}\" has a weigh that is not a function`);\n }\n const sizes = b[\"sizes\"];\n if (!Array.isArray(sizes) || sizes.length === 0) fail(`benchmark \"${bName}\" has no sizes`);\n const labels = new Set<string>();\n for (const s of sizes as unknown[]) {\n if (!isRecord(s)) fail(`benchmark \"${bName}\" holds a size that is not an object`);\n const label = checkName(s[\"label\"], `a size label in benchmark \"${bName}\"`);\n if (labels.has(label)) fail(`benchmark \"${bName}\" has two sizes labelled \"${label}\"`);\n labels.add(label);\n if (typeof s[\"build\"] !== \"function\")\n fail(`size \"${label}\" of \"${bName}\" has no build function`);\n if (s[\"compare\"] !== undefined && typeof s[\"compare\"] !== \"boolean\") {\n fail(`size \"${label}\" of \"${bName}\" has a compare that is not true or false`);\n }\n }\n if (b[\"longRun\"] !== undefined && !labels.has(b[\"longRun\"] as string)) {\n fail(`benchmark \"${bName}\" names a longRun size it does not have`);\n }\n }\n return Object.freeze(value) as unknown as BenchSuite<unknown>;\n}\n\n/** One case: a benchmark at one size. */\nexport interface CaseRef {\n readonly benchmark: string;\n readonly size: string;\n}\n\n/**\n * Every case in a suite, in suite order, optionally kept to the ones `only` names. A name in\n * `only` matches a whole benchmark (`\"scan\"`) or one case (`\"scan @ 1 KiB\"`). A name that matches\n * nothing throws, so a typo cannot quietly measure less than was asked. With `forComparison`, a\n * size marked `compare: false` is left out.\n *\n * @internal\n */\nexport function listCases(\n suite: BenchSuite<unknown>,\n only: readonly string[] = [],\n forComparison = false,\n): CaseRef[] {\n const all: CaseRef[] = [];\n for (const b of suite.benchmarks) {\n for (const s of b.sizes) {\n if (!forComparison || s.compare !== false) all.push({ benchmark: b.name, size: s.label });\n }\n }\n if (only.length === 0) return all;\n for (const o of only) {\n if (!all.some((c) => c.benchmark === o || caseId(c) === o)) {\n fail(`--only \"${o}\" names no benchmark or case in suite \"${suite.name}\"`);\n }\n }\n return all.filter((c) => only.includes(c.benchmark) || only.includes(caseId(c)));\n}\n\n/**\n * How a case is named in reports and in `--only`: `benchmark @ size`.\n *\n * @internal\n */\nexport function caseId(c: CaseRef): string {\n return `${c.benchmark} @ ${c.size}`;\n}\n\n/**\n * Find a case's benchmark and size in a suite. Throws when either is missing.\n *\n * @internal\n */\nexport function findCase(\n suite: BenchSuite<unknown>,\n c: CaseRef,\n): { benchmark: AnyBenchmark<unknown>; size: BenchSize<unknown> } {\n const benchmark = suite.benchmarks.find((b) => b.name === c.benchmark);\n const size = benchmark?.sizes.find((s) => s.label === c.size);\n // No benchmark means no size either, so one check covers both.\n if (size === undefined) fail(`suite \"${suite.name}\" has no case \"${caseId(c)}\"`);\n return { benchmark: benchmark as AnyBenchmark<unknown>, size };\n}\n","/**\n * What runs inside a measuring process: one job, one case, then exit.\n *\n * One case per process is deliberate. A process that measured several functions through one harness\n * loop would make that loop's call site polymorphic, and the cost of the second case would depend on\n * which case ran first. A fresh process also carries no compiled code, heap layout or cache warmth\n * from one side of a comparison to the other, which is what makes the rounds independent.\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync } from \"node:fs\";\nimport { performance } from \"node:perf_hooks\";\nimport { pathToFileURL } from \"node:url\";\n\nimport {\n measureAllocations,\n measureLongRun,\n measurePeak,\n realProbe,\n type AllocationReport,\n type HeapProbe,\n type LongRunReport,\n type PeakReport,\n} from \"./memory.js\";\nimport {\n measureLatency,\n sampleThroughput,\n warmUp,\n type Clock,\n type LatencyReport,\n type NsClock,\n type WarmupReport,\n} from \"./measure.js\";\nimport { median } from \"./stats.js\";\nimport { checkSuite, findCase, type CaseRef } from \"./suite.js\";\n\n/**\n * One job for one measuring process.\n *\n * @internal\n */\nexport type Job =\n | {\n readonly mode: \"timing\";\n readonly suite: string;\n readonly target: string;\n readonly case: CaseRef;\n /** Also time single calls for the latency percentiles. */\n readonly latency: boolean;\n /** A known slowdown, for calibration only. Zero in a real measurement. */\n readonly inject: number;\n }\n | {\n readonly mode: \"alloc\";\n readonly suite: string;\n readonly target: string;\n readonly case: CaseRef;\n }\n | {\n readonly mode: \"heap\";\n readonly suite: string;\n readonly target: string;\n readonly case: CaseRef;\n /** Also run the long run (is the heap flat?) on this case. */\n readonly longRun: boolean;\n };\n\n/** The shape of one corpus, as reported. Sizes and counts only, never content. */\nexport interface CorpusShape {\n readonly inputsPerPass: number;\n readonly bytesPerPass: number | null;\n}\n\n/**\n * What a timing job measured.\n *\n * @internal\n */\nexport type TimingResult = CorpusShape & {\n readonly mode: \"timing\";\n readonly case: CaseRef;\n readonly warmup: WarmupReport;\n} & (\n | {\n readonly status: \"measured\";\n /** Milliseconds per operation, one per sample, in the order taken. */\n readonly samplesMsPerOp: readonly number[];\n readonly medianMsPerOp: number;\n readonly latency: LatencyReport | null;\n }\n | { readonly status: \"skipped\"; readonly reason: \"warmup-unstable\" }\n );\n\n/**\n * What a memory job measured.\n *\n * @internal\n */\nexport type MemoryResult = CorpusShape & {\n readonly mode: \"alloc\" | \"heap\";\n readonly case: CaseRef;\n readonly allocations: AllocationReport | null;\n readonly peak: PeakReport | null;\n readonly longRun: LongRunReport | null;\n};\n\n/**\n * What any job measured.\n *\n * @internal\n */\nexport type JobResult = TimingResult | MemoryResult;\n\n/**\n * The clocks and the heap a job reads. Real ones in a measuring process; scripted ones in tests.\n *\n * @internal\n */\nexport interface JobDeps {\n readonly clock: Clock;\n readonly ns: NsClock;\n readonly probe: HeapProbe | null;\n}\n\nconst realDeps = (): JobDeps => ({\n clock: () => performance.now(),\n ns: () => process.hrtime.bigint(),\n probe: realProbe(),\n});\n\nconst defaultWeigh = (result: unknown): number => (result === undefined || result === null ? 0 : 1);\n\nconst SKIPPED_NO_GC = { status: \"skipped\", reason: \"gc-unavailable\" } as const;\n\n/**\n * Run suite code, and if it throws, throw again without its message. Suite code handles the inputs,\n * and a message may quote one: a report that copied it would put an input in a CI log. The error's\n * class name and the phase are kept, which is enough to know where to look.\n *\n * @internal\n */\nexport function guard<T>(c: CaseRef, phase: string, fn: () => T): T {\n try {\n return fn();\n } catch (error) {\n const kind = error instanceof Error ? error.name : typeof error;\n throw new Error(\n `@cosyte/bench: case \"${c.benchmark} @ ${c.size}\": the suite's ${phase} threw (${kind}). ` +\n \"Its message is not printed, because it may quote an input; run the case directly to read it.\",\n { cause: error },\n );\n }\n}\n\n/**\n * The warmup's last pace, in milliseconds per operation.\n *\n * @internal\n */\nexport function msPerOp(warmup: WarmupReport, shape: CorpusShape): number {\n return (warmup.batches[warmup.batches.length - 1] ?? 0) / shape.inputsPerPass;\n}\n\n/**\n * Measure one job once its corpus is built and its code warm.\n *\n * @internal\n */\nexport function measureJob(\n job: Job,\n shape: CorpusShape,\n warmup: WarmupReport,\n pass: () => void,\n call: (i: number) => void,\n deps: JobDeps,\n): JobResult {\n const base = { case: job.case, ...shape };\n if (job.mode === \"timing\") {\n if (!warmup.stable) {\n return { mode: \"timing\", ...base, warmup, status: \"skipped\", reason: \"warmup-unstable\" };\n }\n const samples = sampleThroughput(pass, shape.inputsPerPass, deps.clock, job.inject);\n return {\n mode: \"timing\",\n ...base,\n warmup,\n status: \"measured\",\n samplesMsPerOp: samples,\n medianMsPerOp: median(samples),\n latency: job.latency ? measureLatency(call, deps.ns) : null,\n };\n }\n const probe = deps.probe;\n if (job.mode === \"alloc\") {\n return {\n mode: \"alloc\",\n ...base,\n allocations:\n probe === null ? SKIPPED_NO_GC : measureAllocations(call, probe, msPerOp(warmup, shape)),\n peak: null,\n longRun: null,\n };\n }\n let longRun: LongRunReport | null = null;\n if (job.longRun) {\n longRun =\n probe === null ? SKIPPED_NO_GC : measureLongRun(pass, shape.inputsPerPass, probe, deps.clock);\n }\n return {\n mode: \"heap\",\n ...base,\n allocations: null,\n peak: probe === null ? SKIPPED_NO_GC : measurePeak(call, probe),\n longRun,\n };\n}\n\n/**\n * Whether a job measured anything, so its sink should have moved. A job that only skipped ran no\n * measured calls after its warmup.\n *\n * @internal\n */\nexport function measuredSomething(result: JobResult): boolean {\n if (result.mode === \"timing\") return result.status === \"measured\";\n return [result.allocations, result.peak, result.longRun].some((r) => r?.status === \"measured\");\n}\n\n/**\n * Run one job: import the suite and the target, build the case's corpus, measure, tear down.\n *\n * Throws when the suite is not a suite, the case is missing, the corpus is empty, or the measured\n * loop's sink did not grow (a loop the compiler may have deleted measures nothing). Messages name\n * the case, never an input.\n *\n * @internal\n */\nexport async function runJob(job: Job, deps: JobDeps = realDeps()): Promise<JobResult> {\n const suiteModule = (await import(pathToFileURL(job.suite).href)) as { default?: unknown };\n const suite = checkSuite(suiteModule.default);\n const target: unknown = await import(pathToFileURL(job.target).href);\n const { benchmark, size } = findCase(suite, job.case);\n const corpus = guard(job.case, \"build\", () => size.build());\n try {\n const inputs = corpus.inputs;\n if (!Array.isArray(inputs) || inputs.length === 0) {\n throw new Error(\n `@cosyte/bench: case \"${job.case.benchmark} @ ${job.case.size}\" built an empty corpus`,\n );\n }\n const fn = guard(job.case, \"bind\", () => benchmark.bind(target));\n const weigh = benchmark.weigh ?? defaultWeigh;\n const sink = { value: 0 };\n const pass = () => {\n let local = 0;\n for (const input of inputs) local += weigh(fn(input));\n sink.value += local;\n };\n const call = (i: number) => {\n sink.value += weigh(fn(inputs[i % inputs.length]));\n };\n const shape: CorpusShape = {\n inputsPerPass: inputs.length,\n bytesPerPass: typeof corpus.bytes === \"number\" ? corpus.bytes : null,\n };\n const warmup = guard(job.case, \"measured function\", () => warmUp(pass, deps.clock));\n const before = sink.value;\n const result = guard(job.case, \"measured function\", () =>\n measureJob(job, shape, warmup, pass, call, deps),\n );\n if (!(sink.value > before) && measuredSomething(result)) {\n throw new Error(\n `@cosyte/bench: case \"${job.case.benchmark} @ ${job.case.size}\": the sink did not grow, so ` +\n \"the measured loop may have been deleted by the compiler, or `weigh` gave nothing positive. \" +\n \"Supply a `weigh` that returns a positive number for a result.\",\n );\n }\n return result;\n } finally {\n corpus.teardown?.();\n }\n}\n\n/**\n * The measuring process's main: read the job file, run it, write `{ ok, result }` or\n * `{ ok, error }` to the out file. Returns the exit code: 0 measured, 2 could not.\n *\n * @internal\n */\nexport async function childMain(jobFile: string, outFile: string, deps?: JobDeps): Promise<number> {\n try {\n const job = JSON.parse(readFileSync(jobFile, \"utf8\")) as Job;\n const result = await runJob(job, deps);\n writeFileSync(outFile, JSON.stringify({ ok: true, result }));\n return 0;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n writeFileSync(outFile, JSON.stringify({ ok: false, error: message }));\n return 2;\n }\n}\n","/**\n * The measuring process. `spawn.ts` starts it with a job file and an out file; it runs the job and\n * exits 0 (measured) or 2 (could not). Its logic is `childMain`, which the tests run in-process; this\n * file is the two lines around it.\n *\n * @packageDocumentation\n */\n\nimport { childMain } from \"./child.js\";\n\nprocess.exitCode = await childMain(process.argv[2] ?? \"\", process.argv[3] ?? \"\");\n"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|