benchforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +432 -0
  2. package/bin/benchforge +3 -0
  3. package/dist/bin/benchforge.mjs +9 -0
  4. package/dist/bin/benchforge.mjs.map +1 -0
  5. package/dist/browser/index.js +914 -0
  6. package/dist/index.mjs +3 -0
  7. package/dist/src-CGuaC3Wo.mjs +3676 -0
  8. package/dist/src-CGuaC3Wo.mjs.map +1 -0
  9. package/package.json +49 -0
  10. package/src/BenchMatrix.ts +380 -0
  11. package/src/Benchmark.ts +33 -0
  12. package/src/BenchmarkReport.ts +156 -0
  13. package/src/GitUtils.ts +79 -0
  14. package/src/HtmlDataPrep.ts +148 -0
  15. package/src/MeasuredResults.ts +127 -0
  16. package/src/NodeGC.ts +48 -0
  17. package/src/PermutationTest.ts +115 -0
  18. package/src/StandardSections.ts +268 -0
  19. package/src/StatisticalUtils.ts +176 -0
  20. package/src/TypeUtil.ts +8 -0
  21. package/src/bin/benchforge.ts +4 -0
  22. package/src/browser/BrowserGcStats.ts +44 -0
  23. package/src/browser/BrowserHeapSampler.ts +248 -0
  24. package/src/cli/CliArgs.ts +64 -0
  25. package/src/cli/FilterBenchmarks.ts +68 -0
  26. package/src/cli/RunBenchCLI.ts +856 -0
  27. package/src/export/JsonExport.ts +103 -0
  28. package/src/export/JsonFormat.ts +91 -0
  29. package/src/export/PerfettoExport.ts +203 -0
  30. package/src/heap-sample/HeapSampleReport.ts +196 -0
  31. package/src/heap-sample/HeapSampler.ts +78 -0
  32. package/src/html/HtmlReport.ts +131 -0
  33. package/src/html/HtmlTemplate.ts +284 -0
  34. package/src/html/Types.ts +88 -0
  35. package/src/html/browser/CIPlot.ts +287 -0
  36. package/src/html/browser/HistogramKde.ts +118 -0
  37. package/src/html/browser/LegendUtils.ts +163 -0
  38. package/src/html/browser/RenderPlots.ts +263 -0
  39. package/src/html/browser/SampleTimeSeries.ts +389 -0
  40. package/src/html/browser/Types.ts +96 -0
  41. package/src/html/browser/index.ts +1 -0
  42. package/src/html/index.ts +17 -0
  43. package/src/index.ts +92 -0
  44. package/src/matrix/CaseLoader.ts +36 -0
  45. package/src/matrix/MatrixFilter.ts +103 -0
  46. package/src/matrix/MatrixReport.ts +290 -0
  47. package/src/matrix/VariantLoader.ts +46 -0
  48. package/src/runners/AdaptiveWrapper.ts +391 -0
  49. package/src/runners/BasicRunner.ts +368 -0
  50. package/src/runners/BenchRunner.ts +60 -0
  51. package/src/runners/CreateRunner.ts +11 -0
  52. package/src/runners/GcStats.ts +107 -0
  53. package/src/runners/RunnerOrchestrator.ts +374 -0
  54. package/src/runners/RunnerUtils.ts +2 -0
  55. package/src/runners/TimingUtils.ts +13 -0
  56. package/src/runners/WorkerScript.ts +256 -0
  57. package/src/table-util/ConvergenceFormatters.ts +19 -0
  58. package/src/table-util/Formatters.ts +152 -0
  59. package/src/table-util/README.md +70 -0
  60. package/src/table-util/TableReport.ts +293 -0
  61. package/src/table-util/test/TableReport.test.ts +105 -0
  62. package/src/table-util/test/TableValueExtractor.test.ts +41 -0
  63. package/src/table-util/test/TableValueExtractor.ts +100 -0
  64. package/src/test/AdaptiveRunner.test.ts +185 -0
  65. package/src/test/AdaptiveStatistics.integration.ts +119 -0
  66. package/src/test/BenchmarkReport.test.ts +82 -0
  67. package/src/test/BrowserBench.e2e.test.ts +44 -0
  68. package/src/test/BrowserBench.test.ts +79 -0
  69. package/src/test/GcStats.test.ts +94 -0
  70. package/src/test/PermutationTest.test.ts +121 -0
  71. package/src/test/RunBenchCLI.test.ts +166 -0
  72. package/src/test/RunnerOrchestrator.test.ts +102 -0
  73. package/src/test/StatisticalUtils.test.ts +112 -0
  74. package/src/test/TestUtils.ts +93 -0
  75. package/src/test/fixtures/test-bench-script.ts +30 -0
  76. package/src/tests/AdaptiveConvergence.test.ts +177 -0
  77. package/src/tests/AdaptiveSampling.test.ts +240 -0
  78. package/src/tests/BenchMatrix.test.ts +366 -0
  79. package/src/tests/MatrixFilter.test.ts +117 -0
  80. package/src/tests/MatrixReport.test.ts +139 -0
  81. package/src/tests/RealDataValidation.test.ts +177 -0
  82. package/src/tests/fixtures/baseline/impl.ts +4 -0
  83. package/src/tests/fixtures/bevy30-samples.ts +158 -0
  84. package/src/tests/fixtures/cases/asyncCases.ts +7 -0
  85. package/src/tests/fixtures/cases/cases.ts +8 -0
  86. package/src/tests/fixtures/cases/variants/product.ts +2 -0
  87. package/src/tests/fixtures/cases/variants/sum.ts +2 -0
  88. package/src/tests/fixtures/discover/fast.ts +1 -0
  89. package/src/tests/fixtures/discover/slow.ts +4 -0
  90. package/src/tests/fixtures/invalid/bad.ts +1 -0
  91. package/src/tests/fixtures/loader/fast.ts +1 -0
  92. package/src/tests/fixtures/loader/slow.ts +4 -0
  93. package/src/tests/fixtures/loader/stateful.ts +2 -0
  94. package/src/tests/fixtures/stateful/stateful.ts +2 -0
  95. package/src/tests/fixtures/variants/extra.ts +1 -0
  96. package/src/tests/fixtures/variants/impl.ts +1 -0
  97. package/src/tests/fixtures/worker/fast.ts +1 -0
  98. package/src/tests/fixtures/worker/slow.ts +4 -0
@@ -0,0 +1,3676 @@
1
+ import fs, { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { getHeapStatistics } from "node:v8";
4
+ import { execSync, fork, spawn } from "node:child_process";
5
+ import path, { dirname, extname, join, resolve } from "node:path";
6
+ import pico from "picocolors";
7
+ import { table } from "table";
8
+ import yargs from "yargs";
9
+ import { hideBin } from "yargs/helpers";
10
+ import { chromium } from "playwright";
11
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
12
+ import { createServer } from "node:http";
13
+ import open from "open";
14
+
15
+ //#region src/matrix/CaseLoader.ts
16
+ /** Load a cases module by URL */
17
+ async function loadCasesModule(moduleUrl) {
18
+ const module = await import(moduleUrl);
19
+ if (!Array.isArray(module.cases)) throw new Error(`Cases module at ${moduleUrl} must export 'cases' array`);
20
+ return {
21
+ cases: module.cases,
22
+ defaultCases: module.defaultCases,
23
+ defaultVariants: module.defaultVariants,
24
+ loadCase: module.loadCase
25
+ };
26
+ }
27
+ /** Load case data from a CasesModule or pass through the caseId */
28
+ async function loadCaseData(casesModule, caseId) {
29
+ if (casesModule?.loadCase) return casesModule.loadCase(caseId);
30
+ return { data: caseId };
31
+ }
32
+
33
+ //#endregion
34
+ //#region src/matrix/VariantLoader.ts
35
+ /** Discover variant ids from a directory of .ts files */
36
+ async function discoverVariants(dirUrl) {
37
+ const dirPath = fileURLToPath(dirUrl);
38
+ return (await fs.readdir(dirPath, { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".ts")).map((e) => e.name.slice(0, -3)).sort();
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/StatisticalUtils.ts
43
+ const bootstrapSamples = 1e4;
44
+ const confidence = .95;
45
+ /** @return relative standard deviation (coefficient of variation) */
46
+ function coefficientOfVariation(samples) {
47
+ const mean = average(samples);
48
+ if (mean === 0) return 0;
49
+ return standardDeviation(samples) / mean;
50
+ }
51
+ /** @return median absolute deviation for robust variability measure */
52
+ function medianAbsoluteDeviation(samples) {
53
+ const median = percentile$1(samples, .5);
54
+ return percentile$1(samples.map((x) => Math.abs(x - median)), .5);
55
+ }
56
+ /** @return mean of values */
57
+ function average(values) {
58
+ return values.reduce((a, b) => a + b, 0) / values.length;
59
+ }
60
+ /** @return standard deviation with Bessel's correction */
61
+ function standardDeviation(samples) {
62
+ if (samples.length <= 1) return 0;
63
+ const mean = average(samples);
64
+ const variance = samples.reduce((sum, x) => sum + (x - mean) ** 2, 0) / (samples.length - 1);
65
+ return Math.sqrt(variance);
66
+ }
67
+ /** @return value at percentile p (0-1) */
68
+ function percentile$1(values, p) {
69
+ const sorted = [...values].sort((a, b) => a - b);
70
+ const index = Math.ceil(sorted.length * p) - 1;
71
+ return sorted[Math.max(0, index)];
72
+ }
73
+ /** @return bootstrap resample with replacement */
74
+ function createResample(samples) {
75
+ const n = samples.length;
76
+ const rand = () => samples[Math.floor(Math.random() * n)];
77
+ return Array.from({ length: n }, rand);
78
+ }
79
+ /** @return confidence interval [lower, upper] */
80
+ function computeInterval(medians, confidence) {
81
+ const alpha = (1 - confidence) / 2;
82
+ return [percentile$1(medians, alpha), percentile$1(medians, 1 - alpha)];
83
+ }
84
+ /** Bin values into histogram for compact visualization */
85
+ function binValues(values, binCount = 30) {
86
+ const sorted = [...values].sort((a, b) => a - b);
87
+ const min = sorted[0];
88
+ const max = sorted[sorted.length - 1];
89
+ if (min === max) return [{
90
+ x: min,
91
+ count: values.length
92
+ }];
93
+ const step = (max - min) / binCount;
94
+ const counts = new Array(binCount).fill(0);
95
+ for (const v of values) {
96
+ const bin = Math.min(Math.floor((v - min) / step), binCount - 1);
97
+ counts[bin]++;
98
+ }
99
+ return counts.map((count, i) => ({
100
+ x: min + (i + .5) * step,
101
+ count
102
+ }));
103
+ }
104
+ /** @return bootstrap CI for percentage difference between baseline and current medians */
105
+ function bootstrapDifferenceCI(baseline, current, options = {}) {
106
+ const { resamples = bootstrapSamples, confidence: conf = confidence } = options;
107
+ const baselineMedian = percentile$1(baseline, .5);
108
+ const observedPercent = (percentile$1(current, .5) - baselineMedian) / baselineMedian * 100;
109
+ const diffs = [];
110
+ for (let i = 0; i < resamples; i++) {
111
+ const resB = createResample(baseline);
112
+ const resC = createResample(current);
113
+ const medB = percentile$1(resB, .5);
114
+ const medC = percentile$1(resC, .5);
115
+ diffs.push((medC - medB) / medB * 100);
116
+ }
117
+ const ci = computeInterval(diffs, conf);
118
+ const excludesZero = ci[0] > 0 || ci[1] < 0;
119
+ let direction = "uncertain";
120
+ if (excludesZero) direction = observedPercent < 0 ? "faster" : "slower";
121
+ const histogram = binValues(diffs);
122
+ return {
123
+ percent: observedPercent,
124
+ ci,
125
+ direction,
126
+ histogram
127
+ };
128
+ }
129
+
130
+ //#endregion
131
+ //#region src/runners/RunnerUtils.ts
132
+ const msToNs = 1e6;
133
+
134
+ //#endregion
135
+ //#region src/runners/AdaptiveWrapper.ts
136
+ const minTime = 1e3;
137
+ const maxTime = 1e4;
138
+ const targetConfidence = 95;
139
+ const fallbackThreshold = 80;
140
+ const windowSize = 50;
141
+ const stability = .05;
142
+ const initialBatch = 100;
143
+ const continueBatch = 100;
144
+ const continueIterations = 10;
145
+ /** @return adaptive sampling runner wrapper */
146
+ function createAdaptiveWrapper(baseRunner, options) {
147
+ return { async runBench(benchmark, runnerOptions, params) {
148
+ return runAdaptiveBench(baseRunner, benchmark, runnerOptions, options, params);
149
+ } };
150
+ }
151
+ /** @return results using adaptive sampling strategy */
152
+ async function runAdaptiveBench(baseRunner, benchmark, runnerOptions, options, params) {
153
+ const { minTime: min = options.minTime ?? minTime, maxTime: max = options.maxTime ?? maxTime, targetConfidence: target = options.convergence ?? targetConfidence } = runnerOptions;
154
+ const allSamples = [];
155
+ const warmup = await collectInitial(baseRunner, benchmark, runnerOptions, params, allSamples);
156
+ const startTime = performance.now();
157
+ await collectAdaptive(baseRunner, benchmark, runnerOptions, params, allSamples, {
158
+ minTime: min,
159
+ maxTime: max,
160
+ targetConfidence: target,
161
+ startTime
162
+ });
163
+ return buildResults(allSamples, startTime, checkConvergence(allSamples.map((s) => s * msToNs)), benchmark.name, warmup);
164
+ }
165
+ /** @return warmupSamples from initial batch */
166
+ async function collectInitial(baseRunner, benchmark, runnerOptions, params, allSamples) {
167
+ const opts = {
168
+ ...runnerOptions,
169
+ maxTime: initialBatch,
170
+ maxIterations: void 0
171
+ };
172
+ const results = await baseRunner.runBench(benchmark, opts, params);
173
+ appendSamples(results[0], allSamples);
174
+ return results[0].warmupSamples;
175
+ }
176
+ /** @return samples until convergence or timeout */
177
+ async function collectAdaptive(baseRunner, benchmark, runnerOptions, params, allSamples, limits) {
178
+ const { minTime, maxTime, targetConfidence, startTime } = limits;
179
+ let lastLog = 0;
180
+ while (performance.now() - startTime < maxTime) {
181
+ const convergence = checkConvergence(allSamples.map((s) => s * msToNs));
182
+ const elapsed = performance.now() - startTime;
183
+ if (elapsed - lastLog > 1e3) {
184
+ const elapsedSec = (elapsed / 1e3).toFixed(1);
185
+ const conf = convergence.confidence.toFixed(0);
186
+ process.stderr.write(`\r◊ ${benchmark.name}: ${conf}% confident (${elapsedSec}s) `);
187
+ lastLog = elapsed;
188
+ }
189
+ if (shouldStop(convergence, targetConfidence, elapsed, minTime)) break;
190
+ const opts = {
191
+ ...runnerOptions,
192
+ maxTime: continueBatch,
193
+ maxIterations: continueIterations,
194
+ skipWarmup: true
195
+ };
196
+ appendSamples((await baseRunner.runBench(benchmark, opts, params))[0], allSamples);
197
+ }
198
+ process.stderr.write("\r" + " ".repeat(60) + "\r");
199
+ }
200
+ /** Append samples one-by-one to avoid stack overflow from spread on large arrays */
201
+ function appendSamples(result, samples) {
202
+ if (!result.samples?.length) return;
203
+ for (const sample of result.samples) samples.push(sample);
204
+ }
205
+ /** @return true if convergence reached or timeout */
206
+ function shouldStop(convergence, targetConfidence, elapsedTime, minTime) {
207
+ if (convergence.converged && convergence.confidence >= targetConfidence) return true;
208
+ const threshold = Math.max(targetConfidence, fallbackThreshold);
209
+ return elapsedTime >= minTime && convergence.confidence >= threshold;
210
+ }
211
+ /** @return measured results with convergence metrics */
212
+ function buildResults(samplesMs, startTime, convergence, name, warmupSamples) {
213
+ const totalTime = (performance.now() - startTime) / 1e3;
214
+ return [{
215
+ name,
216
+ samples: samplesMs,
217
+ warmupSamples,
218
+ time: computeTimeStats(samplesMs.map((s) => s * msToNs)),
219
+ totalTime,
220
+ convergence
221
+ }];
222
+ }
223
+ /** @return time percentiles and statistics in ms */
224
+ function computeTimeStats(samplesNs) {
225
+ const samplesMs = samplesNs.map((s) => s / msToNs);
226
+ const { min, max, sum } = getMinMaxSum(samplesNs);
227
+ const percentiles = getPercentiles(samplesNs);
228
+ const robust = getRobustMetrics(samplesMs);
229
+ return {
230
+ min: min / msToNs,
231
+ max: max / msToNs,
232
+ avg: sum / samplesNs.length / msToNs,
233
+ ...percentiles,
234
+ ...robust
235
+ };
236
+ }
237
+ /** @return min, max, sum of samples */
238
+ function getMinMaxSum(samples) {
239
+ return {
240
+ min: samples.reduce((a, b) => Math.min(a, b), Number.POSITIVE_INFINITY),
241
+ max: samples.reduce((a, b) => Math.max(a, b), Number.NEGATIVE_INFINITY),
242
+ sum: samples.reduce((a, b) => a + b, 0)
243
+ };
244
+ }
245
+ /** @return percentiles in ms */
246
+ function getPercentiles(samples) {
247
+ return {
248
+ p25: percentile$1(samples, .25) / msToNs,
249
+ p50: percentile$1(samples, .5) / msToNs,
250
+ p75: percentile$1(samples, .75) / msToNs,
251
+ p95: percentile$1(samples, .95) / msToNs,
252
+ p99: percentile$1(samples, .99) / msToNs,
253
+ p999: percentile$1(samples, .999) / msToNs
254
+ };
255
+ }
256
+ /** @return robust variability metrics */
257
+ function getRobustMetrics(samplesMs) {
258
+ const impact = getOutlierImpact(samplesMs);
259
+ return {
260
+ cv: coefficientOfVariation(samplesMs),
261
+ mad: medianAbsoluteDeviation(samplesMs),
262
+ outlierRate: impact.ratio
263
+ };
264
+ }
265
+ /** @return outlier impact as proportion of total time */
266
+ function getOutlierImpact(samples) {
267
+ if (samples.length === 0) return {
268
+ ratio: 0,
269
+ count: 0
270
+ };
271
+ const median = percentile$1(samples, .5);
272
+ const threshold = median + 1.5 * (percentile$1(samples, .75) - median);
273
+ let excessTime = 0;
274
+ let count = 0;
275
+ for (const sample of samples) if (sample > threshold) {
276
+ excessTime += sample - median;
277
+ count++;
278
+ }
279
+ const totalTime = samples.reduce((a, b) => a + b, 0);
280
+ return {
281
+ ratio: totalTime > 0 ? excessTime / totalTime : 0,
282
+ count
283
+ };
284
+ }
285
+ /** @return convergence based on window stability */
286
+ function checkConvergence(samples) {
287
+ const windowSize = getWindowSize(samples);
288
+ const minSamples = windowSize * 2;
289
+ if (samples.length < minSamples) return buildProgressResult(samples.length, minSamples);
290
+ return buildConvergence(getStability(samples, windowSize));
291
+ }
292
+ /** @return progress when samples insufficient */
293
+ function buildProgressResult(currentSamples, minSamples) {
294
+ return {
295
+ converged: false,
296
+ confidence: currentSamples / minSamples * 100,
297
+ reason: `Collecting samples: ${currentSamples}/${minSamples}`
298
+ };
299
+ }
300
+ /** @return stability metrics between windows */
301
+ function getStability(samples, windowSize) {
302
+ const recent = samples.slice(-windowSize);
303
+ const previous = samples.slice(-windowSize * 2, -windowSize);
304
+ const recentMs = recent.map((s) => s / msToNs);
305
+ const previousMs = previous.map((s) => s / msToNs);
306
+ const medianRecent = percentile$1(recentMs, .5);
307
+ const medianPrevious = percentile$1(previousMs, .5);
308
+ const medianDrift = Math.abs(medianRecent - medianPrevious) / medianPrevious;
309
+ const impactRecent = getOutlierImpact(recentMs);
310
+ const impactPrevious = getOutlierImpact(previousMs);
311
+ const impactDrift = Math.abs(impactRecent.ratio - impactPrevious.ratio);
312
+ return {
313
+ medianDrift,
314
+ impactDrift,
315
+ medianStable: medianDrift < stability,
316
+ impactStable: impactDrift < stability
317
+ };
318
+ }
319
+ /** @return convergence from stability metrics */
320
+ function buildConvergence(metrics) {
321
+ const { medianDrift, impactDrift, medianStable, impactStable } = metrics;
322
+ if (medianStable && impactStable) return {
323
+ converged: true,
324
+ confidence: 100,
325
+ reason: "Stable performance pattern"
326
+ };
327
+ const confidence = Math.min(100, (1 - medianDrift / stability) * 50 + (1 - impactDrift / stability) * 50);
328
+ const reason = medianDrift > impactDrift ? `Median drifting: ${(medianDrift * 100).toFixed(1)}%` : `Outlier impact changing: ${(impactDrift * 100).toFixed(1)}%`;
329
+ return {
330
+ converged: false,
331
+ confidence: Math.max(0, confidence),
332
+ reason
333
+ };
334
+ }
335
+ /** @return window size scaled to execution time */
336
+ function getWindowSize(samples) {
337
+ if (samples.length < 20) return windowSize;
338
+ const recentMedian = percentile$1(samples.slice(-20).map((s) => s / msToNs), .5);
339
+ if (recentMedian < .01) return 200;
340
+ if (recentMedian < .1) return 100;
341
+ if (recentMedian < 1) return 50;
342
+ if (recentMedian < 10) return 30;
343
+ return 20;
344
+ }
345
+
346
+ //#endregion
347
+ //#region src/runners/BenchRunner.ts
348
+ /** Execute benchmark with optional parameters */
349
+ function executeBenchmark(benchmark, params) {
350
+ benchmark.fn(params);
351
+ }
352
+
353
+ //#endregion
354
+ //#region src/runners/BasicRunner.ts
355
+ /**
356
+ * Wait time after gc() for V8 to stabilize (ms).
357
+ *
358
+ * V8 has 4 compilation tiers: Ignition (interpreter) -> Sparkplug (baseline) ->
359
+ * Maglev (mid-tier optimizer) -> TurboFan (full optimizer). Tiering thresholds:
360
+ * - Ignition -> Sparkplug: 8 invocations
361
+ * - Sparkplug -> Maglev: 500 invocations
362
+ * - Maglev -> TurboFan: 6000 invocations
363
+ *
364
+ * Optimization compilation happens on background threads and requires idle time
365
+ * on the main thread to complete. Without sufficient warmup + settle time,
366
+ * benchmarks exhibit bimodal timing: slow Sparkplug samples (~30% slower) mixed
367
+ * with fast optimized samples.
368
+ *
369
+ * The warmup iterations trigger the optimization decision, then gcSettleTime
370
+ * provides idle time for background compilation to finish before measurement.
371
+ *
372
+ * @see https://v8.dev/blog/sparkplug
373
+ * @see https://v8.dev/blog/maglev
374
+ * @see https://v8.dev/blog/background-compilation
375
+ */
376
+ const gcSettleTime = 1e3;
377
+ /** @return runner with time and iteration limits */
378
+ var BasicRunner = class {
379
+ async runBench(benchmark, options, params) {
380
+ const collected = await collectSamples({
381
+ benchmark,
382
+ params,
383
+ ...defaultCollectOptions,
384
+ ...options
385
+ });
386
+ return [buildMeasuredResults(benchmark.name, collected)];
387
+ }
388
+ };
389
+ const defaultCollectOptions = {
390
+ maxTime: 5e3,
391
+ maxIterations: 1e6,
392
+ warmup: 0,
393
+ traceOpt: false,
394
+ noSettle: false
395
+ };
396
+ function buildMeasuredResults(name, c) {
397
+ const time = computeStats(c.samples);
398
+ const convergence = checkConvergence(c.samples.map((s) => s * msToNs));
399
+ return {
400
+ name,
401
+ samples: c.samples,
402
+ warmupSamples: c.warmupSamples,
403
+ heapSamples: c.heapSamples,
404
+ timestamps: c.timestamps,
405
+ time,
406
+ heapSize: {
407
+ avg: c.heapGrowth,
408
+ min: c.heapGrowth,
409
+ max: c.heapGrowth
410
+ },
411
+ convergence,
412
+ optStatus: c.optStatus,
413
+ optSamples: c.optSamples,
414
+ pausePoints: c.pausePoints
415
+ };
416
+ }
417
+ /** @return timing samples and amortized allocation from benchmark execution */
418
+ async function collectSamples(p) {
419
+ if (!p.maxIterations && !p.maxTime) throw new Error(`At least one of maxIterations or maxTime must be set`);
420
+ const warmupSamples = p.skipWarmup ? [] : await runWarmup(p);
421
+ const heapBefore = process.memoryUsage().heapUsed;
422
+ const { samples, heapSamples, timestamps, optStatuses, pausePoints } = await runSampleLoop(p);
423
+ const heapGrowth = Math.max(0, process.memoryUsage().heapUsed - heapBefore) / 1024 / samples.length;
424
+ if (samples.length === 0) throw new Error(`No samples collected for benchmark: ${p.benchmark.name}`);
425
+ return {
426
+ samples,
427
+ warmupSamples,
428
+ heapGrowth,
429
+ heapSamples,
430
+ timestamps,
431
+ optStatus: p.traceOpt ? analyzeOptStatus(samples, optStatuses) : void 0,
432
+ optSamples: p.traceOpt && optStatuses.length > 0 ? optStatuses : void 0,
433
+ pausePoints
434
+ };
435
+ }
436
+ /** Run warmup iterations with gc + settle time for V8 optimization */
437
+ async function runWarmup(p) {
438
+ const gc = gcFunction();
439
+ const samples = new Array(p.warmup);
440
+ for (let i = 0; i < p.warmup; i++) {
441
+ const start = performance.now();
442
+ executeBenchmark(p.benchmark, p.params);
443
+ samples[i] = performance.now() - start;
444
+ }
445
+ gc();
446
+ if (!p.noSettle) {
447
+ await new Promise((r) => setTimeout(r, gcSettleTime));
448
+ gc();
449
+ }
450
+ return samples;
451
+ }
452
+ /** Estimate sample count for pre-allocation */
453
+ function estimateSampleCount(maxTime, maxIterations) {
454
+ return maxIterations || Math.ceil(maxTime / .1);
455
+ }
456
+ /** Pre-allocate arrays to reduce GC pressure during measurement */
457
+ function createSampleArrays(n, trackHeap, trackOpt) {
458
+ const arr = (track) => track ? new Array(n) : [];
459
+ return {
460
+ samples: new Array(n),
461
+ timestamps: new Array(n),
462
+ heapSamples: arr(trackHeap),
463
+ optStatuses: arr(trackOpt),
464
+ pausePoints: []
465
+ };
466
+ }
467
+ /** Trim arrays to actual sample count */
468
+ function trimArrays(a, count, trackHeap, trackOpt) {
469
+ a.samples.length = a.timestamps.length = count;
470
+ if (trackHeap) a.heapSamples.length = count;
471
+ if (trackOpt) a.optStatuses.length = count;
472
+ }
473
+ /** Collect timing samples with periodic pauses for V8 optimization */
474
+ async function runSampleLoop(p) {
475
+ const { maxTime, maxIterations, pauseFirst, pauseInterval = 0, pauseDuration = 100 } = p;
476
+ const trackHeap = true;
477
+ const getOptStatus = p.traceOpt ? createOptStatusGetter() : void 0;
478
+ const a = createSampleArrays(estimateSampleCount(maxTime, maxIterations), trackHeap, !!getOptStatus);
479
+ let count = 0;
480
+ let elapsed = 0;
481
+ let totalPauseTime = 0;
482
+ const loopStart = performance.now();
483
+ while ((!maxIterations || count < maxIterations) && (!maxTime || elapsed < maxTime)) {
484
+ const start = performance.now();
485
+ executeBenchmark(p.benchmark, p.params);
486
+ const end = performance.now();
487
+ a.samples[count] = end - start;
488
+ a.timestamps[count] = Number(process.hrtime.bigint() / 1000n);
489
+ a.heapSamples[count] = getHeapStatistics().used_heap_size;
490
+ if (getOptStatus) a.optStatuses[count] = getOptStatus(p.benchmark.fn);
491
+ count++;
492
+ if (shouldPause(count, pauseFirst, pauseInterval)) {
493
+ a.pausePoints.push({
494
+ sampleIndex: count - 1,
495
+ durationMs: pauseDuration
496
+ });
497
+ const pauseStart = performance.now();
498
+ await new Promise((r) => setTimeout(r, pauseDuration));
499
+ totalPauseTime += performance.now() - pauseStart;
500
+ }
501
+ elapsed = performance.now() - loopStart - totalPauseTime;
502
+ }
503
+ trimArrays(a, count, trackHeap, !!getOptStatus);
504
+ return {
505
+ samples: a.samples,
506
+ heapSamples: a.heapSamples,
507
+ timestamps: a.timestamps,
508
+ optStatuses: a.optStatuses,
509
+ pausePoints: a.pausePoints
510
+ };
511
+ }
512
+ /** Check if we should pause at this iteration for V8 optimization */
513
+ function shouldPause(iter, first, interval) {
514
+ if (first !== void 0 && iter === first) return true;
515
+ if (interval <= 0) return false;
516
+ if (first === void 0) return iter % interval === 0;
517
+ return (iter - first) % interval === 0;
518
+ }
519
+ /** @return percentiles and basic statistics */
520
+ function computeStats(samples) {
521
+ const sorted = [...samples].sort((a, b) => a - b);
522
+ const avg = samples.reduce((sum, s) => sum + s, 0) / samples.length;
523
+ return {
524
+ min: sorted[0],
525
+ max: sorted[sorted.length - 1],
526
+ avg,
527
+ p50: percentile(sorted, .5),
528
+ p75: percentile(sorted, .75),
529
+ p99: percentile(sorted, .99),
530
+ p999: percentile(sorted, .999)
531
+ };
532
+ }
533
+ /** @return percentile value with linear interpolation */
534
+ function percentile(sortedArray, p) {
535
+ const index = (sortedArray.length - 1) * p;
536
+ const lower = Math.floor(index);
537
+ const upper = Math.ceil(index);
538
+ const weight = index % 1;
539
+ if (upper >= sortedArray.length) return sortedArray[sortedArray.length - 1];
540
+ return sortedArray[lower] * (1 - weight) + sortedArray[upper] * weight;
541
+ }
542
+ /** @return runtime gc() function, or no-op if unavailable */
543
+ function gcFunction() {
544
+ const gc = globalThis.gc || globalThis.__gc;
545
+ if (gc) return gc;
546
+ console.warn("gc() not available, run node/bun with --expose-gc");
547
+ return () => {};
548
+ }
549
+ /** @return function to get V8 optimization status (requires --allow-natives-syntax) */
550
+ function createOptStatusGetter() {
551
+ try {
552
+ const getter = new Function("f", "return %GetOptimizationStatus(f)");
553
+ getter(() => {});
554
+ return getter;
555
+ } catch {
556
+ return;
557
+ }
558
+ }
559
+ /**
560
+ * V8 optimization status bit meanings:
561
+ * Bit 0 (1): is_function
562
+ * Bit 4 (16): is_optimized (TurboFan)
563
+ * Bit 5 (32): is_optimized (Maglev)
564
+ * Bit 7 (128): is_baseline (Sparkplug)
565
+ * Bit 3 (8): maybe_deoptimized
566
+ */
567
+ const statusNames = {
568
+ 1: "interpreted",
569
+ 129: "sparkplug",
570
+ 17: "turbofan",
571
+ 33: "maglev",
572
+ 49: "turbofan+maglev",
573
+ 32769: "optimized"
574
+ };
575
+ /** @return analysis of V8 optimization status per sample */
576
+ function analyzeOptStatus(samples, statuses) {
577
+ if (statuses.length === 0 || statuses[0] === void 0) return void 0;
578
+ const byStatusCode = /* @__PURE__ */ new Map();
579
+ let deoptCount = 0;
580
+ for (let i = 0; i < samples.length; i++) {
581
+ const status = statuses[i];
582
+ if (status === void 0) continue;
583
+ if (status & 8) deoptCount++;
584
+ if (!byStatusCode.has(status)) byStatusCode.set(status, []);
585
+ byStatusCode.get(status).push(samples[i]);
586
+ }
587
+ const byTier = {};
588
+ for (const [status, times] of byStatusCode) {
589
+ const name = statusNames[status] || `status=${status}`;
590
+ const sorted = [...times].sort((a, b) => a - b);
591
+ const median = sorted[Math.floor(sorted.length / 2)];
592
+ byTier[name] = {
593
+ count: times.length,
594
+ medianMs: median
595
+ };
596
+ }
597
+ return {
598
+ byTier,
599
+ deoptCount
600
+ };
601
+ }
602
+
603
+ //#endregion
604
+ //#region src/runners/CreateRunner.ts
605
+ /** @return benchmark runner */
606
+ async function createRunner(_runnerName) {
607
+ return new BasicRunner();
608
+ }
609
+
610
+ //#endregion
611
+ //#region src/runners/GcStats.ts
612
+ /** Parse a single --trace-gc-nvp stderr line */
613
+ function parseGcLine(line) {
614
+ if (!line.includes("pause=")) return void 0;
615
+ const fields = parseNvpFields(line);
616
+ if (!fields.gc) return void 0;
617
+ const int = (k) => Number.parseInt(fields[k] || "0", 10);
618
+ const type = parseGcType(fields.gc);
619
+ const pauseMs = Number.parseFloat(fields.pause || "0");
620
+ const allocated = int("allocated");
621
+ const promoted = int("promoted");
622
+ const survived = int("new_space_survived") || int("survived");
623
+ const startSize = int("start_object_size");
624
+ const endSize = int("end_object_size");
625
+ const collected = startSize > endSize ? startSize - endSize : 0;
626
+ if (Number.isNaN(pauseMs)) return void 0;
627
+ return {
628
+ type,
629
+ pauseMs,
630
+ allocated,
631
+ collected,
632
+ promoted,
633
+ survived
634
+ };
635
+ }
636
+ /** Parse name=value pairs from trace-gc-nvp line */
637
+ function parseNvpFields(line) {
638
+ const fields = {};
639
+ const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
640
+ for (const [, key, value] of matches) fields[key] = value;
641
+ return fields;
642
+ }
643
+ /** Map V8 gc type codes to our types */
644
+ function parseGcType(gcField) {
645
+ if (gcField === "s" || gcField === "scavenge") return "scavenge";
646
+ if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact") return "mark-compact";
647
+ if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms") return "minor-ms";
648
+ return "unknown";
649
+ }
650
+ /** Aggregate GC events into summary stats */
651
+ function aggregateGcStats(events) {
652
+ let scavenges = 0;
653
+ let markCompacts = 0;
654
+ let gcPauseTime = 0;
655
+ let totalCollected = 0;
656
+ let hasNodeFields = false;
657
+ let totalAllocated = 0;
658
+ let totalPromoted = 0;
659
+ let totalSurvived = 0;
660
+ for (const e of events) {
661
+ if (e.type === "scavenge" || e.type === "minor-ms") scavenges++;
662
+ else if (e.type === "mark-compact") markCompacts++;
663
+ gcPauseTime += e.pauseMs;
664
+ totalCollected += e.collected;
665
+ if (e.allocated != null) {
666
+ hasNodeFields = true;
667
+ totalAllocated += e.allocated;
668
+ totalPromoted += e.promoted ?? 0;
669
+ totalSurvived += e.survived ?? 0;
670
+ }
671
+ }
672
+ return {
673
+ scavenges,
674
+ markCompacts,
675
+ totalCollected,
676
+ gcPauseTime,
677
+ ...hasNodeFields && {
678
+ totalAllocated,
679
+ totalPromoted,
680
+ totalSurvived
681
+ }
682
+ };
683
+ }
684
+
685
+ //#endregion
686
+ //#region src/runners/TimingUtils.ts
687
+ const debugWorkerTiming = false;
688
+ /** Get current time or 0 if debugging disabled */
689
+ function getPerfNow() {
690
+ return 0;
691
+ }
692
+ /** Calculate elapsed milliseconds between marks */
693
+ function getElapsed(startMark, endMark) {
694
+ return 0;
695
+ }
696
+
697
+ //#endregion
698
+ //#region src/runners/RunnerOrchestrator.ts
699
+ const logTiming = debugWorkerTiming ? (message) => console.log(`[RunnerOrchestrator] ${message}`) : () => {};
700
+ /** Execute benchmarks directly or in worker process */
701
+ async function runBenchmark({ spec, runner, options, useWorker = false, params }) {
702
+ if (!useWorker) {
703
+ const resolvedSpec = spec.modulePath ? await resolveModuleSpec(spec, params) : {
704
+ spec,
705
+ params
706
+ };
707
+ const base = await createRunner(runner);
708
+ return (options.adaptive ? createAdaptiveWrapper(base, options) : base).runBench(resolvedSpec.spec, options, resolvedSpec.params);
709
+ }
710
+ return runInWorker({
711
+ spec,
712
+ runner,
713
+ options,
714
+ params
715
+ });
716
+ }
717
+ /** Resolve modulePath/exportName to a real function for non-worker mode */
718
+ async function resolveModuleSpec(spec, params) {
719
+ const module = await import(spec.modulePath);
720
+ const fn = spec.exportName ? module[spec.exportName] : module.default || module;
721
+ if (typeof fn !== "function") {
722
+ const name = spec.exportName || "default";
723
+ throw new Error(`Export '${name}' from ${spec.modulePath} is not a function`);
724
+ }
725
+ let resolvedParams = params;
726
+ if (spec.setupExportName) {
727
+ const setupFn = module[spec.setupExportName];
728
+ if (typeof setupFn !== "function") {
729
+ const msg = `Setup export '${spec.setupExportName}' from ${spec.modulePath} is not a function`;
730
+ throw new Error(msg);
731
+ }
732
+ resolvedParams = await setupFn(params);
733
+ }
734
+ return {
735
+ spec: {
736
+ ...spec,
737
+ fn
738
+ },
739
+ params: resolvedParams
740
+ };
741
+ }
742
+ /** Run benchmark in isolated worker process */
743
+ async function runInWorker(workerParams) {
744
+ const { spec, runner, options, params } = workerParams;
745
+ const msg = createRunMessage(spec, runner, options, params);
746
+ return runWorkerWithMessage(spec.name, options, msg);
747
+ }
748
+ /** Create worker process with timing logs */
749
+ function createWorkerWithTiming(gcStats) {
750
+ const workerStart = getPerfNow();
751
+ const gcEvents = [];
752
+ const worker = createWorkerProcess(gcStats);
753
+ const createTime = getPerfNow();
754
+ if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
755
+ logTiming(`Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`);
756
+ return {
757
+ worker,
758
+ createTime,
759
+ gcEvents
760
+ };
761
+ }
762
+ /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
763
+ function setupGcCapture(worker, gcEvents) {
764
+ let buffer = "";
765
+ worker.stdout.on("data", (data) => {
766
+ buffer += data.toString();
767
+ const lines = buffer.split("\n");
768
+ buffer = lines.pop() || "";
769
+ for (const line of lines) {
770
+ const event = parseGcLine(line);
771
+ if (event) gcEvents.push(event);
772
+ else if (line.trim()) process.stdout.write(line + "\n");
773
+ }
774
+ });
775
+ }
776
+ /** Spawn worker, wire handlers, send message, return results */
777
+ function runWorkerWithMessage(name, options, message) {
778
+ const startTime = getPerfNow();
779
+ const collectGcStats = options.gcStats ?? false;
780
+ logTiming(`Starting worker for ${name}`);
781
+ return new Promise((resolve, reject) => {
782
+ const { worker, createTime, gcEvents } = createWorkerWithTiming(collectGcStats);
783
+ setupWorkerHandlers(worker, name, createWorkerHandlers(name, startTime, gcEvents, resolve, reject));
784
+ sendWorkerMessage(worker, message, createTime);
785
+ });
786
+ }
787
+ /** Send message to worker with timing log */
788
+ function sendWorkerMessage(worker, message, createTime) {
789
+ const messageTime = getPerfNow();
790
+ worker.send(message);
791
+ logTiming(`Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`);
792
+ }
793
+ /** Setup worker event handlers with cleanup */
794
+ function setupWorkerHandlers(worker, specName, handlers) {
795
+ const { resolve, reject } = handlers;
796
+ const cleanup = createCleanup(worker, specName, reject);
797
+ worker.on("message", createMessageHandler(specName, cleanup, resolve, reject));
798
+ worker.on("error", createErrorHandler(specName, cleanup, reject));
799
+ worker.on("exit", createExitHandler(specName, cleanup, reject));
800
+ }
801
+ /** Handle worker messages (results or errors) */
802
+ function createMessageHandler(specName, cleanup, resolve, reject) {
803
+ return (msg) => {
804
+ cleanup();
805
+ if (msg.type === "result") resolve(msg.results, msg.heapProfile);
806
+ else if (msg.type === "error") {
807
+ const error = /* @__PURE__ */ new Error(`Benchmark "${specName}" failed: ${msg.error}`);
808
+ if (msg.stack) error.stack = msg.stack;
809
+ reject(error);
810
+ }
811
+ };
812
+ }
813
+ /** Handle worker process errors */
814
+ function createErrorHandler(specName, cleanup, reject) {
815
+ return (error) => {
816
+ cleanup();
817
+ reject(/* @__PURE__ */ new Error(`Worker process failed for benchmark "${specName}": ${error.message}`));
818
+ };
819
+ }
820
+ /** Handle worker process exit */
821
+ function createExitHandler(specName, cleanup, reject) {
822
+ return (code, _signal) => {
823
+ if (code !== 0 && code !== null) {
824
+ cleanup();
825
+ const msg = `Worker exited with code ${code} for benchmark "${specName}"`;
826
+ reject(new Error(msg));
827
+ }
828
+ };
829
+ }
830
+ /** Create cleanup for timeout and termination */
831
+ function createCleanup(worker, specName, reject) {
832
+ const timeoutId = setTimeout(() => {
833
+ cleanup();
834
+ reject(/* @__PURE__ */ new Error(`Benchmark "${specName}" timed out after 60 seconds`));
835
+ }, 6e4);
836
+ const cleanup = () => {
837
+ clearTimeout(timeoutId);
838
+ if (!worker.killed) worker.kill("SIGTERM");
839
+ };
840
+ return cleanup;
841
+ }
842
+ /** Create worker process with configuration */
843
+ function createWorkerProcess(gcStats) {
844
+ const workerPath = path.join(import.meta.dirname, "WorkerScript.ts");
845
+ const execArgv = [
846
+ "--expose-gc",
847
+ "--allow-natives-syntax",
848
+ "--experimental-strip-types",
849
+ "--no-warnings=ExperimentalWarning"
850
+ ];
851
+ if (gcStats) execArgv.push("--trace-gc-nvp");
852
+ return fork(workerPath, [], {
853
+ execArgv,
854
+ silent: gcStats,
855
+ env: {
856
+ ...process.env,
857
+ NODE_OPTIONS: ""
858
+ }
859
+ });
860
+ }
861
+ /** @return handlers that attach GC stats and heap profile to results */
862
+ function createWorkerHandlers(specName, startTime, gcEvents, resolve, reject) {
863
+ return {
864
+ resolve: (results, heapProfile) => {
865
+ logTiming(`Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`);
866
+ if (gcEvents?.length) {
867
+ const gcStats = aggregateGcStats(gcEvents);
868
+ for (const r of results) r.gcStats = gcStats;
869
+ }
870
+ if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
871
+ resolve(results);
872
+ },
873
+ reject
874
+ };
875
+ }
876
+ /** Create message for worker execution */
877
+ function createRunMessage(spec, runnerName, options, params) {
878
+ const { fn, ...rest } = spec;
879
+ const message = {
880
+ type: "run",
881
+ spec: rest,
882
+ runnerName,
883
+ options,
884
+ params
885
+ };
886
+ if (spec.modulePath) {
887
+ message.modulePath = spec.modulePath;
888
+ message.exportName = spec.exportName;
889
+ if (spec.setupExportName) message.setupExportName = spec.setupExportName;
890
+ } else message.fnCode = fn.toString();
891
+ return message;
892
+ }
893
+ /** Run a matrix variant benchmark in isolated worker process */
894
+ async function runMatrixVariant(params) {
895
+ const { variantDir, variantId, caseId, caseData, casesModule, runner, options } = params;
896
+ const name = `${variantId}/${caseId}`;
897
+ return runWorkerWithMessage(name, options, {
898
+ type: "run",
899
+ spec: {
900
+ name,
901
+ fn: () => {}
902
+ },
903
+ runnerName: runner,
904
+ options,
905
+ variantDir,
906
+ variantId,
907
+ caseId,
908
+ caseData,
909
+ casesModule
910
+ });
911
+ }
912
+
913
+ //#endregion
914
+ //#region src/BenchMatrix.ts
915
+ /** @return true if variant is a StatefulVariant (has setup + run) */
916
+ function isStatefulVariant(v) {
917
+ return typeof v === "object" && "setup" in v && "run" in v;
918
+ }
919
+ /** Run a BenchMatrix with inline variants or variantDir */
920
+ async function runMatrix(matrix, options = {}) {
921
+ validateBaseline(matrix);
922
+ const effectiveOptions = {
923
+ ...matrix.defaults,
924
+ ...options
925
+ };
926
+ if (matrix.variantDir) return runMatrixWithDir(matrix, effectiveOptions);
927
+ if (matrix.variants) return runMatrixInline(matrix, effectiveOptions);
928
+ throw new Error("BenchMatrix requires either 'variants' or 'variantDir'");
929
+ }
930
+ /** @throws if both baselineDir and baselineVariant are set */
931
+ function validateBaseline(matrix) {
932
+ const msg = "BenchMatrix cannot have both 'baselineDir' and 'baselineVariant'";
933
+ if (matrix.baselineDir && matrix.baselineVariant) throw new Error(msg);
934
+ }
935
+ function buildRunnerOptions(options) {
936
+ return {
937
+ maxIterations: options.iterations,
938
+ maxTime: options.maxTime ?? 1e3,
939
+ warmup: options.warmup ?? 0,
940
+ collect: options.collect,
941
+ cpuCounters: options.cpuCounters,
942
+ traceOpt: options.traceOpt,
943
+ noSettle: options.noSettle,
944
+ pauseFirst: options.pauseFirst,
945
+ pauseInterval: options.pauseInterval,
946
+ pauseDuration: options.pauseDuration,
947
+ gcStats: options.gcStats,
948
+ heapSample: options.heapSample,
949
+ heapInterval: options.heapInterval,
950
+ heapDepth: options.heapDepth
951
+ };
952
+ }
953
+ /** Load cases module and resolve filtered case IDs */
954
+ async function resolveCases(matrix, options) {
955
+ const casesModule = matrix.casesModule ? await loadCasesModule(matrix.casesModule) : void 0;
956
+ const allCaseIds = casesModule?.cases ?? matrix.cases ?? ["default"];
957
+ return {
958
+ casesModule,
959
+ caseIds: options.filteredCases ?? allCaseIds
960
+ };
961
+ }
962
+ /** Run matrix with inline variants (non-worker mode) */
963
+ async function runMatrixInline(matrix, options) {
964
+ const msg = "BenchMatrix with inline 'variants' cannot use 'baselineDir'. Use 'variantDir' instead.";
965
+ if (matrix.baselineDir) throw new Error(msg);
966
+ const { casesModule, caseIds } = await resolveCases(matrix, options);
967
+ const runner = new BasicRunner();
968
+ const runnerOpts = buildRunnerOptions(options);
969
+ const variantEntries = options.filteredVariants ? Object.entries(matrix.variants).filter(([id]) => options.filteredVariants.includes(id)) : Object.entries(matrix.variants);
970
+ const variants = [];
971
+ for (const [variantId, variant] of variantEntries) {
972
+ const cases = [];
973
+ for (const caseId of caseIds) {
974
+ const loaded = await loadCaseData(casesModule, caseId);
975
+ const measured = await runVariant(variant, casesModule || matrix.cases ? loaded.data : void 0, variantId, runner, runnerOpts);
976
+ cases.push({
977
+ caseId,
978
+ measured,
979
+ metadata: loaded.metadata
980
+ });
981
+ }
982
+ variants.push({
983
+ id: variantId,
984
+ cases
985
+ });
986
+ }
987
+ if (matrix.baselineVariant) applyBaselineVariant(variants, matrix.baselineVariant);
988
+ return {
989
+ name: matrix.name,
990
+ variants
991
+ };
992
+ }
993
+ /** Run matrix with variantDir (worker mode for memory isolation) */
994
+ async function runMatrixWithDir(matrix, options) {
995
+ const allVariantIds = await discoverVariants(matrix.variantDir);
996
+ if (allVariantIds.length === 0) throw new Error(`No variants found in ${matrix.variantDir}`);
997
+ const variants = await runDirVariants(options.filteredVariants ?? allVariantIds, await createDirContext(matrix, options));
998
+ if (matrix.baselineVariant) applyBaselineVariant(variants, matrix.baselineVariant);
999
+ return {
1000
+ name: matrix.name,
1001
+ variants
1002
+ };
1003
+ }
1004
+ /** Create context for directory-based matrix execution */
1005
+ async function createDirContext(matrix, options) {
1006
+ const baselineIds = matrix.baselineDir ? await discoverVariants(matrix.baselineDir) : [];
1007
+ const { casesModule, caseIds } = await resolveCases(matrix, options);
1008
+ return {
1009
+ matrix,
1010
+ casesModule,
1011
+ baselineIds,
1012
+ caseIds,
1013
+ runnerOpts: buildRunnerOptions(options)
1014
+ };
1015
+ }
1016
+ /** Run all variants using worker processes */
1017
+ async function runDirVariants(variantIds, ctx) {
1018
+ const variants = [];
1019
+ for (const variantId of variantIds) {
1020
+ const cases = await runDirVariantCases(variantId, ctx);
1021
+ variants.push({
1022
+ id: variantId,
1023
+ cases
1024
+ });
1025
+ }
1026
+ return variants;
1027
+ }
1028
+ /** Run all cases for a single variant */
1029
+ async function runDirVariantCases(variantId, ctx) {
1030
+ const { matrix, casesModule, caseIds, runnerOpts } = ctx;
1031
+ const cases = [];
1032
+ for (const caseId of caseIds) {
1033
+ const caseData = !matrix.casesModule && matrix.cases ? caseId : void 0;
1034
+ const [measured] = await runMatrixVariant({
1035
+ variantDir: matrix.variantDir,
1036
+ variantId,
1037
+ caseId,
1038
+ caseData,
1039
+ casesModule: matrix.casesModule,
1040
+ runner: "basic",
1041
+ options: runnerOpts
1042
+ });
1043
+ const loaded = await loadCaseData(casesModule, caseId);
1044
+ const baseline = await runBaselineIfExists(variantId, caseId, caseData, ctx);
1045
+ const deltaPercent = baseline ? computeDeltaPercent(baseline, measured) : void 0;
1046
+ const metadata = loaded.metadata;
1047
+ cases.push({
1048
+ caseId,
1049
+ measured,
1050
+ metadata,
1051
+ baseline,
1052
+ deltaPercent
1053
+ });
1054
+ }
1055
+ return cases;
1056
+ }
1057
+ /** Run baseline variant if it exists in baselineDir */
1058
+ async function runBaselineIfExists(variantId, caseId, caseData, ctx) {
1059
+ const { matrix, baselineIds, runnerOpts } = ctx;
1060
+ if (!matrix.baselineDir || !baselineIds.includes(variantId)) return void 0;
1061
+ const [measured] = await runMatrixVariant({
1062
+ variantDir: matrix.baselineDir,
1063
+ variantId,
1064
+ caseId,
1065
+ caseData,
1066
+ casesModule: matrix.casesModule,
1067
+ runner: "basic",
1068
+ options: runnerOpts
1069
+ });
1070
+ return measured;
1071
+ }
1072
+ /** Compute delta percentage: (current - baseline) / baseline * 100 */
1073
+ function computeDeltaPercent(baseline, current) {
1074
+ const baseAvg = average(baseline.samples);
1075
+ if (baseAvg === 0) return 0;
1076
+ return (average(current.samples) - baseAvg) / baseAvg * 100;
1077
+ }
1078
+ /** Apply baselineVariant comparison - one variant is the reference for all others */
1079
+ function applyBaselineVariant(variants, baselineVariantId) {
1080
+ const baselineVariant = variants.find((v) => v.id === baselineVariantId);
1081
+ if (!baselineVariant) return;
1082
+ const baselineByCase = /* @__PURE__ */ new Map();
1083
+ for (const c of baselineVariant.cases) baselineByCase.set(c.caseId, c.measured);
1084
+ for (const variant of variants) {
1085
+ if (variant.id === baselineVariantId) continue;
1086
+ for (const caseResult of variant.cases) {
1087
+ const baseline = baselineByCase.get(caseResult.caseId);
1088
+ if (baseline) {
1089
+ caseResult.baseline = baseline;
1090
+ caseResult.deltaPercent = computeDeltaPercent(baseline, caseResult.measured);
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ /** Run a single variant with case data */
1096
+ async function runVariant(variant, caseData, name, runner, options) {
1097
+ if (isStatefulVariant(variant)) {
1098
+ const state = await variant.setup(caseData);
1099
+ const [result] = await runner.runBench({
1100
+ name,
1101
+ fn: () => variant.run(state)
1102
+ }, options);
1103
+ return result;
1104
+ }
1105
+ const [result] = await runner.runBench({
1106
+ name,
1107
+ fn: () => variant(caseData)
1108
+ }, options);
1109
+ return result;
1110
+ }
1111
+
1112
+ //#endregion
1113
+ //#region src/table-util/Formatters.ts
1114
+ const { red: red$1, green } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? {
1115
+ red: (str) => str,
1116
+ green: (str) => str
1117
+ } : pico;
1118
+ /** Format percentages with custom precision */
1119
+ function percentPrecision(precision) {
1120
+ return (x) => {
1121
+ if (typeof x !== "number") return null;
1122
+ return percent(x, precision);
1123
+ };
1124
+ }
1125
+ /** Format duration in milliseconds with appropriate units */
1126
+ function duration(ms) {
1127
+ if (typeof ms !== "number") return null;
1128
+ if (ms < .001) return `${(ms * 1e6).toFixed(0)}ns`;
1129
+ if (ms < 1) return `${(ms * 1e3).toFixed(1)}μs`;
1130
+ if (ms < 1e3) return `${ms.toFixed(2)}ms`;
1131
+ return `${(ms / 1e3).toFixed(2)}s`;
1132
+ }
1133
+ /** Format time in milliseconds, showing very small values with units */
1134
+ function timeMs(ms) {
1135
+ if (typeof ms !== "number") return null;
1136
+ if (ms < .001) return `${(ms * 1e6).toFixed(0)}ns`;
1137
+ if (ms < .01) return `${(ms * 1e3).toFixed(1)}μs`;
1138
+ if (ms >= 10) return ms.toFixed(0);
1139
+ return ms.toFixed(2);
1140
+ }
1141
+ /** Format integer with thousand separators */
1142
+ function integer(x) {
1143
+ if (typeof x !== "number") return null;
1144
+ return new Intl.NumberFormat("en-US").format(Math.round(x));
1145
+ }
1146
+ /** Format fraction as percentage (0.473 → 47.3%) */
1147
+ function percent(fraction, precision = 1) {
1148
+ if (typeof fraction !== "number") return null;
1149
+ return `${Math.abs(fraction * 100).toFixed(precision)}%`;
1150
+ }
1151
+ /** Format percentage difference between two values */
1152
+ function diffPercent(main, base) {
1153
+ if (typeof main !== "number" || typeof base !== "number") return " ";
1154
+ return coloredPercent(main - base, base);
1155
+ }
1156
+ /** Format fraction as colored +/- percentage */
1157
+ function coloredPercent(numerator, denominator, positiveIsGreen = true) {
1158
+ const fraction = numerator / denominator;
1159
+ if (Number.isNaN(fraction) || !Number.isFinite(fraction)) return " ";
1160
+ const positive = fraction >= 0;
1161
+ const percentStr = `${positive ? "+" : "-"}${percent(fraction)}`;
1162
+ return positive === positiveIsGreen ? green(percentStr) : red$1(percentStr);
1163
+ }
1164
+ /** Format bytes with appropriate units (B, KB, MB, GB) */
1165
+ function formatBytes(bytes) {
1166
+ if (typeof bytes !== "number") return null;
1167
+ if (bytes < 1024) return `${bytes.toFixed(0)}B`;
1168
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1169
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
1170
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
1171
+ }
1172
+ /** Format percentage difference with confidence interval */
1173
+ function formatDiffWithCI(value) {
1174
+ if (!isDifferenceCI(value)) return null;
1175
+ const { percent, ci, direction } = value;
1176
+ return colorByDirection(diffCIText(percent, ci), direction);
1177
+ }
1178
+ /** Format percentage difference with CI for throughput metrics (higher is better) */
1179
+ function formatDiffWithCIHigherIsBetter(value) {
1180
+ if (!isDifferenceCI(value)) return null;
1181
+ const { percent, ci, direction } = value;
1182
+ return colorByDirection(diffCIText(-percent, [-ci[1], -ci[0]]), direction);
1183
+ }
1184
+ /** @return formatted "pct [lo, hi]" text for a diff with CI */
1185
+ function diffCIText(pct, ci) {
1186
+ return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
1187
+ }
1188
+ /** @return text colored green for faster, red for slower */
1189
+ function colorByDirection(text, direction) {
1190
+ if (direction === "faster") return green(text);
1191
+ if (direction === "slower") return red$1(text);
1192
+ return text;
1193
+ }
1194
+ /** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
1195
+ function formatBound(v) {
1196
+ return `${v >= 0 ? "+" : ""}${v.toFixed(1)}%`;
1197
+ }
1198
+ /** @return true if value is a DifferenceCI object */
1199
+ function isDifferenceCI(x) {
1200
+ return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
1201
+ }
1202
+ /** @return truncated string with ellipsis if over maxLen */
1203
+ function truncate(str, maxLen = 30) {
1204
+ return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
1205
+ }
1206
+
1207
+ //#endregion
1208
+ //#region src/table-util/TableReport.ts
1209
+ const { bold } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? { bold: (str) => str } : pico;
1210
+ /** Build formatted table with column groups and baselines */
1211
+ function buildTable(columnGroups, resultGroups, nameKey = "name") {
1212
+ return createTable(columnGroups, flattenGroups(columnGroups, resultGroups, nameKey));
1213
+ }
1214
+ /** Convert columns and records to formatted table */
1215
+ function createTable(groups, records) {
1216
+ const dataRows = toRows(records, groups);
1217
+ const { headerRows, config } = setup(groups, dataRows);
1218
+ return table([...headerRows, ...dataRows], config);
1219
+ }
1220
+ /** Create header rows with group titles */
1221
+ function createGroupHeaders(groups, numColumns) {
1222
+ if (!groups.some((g) => g.groupTitle)) return [];
1223
+ return [groups.flatMap((g) => {
1224
+ return padWithBlanks(g.groupTitle ? [bold(g.groupTitle)] : [], g.columns.length);
1225
+ }), padWithBlanks([], numColumns)];
1226
+ }
1227
+ /** @return draw functions for horizontal/vertical table borders */
1228
+ function createLines(groups) {
1229
+ const { sectionBorders, headerBottom } = calcBorders(groups);
1230
+ function drawVerticalLine(index, size) {
1231
+ return index === 0 || index === size || sectionBorders.includes(index);
1232
+ }
1233
+ function drawHorizontalLine(index, size) {
1234
+ return index === 0 || index === size || index === headerBottom;
1235
+ }
1236
+ return {
1237
+ drawHorizontalLine,
1238
+ drawVerticalLine
1239
+ };
1240
+ }
1241
+ /** @return spanning cell configs for group title headers */
1242
+ function createSectionSpans(groups) {
1243
+ let col = 0;
1244
+ const alignment = "center";
1245
+ return groups.map((g) => {
1246
+ const colSpan = g.columns.length;
1247
+ const span = {
1248
+ row: 0,
1249
+ col,
1250
+ colSpan,
1251
+ alignment
1252
+ };
1253
+ col += colSpan;
1254
+ return span;
1255
+ });
1256
+ }
1257
+ /** @return bolded column title strings */
1258
+ function getTitles(groups) {
1259
+ return groups.flatMap((g) => g.columns.map((c) => bold(c.title || " ")));
1260
+ }
1261
+ /** @return array padded with blank strings to the given length */
1262
+ function padWithBlanks(arr, length) {
1263
+ if (arr.length >= length) return arr;
1264
+ return [...arr, ...Array(length - arr.length).fill(" ")];
1265
+ }
1266
+ /** Convert records to string arrays for table */
1267
+ function toRows(records, groups) {
1268
+ const allColumns = groups.flatMap((group) => group.columns);
1269
+ return records.map((record) => allColumns.map((col) => {
1270
+ const value = record[col.key];
1271
+ return col.formatter ? col.formatter(value) : value;
1272
+ })).map((row) => row.map((cell) => cell ?? " "));
1273
+ }
1274
+ /** Add comparison values for diff columns */
1275
+ function addComparisons(groups, mainRecord, baselineRecord) {
1276
+ const diffColumns = groups.flatMap((g) => g.columns).filter((col) => col.diffKey);
1277
+ const updatedMain = { ...mainRecord };
1278
+ for (const col of diffColumns) {
1279
+ const dcol = col;
1280
+ const diffKey = dcol.diffKey;
1281
+ const mainValue = mainRecord[diffKey];
1282
+ const baselineValue = baselineRecord[diffKey];
1283
+ const diffStr = (dcol.diffFormatter ?? diffPercent)(mainValue, baselineValue);
1284
+ updatedMain[col.key] = diffStr;
1285
+ }
1286
+ return updatedMain;
1287
+ }
1288
+ /** Flatten groups with spacing */
1289
+ function flattenGroups(columnGroups, resultGroups, nameKey) {
1290
+ return resultGroups.flatMap((group, i) => {
1291
+ const groupRecords = addBaseline(columnGroups, group, nameKey);
1292
+ return i === resultGroups.length - 1 ? groupRecords : [...groupRecords, {}];
1293
+ });
1294
+ }
1295
+ /** Process results with baseline comparisons */
1296
+ function addBaseline(columnGroups, group, nameKey) {
1297
+ const { results, baseline } = group;
1298
+ if (!baseline) return results;
1299
+ const diffResults = results.map((result) => addComparisons(columnGroups, result, baseline));
1300
+ const markedBaseline = {
1301
+ ...baseline,
1302
+ [nameKey]: `--> ${baseline[nameKey]}`
1303
+ };
1304
+ return [...diffResults, markedBaseline];
1305
+ }
1306
+ /** Calculate vertical lines between sections and header bottom position */
1307
+ function calcBorders(groups) {
1308
+ if (groups.length === 0) return {
1309
+ sectionBorders: [],
1310
+ headerBottom: 1
1311
+ };
1312
+ const sectionBorders = [];
1313
+ let border = 0;
1314
+ for (const g of groups) {
1315
+ border += g.columns.length;
1316
+ sectionBorders.push(border);
1317
+ }
1318
+ return {
1319
+ sectionBorders,
1320
+ headerBottom: 3
1321
+ };
1322
+ }
1323
+ /** Create headers and table configuration */
1324
+ function setup(groups, dataRows) {
1325
+ const titles = getTitles(groups);
1326
+ const numColumns = titles.length;
1327
+ return {
1328
+ headerRows: [...createGroupHeaders(groups, numColumns), titles],
1329
+ config: {
1330
+ spanningCells: createSectionSpans(groups),
1331
+ columns: calcColumnWidths(groups, titles, dataRows),
1332
+ ...createLines(groups)
1333
+ }
1334
+ };
1335
+ }
1336
+ /** Calculate column widths based on content, including group titles */
1337
+ function calcColumnWidths(groups, titles, dataRows) {
1338
+ const widths = [];
1339
+ for (let i = 0; i < titles.length; i++) {
1340
+ const titleW = cellWidth(titles[i]);
1341
+ const maxDataW = dataRows.reduce((max, row) => Math.max(max, cellWidth(row[i])), 0);
1342
+ widths.push(Math.max(titleW, maxDataW));
1343
+ }
1344
+ let colIndex = 0;
1345
+ for (const group of groups) {
1346
+ const groupW = cellWidth(group.groupTitle);
1347
+ if (groupW > 0) {
1348
+ const numCols = group.columns.length;
1349
+ const separatorWidth = (numCols - 1) * 3;
1350
+ const needed = groupW - widths.slice(colIndex, colIndex + numCols).reduce((a, b) => a + b, 0) - separatorWidth;
1351
+ if (needed > 0) widths[colIndex + numCols - 1] += needed;
1352
+ }
1353
+ colIndex += group.columns.length;
1354
+ }
1355
+ return Object.fromEntries(widths.map((w, i) => [i, {
1356
+ width: w,
1357
+ wrapWord: false
1358
+ }]));
1359
+ }
1360
+ const ansiEscapeRegex = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*m", "g");
1361
+ /** Get visible length of a cell value (strips ANSI escape codes) */
1362
+ function cellWidth(value) {
1363
+ if (value == null) return 0;
1364
+ return String(value).replace(ansiEscapeRegex, "").length;
1365
+ }
1366
+
1367
+ //#endregion
1368
+ //#region src/BenchmarkReport.ts
1369
+ /** @return formatted table report with optional baseline comparisons */
1370
+ function reportResults(groups, sections) {
1371
+ const results = groups.map((group) => resultGroupValues(group, sections));
1372
+ return buildTable(createColumnGroups(sections, results.some((g) => g.baseline)), results);
1373
+ }
1374
+ /** @return values for report group */
1375
+ function resultGroupValues(group, sections) {
1376
+ const { reports, baseline } = group;
1377
+ const baselineSamples = baseline?.measuredResults.samples;
1378
+ return {
1379
+ results: reports.map((report) => {
1380
+ const row = {
1381
+ name: truncate(report.name),
1382
+ ...extractReportValues(report, sections)
1383
+ };
1384
+ if (baselineSamples && report.measuredResults.samples) row.diffCI = bootstrapDifferenceCI(baselineSamples, report.measuredResults.samples);
1385
+ return row;
1386
+ }),
1387
+ baseline: baseline && valuesForReports([baseline], sections)[0]
1388
+ };
1389
+ }
1390
+ /** @return rows with stats from sections */
1391
+ function valuesForReports(reports, sections) {
1392
+ return reports.map((report) => ({
1393
+ name: truncate(report.name),
1394
+ ...extractReportValues(report, sections)
1395
+ }));
1396
+ }
1397
+ /** @return merged statistics from all sections */
1398
+ function extractReportValues(report, sections) {
1399
+ const { measuredResults, metadata } = report;
1400
+ const entries = sections.flatMap((s) => Object.entries(s.extract(measuredResults, metadata)));
1401
+ return Object.fromEntries(entries);
1402
+ }
1403
+ /** @return column groups with diff columns if baseline exists */
1404
+ function createColumnGroups(sections, hasBaseline) {
1405
+ const nameColumn = { columns: [{
1406
+ key: "name",
1407
+ title: "name"
1408
+ }] };
1409
+ const groups = sections.flatMap((section) => section.columns());
1410
+ return [nameColumn, ...hasBaseline ? injectDiffColumns(groups) : groups];
1411
+ }
1412
+ /** @return groups with single CI column after first comparable field */
1413
+ function injectDiffColumns(reportGroups) {
1414
+ let ciAdded = false;
1415
+ return reportGroups.map((group) => ({
1416
+ groupTitle: group.groupTitle,
1417
+ columns: group.columns.flatMap((col) => {
1418
+ if (col.comparable && !ciAdded) {
1419
+ ciAdded = true;
1420
+ return [col, {
1421
+ title: "Δ% CI",
1422
+ key: "diffCI",
1423
+ formatter: col.higherIsBetter ? formatDiffWithCIHigherIsBetter : formatDiffWithCI
1424
+ }];
1425
+ }
1426
+ return [col];
1427
+ })
1428
+ }));
1429
+ }
1430
+
1431
+ //#endregion
1432
+ //#region src/cli/CliArgs.ts
1433
+ const defaultTime = .642;
1434
+ const defaultAdaptiveMaxTime = 20;
1435
+ const defaultPauseInterval = 0;
1436
+ const defaultPauseDuration = 100;
1437
+ const cliOptions = {
1438
+ time: {
1439
+ type: "number",
1440
+ default: defaultTime,
1441
+ requiresArg: true,
1442
+ describe: "test duration in seconds"
1443
+ },
1444
+ cpu: {
1445
+ type: "boolean",
1446
+ default: false,
1447
+ describe: "CPU counter measurements (requires root)"
1448
+ },
1449
+ collect: {
1450
+ type: "boolean",
1451
+ default: false,
1452
+ describe: "force GC after each iteration"
1453
+ },
1454
+ "gc-stats": {
1455
+ type: "boolean",
1456
+ default: false,
1457
+ describe: "collect GC statistics (Node: --trace-gc-nvp, browser: CDP tracing)"
1458
+ },
1459
+ profile: {
1460
+ type: "boolean",
1461
+ default: false,
1462
+ describe: "run once for profiling"
1463
+ },
1464
+ filter: {
1465
+ type: "string",
1466
+ requiresArg: true,
1467
+ describe: "filter benchmarks by regex or substring"
1468
+ },
1469
+ all: {
1470
+ type: "boolean",
1471
+ default: false,
1472
+ describe: "run all cases (ignore defaultCases)"
1473
+ },
1474
+ worker: {
1475
+ type: "boolean",
1476
+ default: true,
1477
+ describe: "run in worker process for isolation (default: true)"
1478
+ },
1479
+ adaptive: {
1480
+ type: "boolean",
1481
+ default: false,
1482
+ describe: "use adaptive sampling mode"
1483
+ },
1484
+ "min-time": {
1485
+ type: "number",
1486
+ default: 1,
1487
+ describe: "minimum time in seconds before adaptive convergence can stop"
1488
+ },
1489
+ convergence: {
1490
+ type: "number",
1491
+ default: 95,
1492
+ describe: "confidence threshold (0-100)"
1493
+ },
1494
+ warmup: {
1495
+ type: "number",
1496
+ default: 0,
1497
+ describe: "warmup iterations before measurement"
1498
+ },
1499
+ html: {
1500
+ type: "boolean",
1501
+ default: false,
1502
+ describe: "generate HTML report and open in browser"
1503
+ },
1504
+ "export-html": {
1505
+ type: "string",
1506
+ requiresArg: true,
1507
+ describe: "export HTML report to specified file"
1508
+ },
1509
+ json: {
1510
+ type: "string",
1511
+ requiresArg: true,
1512
+ describe: "export benchmark data to JSON file"
1513
+ },
1514
+ perfetto: {
1515
+ type: "string",
1516
+ requiresArg: true,
1517
+ describe: "export Perfetto trace file (view at ui.perfetto.dev)"
1518
+ },
1519
+ "trace-opt": {
1520
+ type: "boolean",
1521
+ default: false,
1522
+ describe: "trace V8 optimization tiers (requires --allow-natives-syntax)"
1523
+ },
1524
+ "skip-settle": {
1525
+ type: "boolean",
1526
+ default: false,
1527
+ describe: "skip post-warmup settle time (see V8 optimization cold start)"
1528
+ },
1529
+ "pause-first": {
1530
+ type: "number",
1531
+ describe: "iterations before first pause (then pause-interval applies)"
1532
+ },
1533
+ "pause-interval": {
1534
+ type: "number",
1535
+ default: defaultPauseInterval,
1536
+ describe: "iterations between pauses for V8 optimization (0 to disable)"
1537
+ },
1538
+ "pause-duration": {
1539
+ type: "number",
1540
+ default: defaultPauseDuration,
1541
+ describe: "pause duration in ms for V8 optimization"
1542
+ },
1543
+ batches: {
1544
+ type: "number",
1545
+ default: 1,
1546
+ describe: "divide time into N batches, alternating baseline/current order"
1547
+ },
1548
+ iterations: {
1549
+ type: "number",
1550
+ requiresArg: true,
1551
+ describe: "exact number of iterations (overrides --time)"
1552
+ },
1553
+ "heap-sample": {
1554
+ type: "boolean",
1555
+ default: false,
1556
+ describe: "heap sampling allocation attribution (includes garbage)"
1557
+ },
1558
+ "heap-interval": {
1559
+ type: "number",
1560
+ default: 32768,
1561
+ describe: "heap sampling interval in bytes"
1562
+ },
1563
+ "heap-depth": {
1564
+ type: "number",
1565
+ default: 64,
1566
+ describe: "heap sampling stack depth"
1567
+ },
1568
+ "heap-rows": {
1569
+ type: "number",
1570
+ default: 20,
1571
+ describe: "top allocation sites to show"
1572
+ },
1573
+ "heap-stack": {
1574
+ type: "number",
1575
+ default: 3,
1576
+ describe: "call stack depth to display"
1577
+ },
1578
+ "heap-verbose": {
1579
+ type: "boolean",
1580
+ default: false,
1581
+ describe: "verbose output with file:// paths and line numbers"
1582
+ },
1583
+ "heap-user-only": {
1584
+ type: "boolean",
1585
+ default: false,
1586
+ describe: "filter to user code only (hide node internals)"
1587
+ },
1588
+ url: {
1589
+ type: "string",
1590
+ requiresArg: true,
1591
+ describe: "page URL for browser profiling (enables browser mode)"
1592
+ },
1593
+ headless: {
1594
+ type: "boolean",
1595
+ default: true,
1596
+ describe: "run browser in headless mode"
1597
+ },
1598
+ timeout: {
1599
+ type: "number",
1600
+ default: 60,
1601
+ describe: "browser page timeout in seconds"
1602
+ }
1603
+ };
1604
+ /** @return yargs with standard benchmark options */
1605
+ function defaultCliArgs(yargsInstance) {
1606
+ return yargsInstance.options(cliOptions).help().strict();
1607
+ }
1608
+ /** @return parsed command line arguments */
1609
+ function parseCliArgs(args, configure = defaultCliArgs) {
1610
+ return configure(yargs(args)).parseSync();
1611
+ }
1612
+
1613
+ //#endregion
1614
+ //#region src/browser/BrowserGcStats.ts
1615
+ /** Parse CDP trace events (MinorGC/MajorGC) into GcEvent[] */
1616
+ function parseGcTraceEvents(traceEvents) {
1617
+ return traceEvents.flatMap((e) => {
1618
+ if (e.ph !== "X") return [];
1619
+ const type = gcType(e.name);
1620
+ if (!type) return [];
1621
+ const durUs = e.dur ?? 0;
1622
+ const heapBefore = e.args?.usedHeapSizeBefore ?? 0;
1623
+ const heapAfter = e.args?.usedHeapSizeAfter ?? 0;
1624
+ return [{
1625
+ type,
1626
+ pauseMs: durUs / 1e3,
1627
+ collected: Math.max(0, heapBefore - heapAfter)
1628
+ }];
1629
+ });
1630
+ }
1631
+ function gcType(name) {
1632
+ if (name === "MinorGC") return "scavenge";
1633
+ if (name === "MajorGC") return "mark-compact";
1634
+ }
1635
+ /** Parse CDP trace events and aggregate into GcStats */
1636
+ function browserGcStats(traceEvents) {
1637
+ return aggregateGcStats(parseGcTraceEvents(traceEvents));
1638
+ }
1639
+
1640
+ //#endregion
1641
+ //#region src/browser/BrowserHeapSampler.ts
1642
+ /** Run browser benchmark, auto-detecting page API mode.
1643
+ * Bench function (window.__bench): CLI controls iteration and timing.
1644
+ * Lap mode (__start/__lap/__done): page controls the measured region. */
1645
+ async function profileBrowser(params) {
1646
+ const { url, headless = true, timeout = 60 } = params;
1647
+ const { gcStats: collectGc } = params;
1648
+ const { samplingInterval = 32768 } = params.heapOptions ?? {};
1649
+ const browser = await chromium.launch({ headless });
1650
+ try {
1651
+ const page = await browser.newPage();
1652
+ page.setDefaultTimeout(timeout * 1e3);
1653
+ const cdp = await page.context().newCDPSession(page);
1654
+ const pageErrors = [];
1655
+ page.on("pageerror", (err) => pageErrors.push(err.message));
1656
+ const traceEvents = collectGc ? await startGcTracing(cdp) : [];
1657
+ const lapMode = await setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors);
1658
+ await page.goto(url, { waitUntil: "load" });
1659
+ const hasBench = await page.evaluate(() => typeof globalThis.__bench === "function");
1660
+ let result;
1661
+ if (hasBench) {
1662
+ lapMode.cancel();
1663
+ lapMode.promise.catch(() => {});
1664
+ result = await runBenchLoop(page, cdp, params, samplingInterval);
1665
+ } else {
1666
+ result = await lapMode.promise;
1667
+ lapMode.cancel();
1668
+ }
1669
+ if (collectGc) result = {
1670
+ ...result,
1671
+ gcStats: await collectTracing(cdp, traceEvents)
1672
+ };
1673
+ return result;
1674
+ } finally {
1675
+ await browser.close();
1676
+ }
1677
+ }
1678
+ /** Inject __start/__lap as in-page functions, expose __done for results collection.
1679
+ * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
1680
+ * instrument start. __done() stops instruments and collects timing data. */
1681
+ async function setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors) {
1682
+ const { heapSample } = params;
1683
+ const { promise, resolve, reject } = Promise.withResolvers();
1684
+ let instrumentsStarted = false;
1685
+ await page.exposeFunction("__benchInstrumentStart", async () => {
1686
+ if (instrumentsStarted) return;
1687
+ instrumentsStarted = true;
1688
+ if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
1689
+ });
1690
+ await page.exposeFunction("__benchCollect", async (samples, wallTimeMs) => {
1691
+ let heapProfile;
1692
+ if (heapSample && instrumentsStarted) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
1693
+ resolve({
1694
+ samples,
1695
+ heapProfile,
1696
+ wallTimeMs
1697
+ });
1698
+ });
1699
+ await page.addInitScript(injectLapFunctions);
1700
+ const timer = setTimeout(() => {
1701
+ const lines = [`Timed out after ${timeout}s`];
1702
+ if (pageErrors.length) lines.push("Page JS errors:", ...pageErrors.map((e) => ` ${e}`));
1703
+ else lines.push("Page did not call __done() or define window.__bench");
1704
+ reject(new Error(lines.join("\n")));
1705
+ }, timeout * 1e3);
1706
+ return {
1707
+ promise,
1708
+ cancel: () => clearTimeout(timer)
1709
+ };
1710
+ }
1711
+ /** In-page timing functions injected via addInitScript (zero CDP overhead).
1712
+ * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
1713
+ function injectLapFunctions() {
1714
+ const g = globalThis;
1715
+ g.__benchSamples = [];
1716
+ g.__benchLastTime = 0;
1717
+ g.__benchFirstStart = 0;
1718
+ g.__start = () => {
1719
+ const now = performance.now();
1720
+ g.__benchLastTime = now;
1721
+ if (!g.__benchFirstStart) {
1722
+ g.__benchFirstStart = now;
1723
+ return g.__benchInstrumentStart();
1724
+ }
1725
+ };
1726
+ g.__lap = () => {
1727
+ const now = performance.now();
1728
+ g.__benchSamples.push(now - g.__benchLastTime);
1729
+ g.__benchLastTime = now;
1730
+ };
1731
+ g.__done = () => {
1732
+ const wall = g.__benchFirstStart ? performance.now() - g.__benchFirstStart : 0;
1733
+ return g.__benchCollect(g.__benchSamples.slice(), wall);
1734
+ };
1735
+ }
1736
+ function heapSamplingParams(samplingInterval) {
1737
+ return {
1738
+ samplingInterval,
1739
+ includeObjectsCollectedByMajorGC: true,
1740
+ includeObjectsCollectedByMinorGC: true
1741
+ };
1742
+ }
1743
+ /** Start CDP GC tracing, returns the event collector array. */
1744
+ async function startGcTracing(cdp) {
1745
+ const events = [];
1746
+ cdp.on("Tracing.dataCollected", ({ value }) => {
1747
+ for (const e of value) events.push(e);
1748
+ });
1749
+ await cdp.send("Tracing.start", { traceConfig: { includedCategories: ["v8", "v8.gc"] } });
1750
+ return events;
1751
+ }
1752
+ /** Bench function mode: run window.__bench in a timed iteration loop. */
1753
+ async function runBenchLoop(page, cdp, params, samplingInterval) {
1754
+ const { heapSample } = params;
1755
+ const maxTime = params.maxTime ?? 642;
1756
+ const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;
1757
+ if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
1758
+ const { samples, totalMs } = await page.evaluate(async ({ maxTime, maxIter }) => {
1759
+ const bench = globalThis.__bench;
1760
+ const samples = [];
1761
+ const startAll = performance.now();
1762
+ const deadline = startAll + maxTime;
1763
+ for (let i = 0; i < maxIter && performance.now() < deadline; i++) {
1764
+ const t0 = performance.now();
1765
+ await bench();
1766
+ samples.push(performance.now() - t0);
1767
+ }
1768
+ return {
1769
+ samples,
1770
+ totalMs: performance.now() - startAll
1771
+ };
1772
+ }, {
1773
+ maxTime,
1774
+ maxIter
1775
+ });
1776
+ let heapProfile;
1777
+ if (heapSample) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
1778
+ return {
1779
+ samples,
1780
+ heapProfile,
1781
+ wallTimeMs: totalMs
1782
+ };
1783
+ }
1784
+ /** Stop CDP tracing and parse GC events into GcStats. */
1785
+ async function collectTracing(cdp, traceEvents) {
1786
+ const complete = new Promise((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
1787
+ await cdp.send("Tracing.end");
1788
+ await complete;
1789
+ return browserGcStats(traceEvents);
1790
+ }
1791
+
1792
+ //#endregion
1793
+ //#region src/export/JsonExport.ts
1794
+ /** Export benchmark results to JSON file */
1795
+ async function exportBenchmarkJson(groups, outputPath, args, suiteName = "Benchmark Suite") {
1796
+ const jsonData = prepareJsonData(groups, args, suiteName);
1797
+ await writeFile(outputPath, JSON.stringify(jsonData, null, 2), "utf-8");
1798
+ console.log(`Benchmark data exported to: ${outputPath}`);
1799
+ }
1800
+ /** Convert ReportGroup data to JSON format */
1801
+ function prepareJsonData(groups, args, suiteName) {
1802
+ return {
1803
+ meta: {
1804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1805
+ version: process.env.npm_package_version || "unknown",
1806
+ args: cleanCliArgs(args),
1807
+ environment: {
1808
+ node: process.version,
1809
+ platform: process.platform,
1810
+ arch: process.arch
1811
+ }
1812
+ },
1813
+ suites: [{
1814
+ name: suiteName,
1815
+ groups: groups.map(convertGroup)
1816
+ }]
1817
+ };
1818
+ }
1819
+ /** Convert a report group, mapping each report to the JSON result format */
1820
+ function convertGroup(group) {
1821
+ return {
1822
+ name: "Benchmark Group",
1823
+ baseline: group.baseline ? convertReport(group.baseline) : void 0,
1824
+ benchmarks: group.reports.map(convertReport)
1825
+ };
1826
+ }
1827
+ /** Extract measured stats and optional metrics into JSON result shape */
1828
+ function convertReport(report) {
1829
+ const { name, measuredResults: m } = report;
1830
+ const { time, heapSize, gcTime, cpu } = m;
1831
+ const minMaxMean = (s) => s ? {
1832
+ min: s.min,
1833
+ max: s.max,
1834
+ mean: s.avg
1835
+ } : void 0;
1836
+ return {
1837
+ name,
1838
+ status: "completed",
1839
+ samples: m.samples || [],
1840
+ time: {
1841
+ ...minMaxMean(time),
1842
+ p50: time.p50,
1843
+ p75: time.p75,
1844
+ p99: time.p99,
1845
+ p999: time.p999
1846
+ },
1847
+ heapSize: minMaxMean(heapSize),
1848
+ gcTime: minMaxMean(gcTime),
1849
+ cpu: cpu ? {
1850
+ instructions: cpu.instructions,
1851
+ cycles: cpu.cycles,
1852
+ cacheMisses: m.cpuCacheMiss,
1853
+ branchMisses: cpu.branchMisses
1854
+ } : void 0,
1855
+ execution: {
1856
+ iterations: m.samples?.length || 0,
1857
+ totalTime: m.totalTime || 0,
1858
+ warmupRuns: void 0
1859
+ }
1860
+ };
1861
+ }
1862
+ /** Clean CLI args for JSON export (remove undefined values) */
1863
+ function cleanCliArgs(args) {
1864
+ const toCamel = (k) => k.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
1865
+ const entries = Object.entries(args).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => [toCamel(k), v]);
1866
+ return Object.fromEntries(entries);
1867
+ }
1868
+
1869
+ //#endregion
1870
+ //#region src/export/PerfettoExport.ts
1871
+ const pid = 1;
1872
+ const tid = 1;
1873
+ /** Export benchmark results to Perfetto-compatible trace file */
1874
+ function exportPerfettoTrace(groups, outputPath, args) {
1875
+ const absPath = resolve(outputPath);
1876
+ writeTraceFile(absPath, mergeV8Trace(buildTraceEvents(groups, args)));
1877
+ console.log(`Perfetto trace exported to: ${outputPath}`);
1878
+ scheduleDeferredMerge(absPath);
1879
+ }
1880
+ /** Build trace events from benchmark results */
1881
+ function buildTraceEvents(groups, args) {
1882
+ const meta = (name, a) => ({
1883
+ ph: "M",
1884
+ ts: 0,
1885
+ pid,
1886
+ tid,
1887
+ name,
1888
+ args: a
1889
+ });
1890
+ const events = [
1891
+ meta("process_name", { name: "wesl-bench" }),
1892
+ meta("thread_name", { name: "MainThread" }),
1893
+ meta("bench_settings", cleanArgs(args))
1894
+ ];
1895
+ for (const group of groups) for (const report of group.reports) {
1896
+ const results = report.measuredResults;
1897
+ events.push(...buildBenchmarkEvents(results));
1898
+ }
1899
+ return events;
1900
+ }
1901
+ function instant(ts, name, args) {
1902
+ return {
1903
+ ph: "i",
1904
+ ts,
1905
+ pid,
1906
+ tid,
1907
+ cat: "bench",
1908
+ name,
1909
+ s: "t",
1910
+ args
1911
+ };
1912
+ }
1913
+ function counter(ts, name, args) {
1914
+ return {
1915
+ ph: "C",
1916
+ ts,
1917
+ pid,
1918
+ tid,
1919
+ cat: "bench",
1920
+ name,
1921
+ args
1922
+ };
1923
+ }
1924
+ /** Build events for a single benchmark run */
1925
+ function buildBenchmarkEvents(results) {
1926
+ const { samples, heapSamples, timestamps, pausePoints } = results;
1927
+ if (!timestamps?.length) return [];
1928
+ const events = [];
1929
+ for (let i = 0; i < samples.length; i++) {
1930
+ const ts = timestamps[i];
1931
+ const ms = Math.round(samples[i] * 100) / 100;
1932
+ events.push(instant(ts, results.name, {
1933
+ n: i,
1934
+ ms
1935
+ }));
1936
+ events.push(counter(ts, "duration", { ms }));
1937
+ if (heapSamples?.[i] !== void 0) {
1938
+ const MB = Math.round(heapSamples[i] / 1024 / 1024 * 10) / 10;
1939
+ events.push(counter(ts, "heap", { MB }));
1940
+ }
1941
+ }
1942
+ for (const pause of pausePoints ?? []) {
1943
+ const ts = timestamps[pause.sampleIndex];
1944
+ if (ts) events.push(instant(ts, "pause", { ms: pause.durationMs }));
1945
+ }
1946
+ return events;
1947
+ }
1948
+ /** Normalize timestamps so events start at 0 */
1949
+ function normalizeTimestamps(events) {
1950
+ const times = events.filter((e) => e.ts > 0).map((e) => e.ts);
1951
+ if (times.length === 0) return;
1952
+ const minTs = Math.min(...times);
1953
+ for (const e of events) if (e.ts > 0) e.ts -= minTs;
1954
+ }
1955
+ /** Merge V8 trace events from a previous run, aligning timestamps */
1956
+ function mergeV8Trace(customEvents) {
1957
+ const v8Events = loadV8Events(readdirSync(".").filter((f) => f.startsWith("node_trace.") && f.endsWith(".log"))[0]);
1958
+ normalizeTimestamps(customEvents);
1959
+ if (!v8Events) return customEvents;
1960
+ normalizeTimestamps(v8Events);
1961
+ return [...v8Events, ...customEvents];
1962
+ }
1963
+ /** Load V8 trace events from file, or undefined if unavailable */
1964
+ function loadV8Events(v8TracePath) {
1965
+ if (!v8TracePath) return void 0;
1966
+ try {
1967
+ const v8Data = JSON.parse(readFileSync(v8TracePath, "utf-8"));
1968
+ console.log(`Merged ${v8Data.traceEvents.length} V8 events from ${v8TracePath}`);
1969
+ return v8Data.traceEvents;
1970
+ } catch {
1971
+ console.warn(`Could not parse V8 trace file: ${v8TracePath}`);
1972
+ return;
1973
+ }
1974
+ }
1975
+ /** Write trace events to JSON file */
1976
+ function writeTraceFile(outputPath, events) {
1977
+ const traceFile = { traceEvents: events };
1978
+ writeFileSync(outputPath, JSON.stringify(traceFile));
1979
+ }
1980
+ /** Clean CLI args for metadata */
1981
+ function cleanArgs(args) {
1982
+ const skip = new Set(["_", "$0"]);
1983
+ const entries = Object.entries(args).filter(([k, v]) => v !== void 0 && !skip.has(k));
1984
+ return Object.fromEntries(entries);
1985
+ }
1986
+ /** Spawn a detached child to merge V8 trace after process exit */
1987
+ function scheduleDeferredMerge(outputPath) {
1988
+ const cwd = process.cwd();
1989
+ const mergeScript = `
1990
+ const { readdirSync, readFileSync, writeFileSync } = require('fs');
1991
+ function normalize(events) {
1992
+ const times = events.filter(e => e.ts > 0).map(e => e.ts);
1993
+ if (!times.length) return;
1994
+ const min = Math.min(...times);
1995
+ for (const e of events) if (e.ts > 0) e.ts -= min;
1996
+ }
1997
+ setTimeout(() => {
1998
+ const traceFiles = readdirSync('.').filter(f => f.startsWith('node_trace.') && f.endsWith('.log'));
1999
+ if (traceFiles.length === 0) process.exit(0);
2000
+ try {
2001
+ const v8Data = JSON.parse(readFileSync(traceFiles[0], 'utf-8'));
2002
+ const ourData = JSON.parse(readFileSync('${outputPath}', 'utf-8'));
2003
+ normalize(v8Data.traceEvents);
2004
+ const merged = { traceEvents: [...v8Data.traceEvents, ...ourData.traceEvents] };
2005
+ writeFileSync('${outputPath}', JSON.stringify(merged));
2006
+ console.log('Merged ' + v8Data.traceEvents.length + ' V8 events into ' + '${outputPath}');
2007
+ } catch (e) { console.error('Merge failed:', e.message); }
2008
+ }, 100);
2009
+ `;
2010
+ process.on("exit", () => {
2011
+ spawn("node", ["-e", mergeScript], {
2012
+ detached: true,
2013
+ stdio: "inherit",
2014
+ cwd
2015
+ }).unref();
2016
+ });
2017
+ }
2018
+
2019
+ //#endregion
2020
+ //#region src/HtmlDataPrep.ts
2021
+ /** Find higherIsBetter from first comparable column in sections */
2022
+ function findHigherIsBetter(sections) {
2023
+ return (sections?.flatMap((s) => s.columns().flatMap((g) => g.columns)))?.find((c) => c.comparable)?.higherIsBetter ?? false;
2024
+ }
2025
+ /** Flip CI percent for metrics where higher is better (e.g., lines/sec) */
2026
+ function flipCI(ci) {
2027
+ return {
2028
+ percent: -ci.percent,
2029
+ ci: [-ci.ci[1], -ci.ci[0]],
2030
+ direction: ci.direction,
2031
+ histogram: ci.histogram?.map((bin) => ({
2032
+ x: -bin.x,
2033
+ count: bin.count
2034
+ }))
2035
+ };
2036
+ }
2037
+ /** Prepare ReportData from benchmark results for HTML rendering */
2038
+ function prepareHtmlData(groups, options) {
2039
+ const { cliArgs, sections, currentVersion, baselineVersion } = options;
2040
+ const higherIsBetter = findHigherIsBetter(sections);
2041
+ return {
2042
+ groups: groups.map((group) => prepareGroupData(group, sections, higherIsBetter)),
2043
+ metadata: {
2044
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2045
+ bencherVersion: process.env.npm_package_version || "unknown",
2046
+ cliArgs,
2047
+ gcTrackingEnabled: cliArgs?.["gc-stats"] === true,
2048
+ currentVersion,
2049
+ baselineVersion
2050
+ }
2051
+ };
2052
+ }
2053
+ /** @return group data with bootstrap CI comparisons against baseline */
2054
+ function prepareGroupData(group, sections, higherIsBetter) {
2055
+ const baselineSamples = group.baseline?.measuredResults.samples;
2056
+ return {
2057
+ name: group.name,
2058
+ baseline: group.baseline ? prepareBenchmarkData(group.baseline, sections) : void 0,
2059
+ benchmarks: group.reports.map((report) => {
2060
+ const samples = report.measuredResults.samples;
2061
+ const rawCI = baselineSamples && samples ? bootstrapDifferenceCI(baselineSamples, samples) : void 0;
2062
+ const comparisonCI = rawCI && higherIsBetter ? flipCI(rawCI) : rawCI;
2063
+ return {
2064
+ ...prepareBenchmarkData(report, sections),
2065
+ comparisonCI
2066
+ };
2067
+ })
2068
+ };
2069
+ }
2070
+ /** @return benchmark data with samples, stats, and formatted section values */
2071
+ function prepareBenchmarkData(report, sections) {
2072
+ const { measuredResults } = report;
2073
+ return {
2074
+ name: report.name,
2075
+ samples: measuredResults.samples,
2076
+ warmupSamples: measuredResults.warmupSamples,
2077
+ allocationSamples: measuredResults.allocationSamples,
2078
+ heapSamples: measuredResults.heapSamples,
2079
+ gcEvents: measuredResults.nodeGcTime?.events,
2080
+ optSamples: measuredResults.optSamples,
2081
+ pausePoints: measuredResults.pausePoints,
2082
+ stats: measuredResults.time,
2083
+ heapSize: measuredResults.heapSize,
2084
+ sectionStats: sections ? extractSectionStats(report, sections) : void 0
2085
+ };
2086
+ }
2087
+ /** @return formatted stats from all sections for tooltip display */
2088
+ function extractSectionStats(report, sections) {
2089
+ return sections.flatMap((section) => {
2090
+ const vals = section.extract(report.measuredResults, report.metadata);
2091
+ return section.columns().flatMap((g) => formatGroupStats(vals, g));
2092
+ });
2093
+ }
2094
+ /** @return formatted stats for one column group, skipping undefined values */
2095
+ function formatGroupStats(values, group) {
2096
+ return group.columns.map((c) => formatColumnStat(values, c, group.groupTitle)).filter((s) => s !== void 0);
2097
+ }
2098
+ /** @return formatted stat for a single column, or undefined if empty/placeholder */
2099
+ function formatColumnStat(values, col, groupTitle) {
2100
+ const raw = values[col.key];
2101
+ if (raw === void 0) return void 0;
2102
+ const formatted = col.formatter ? col.formatter(raw) : String(raw);
2103
+ if (!formatted || formatted === "—" || formatted === "") return void 0;
2104
+ return {
2105
+ label: col.title,
2106
+ value: formatted,
2107
+ groupTitle
2108
+ };
2109
+ }
2110
+
2111
+ //#endregion
2112
+ //#region src/heap-sample/HeapSampleReport.ts
2113
+ /** Sum selfSize across all nodes in profile (before any filtering) */
2114
+ function totalProfileBytes(profile) {
2115
+ let total = 0;
2116
+ function walk(node) {
2117
+ total += node.selfSize;
2118
+ for (const child of node.children || []) walk(child);
2119
+ }
2120
+ walk(profile.head);
2121
+ return total;
2122
+ }
2123
+ /** Flatten profile tree into sorted list of allocation sites with call stacks */
2124
+ function flattenProfile(profile) {
2125
+ const sites = [];
2126
+ function walk(node, stack) {
2127
+ const { functionName, url, lineNumber, columnNumber } = node.callFrame;
2128
+ const fn = functionName || "(anonymous)";
2129
+ const col = columnNumber ?? 0;
2130
+ const frame = {
2131
+ fn,
2132
+ url: url || "",
2133
+ line: lineNumber + 1,
2134
+ col
2135
+ };
2136
+ const newStack = [...stack, frame];
2137
+ if (node.selfSize > 0) sites.push({
2138
+ ...frame,
2139
+ bytes: node.selfSize,
2140
+ stack: newStack
2141
+ });
2142
+ for (const child of node.children || []) walk(child, newStack);
2143
+ }
2144
+ walk(profile.head, []);
2145
+ return sites.sort((a, b) => b.bytes - a.bytes);
2146
+ }
2147
+ /** Check if site is user code (not node internals) */
2148
+ function isNodeUserCode(site) {
2149
+ if (!site.url) return false;
2150
+ if (site.url.startsWith("node:")) return false;
2151
+ if (site.url.includes("(native)")) return false;
2152
+ if (site.url.includes("internal/")) return false;
2153
+ return true;
2154
+ }
2155
+ /** Check if site is user code (not browser internals) */
2156
+ function isBrowserUserCode(site) {
2157
+ if (!site.url) return false;
2158
+ if (site.url.startsWith("chrome-extension://")) return false;
2159
+ if (site.url.startsWith("devtools://")) return false;
2160
+ if (site.url.includes("(native)")) return false;
2161
+ return true;
2162
+ }
2163
+ /** Filter sites to user code only */
2164
+ function filterSites(sites, isUser = isNodeUserCode) {
2165
+ return sites.filter(isUser);
2166
+ }
2167
+ /** Aggregate sites by location (combine same file:line:col) */
2168
+ function aggregateSites(sites) {
2169
+ const byLocation = /* @__PURE__ */ new Map();
2170
+ for (const site of sites) {
2171
+ const key = `${site.url}:${site.line}:${site.col}`;
2172
+ const existing = byLocation.get(key);
2173
+ if (existing) existing.bytes += site.bytes;
2174
+ else byLocation.set(key, { ...site });
2175
+ }
2176
+ return [...byLocation.values()].sort((a, b) => b.bytes - a.bytes);
2177
+ }
2178
+ function fmtBytes(bytes) {
2179
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
2180
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2181
+ return `${bytes} B`;
2182
+ }
2183
+ /** Format heap report for console output */
2184
+ function formatHeapReport(sites, options) {
2185
+ const { topN, stackDepth = 3, verbose = false } = options;
2186
+ const { totalAll, totalUserCode, sampleCount } = options;
2187
+ const isUser = options.isUserCode ?? isNodeUserCode;
2188
+ const lines = [];
2189
+ lines.push(`Heap allocation sites (top ${topN}, garbage included):`);
2190
+ for (const site of sites.slice(0, topN)) if (verbose) formatVerboseSite(lines, site, stackDepth, isUser);
2191
+ else formatCompactSite(lines, site, stackDepth, isUser);
2192
+ lines.push("");
2193
+ if (totalAll !== void 0) lines.push(`Total (all): ${fmtBytes(totalAll)}`);
2194
+ if (totalUserCode !== void 0) lines.push(`Total (user-code): ${fmtBytes(totalUserCode)}`);
2195
+ if (sampleCount !== void 0) lines.push(`Samples: ${sampleCount.toLocaleString()}`);
2196
+ return lines.join("\n");
2197
+ }
2198
+ /** Compact single-line format: `49 MB fn1 <- fn2 <- fn3` */
2199
+ function formatCompactSite(lines, site, stackDepth, isUser) {
2200
+ const bytes = fmtBytes(site.bytes).padStart(10);
2201
+ const fns = [site.fn];
2202
+ if (site.stack && site.stack.length > 1) {
2203
+ const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
2204
+ for (const frame of callers) {
2205
+ if (!frame.url || !isUser(frame)) continue;
2206
+ fns.push(frame.fn);
2207
+ }
2208
+ }
2209
+ const line = `${bytes} ${fns.join(" <- ")}`;
2210
+ lines.push(isUser(site) ? line : pico.dim(line));
2211
+ }
2212
+ /** Verbose multi-line format with file:// paths and line numbers */
2213
+ function formatVerboseSite(lines, site, stackDepth, isUser) {
2214
+ const bytes = fmtBytes(site.bytes).padStart(10);
2215
+ const loc = site.url ? `${site.url}:${site.line}:${site.col}` : "(unknown)";
2216
+ const dimFn = isUser(site) ? (s) => s : pico.dim;
2217
+ lines.push(dimFn(`${bytes} ${site.fn} ${loc}`));
2218
+ if (site.stack && site.stack.length > 1) {
2219
+ const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
2220
+ for (const frame of callers) {
2221
+ if (!frame.url || !isUser(frame)) continue;
2222
+ const callerLoc = `${frame.url}:${frame.line}:${frame.col}`;
2223
+ lines.push(dimFn(` <- ${frame.fn} ${callerLoc}`));
2224
+ }
2225
+ }
2226
+ }
2227
+
2228
+ //#endregion
2229
+ //#region src/html/HtmlTemplate.ts
2230
+ const skipArgs = new Set([
2231
+ "_",
2232
+ "$0",
2233
+ "html",
2234
+ "export-html"
2235
+ ]);
2236
+ /** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
2237
+ function formatDateWithTimezone(isoDate) {
2238
+ const date = new Date(isoDate);
2239
+ return `${date.toLocaleString("en-US", {
2240
+ month: "short",
2241
+ day: "numeric",
2242
+ year: "numeric",
2243
+ hour: "numeric",
2244
+ minute: "2-digit"
2245
+ })} (${date.toISOString().replace(".000Z", "Z")})`;
2246
+ }
2247
+ /** Format relative time: "5m ago", "2h ago", "yesterday", "3 days ago" */
2248
+ function formatRelativeTime(isoDate) {
2249
+ const date = new Date(isoDate);
2250
+ const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
2251
+ const diffMins = Math.floor(diffMs / 6e4);
2252
+ const diffHours = Math.floor(diffMs / 36e5);
2253
+ const diffDays = Math.floor(diffMs / 864e5);
2254
+ if (diffMins < 1) return "just now";
2255
+ if (diffMins < 60) return `${diffMins}m ago`;
2256
+ if (diffHours < 24) return `${diffHours}h ago`;
2257
+ if (diffDays === 1) return "yesterday";
2258
+ if (diffDays < 30) return `${diffDays} days ago`;
2259
+ return date.toLocaleDateString("en-US", {
2260
+ month: "short",
2261
+ day: "numeric"
2262
+ });
2263
+ }
2264
+ /** Format git version for display: "abc1234* (5m ago)" */
2265
+ function formatVersion(version) {
2266
+ if (!version || version.hash === "unknown") return "unknown";
2267
+ const hashDisplay = version.dirty ? `${version.hash}*` : version.hash;
2268
+ const timeDisplay = version.date ? formatRelativeTime(version.date) : "";
2269
+ return timeDisplay ? `${hashDisplay} (${timeDisplay})` : hashDisplay;
2270
+ }
2271
+ /** Render current/baseline version info as an HTML div */
2272
+ function versionInfoHtml(data) {
2273
+ const { currentVersion, baselineVersion } = data.metadata;
2274
+ if (!currentVersion && !baselineVersion) return "";
2275
+ const parts = [];
2276
+ if (currentVersion) parts.push(`Current: ${formatVersion(currentVersion)}`);
2277
+ if (baselineVersion) parts.push(`Baseline: ${formatVersion(baselineVersion)}`);
2278
+ return `<div class="version-info">${parts.join(" | ")}</div>`;
2279
+ }
2280
+ const badgeLabels = {
2281
+ faster: "Faster",
2282
+ slower: "Slower",
2283
+ uncertain: "Inconclusive"
2284
+ };
2285
+ /** Render faster/slower/uncertain badge with CI plot container */
2286
+ function comparisonBadge(group, groupIndex) {
2287
+ const ci = group.benchmarks[0]?.comparisonCI;
2288
+ if (!ci) return "";
2289
+ const label = badgeLabels[ci.direction];
2290
+ return `
2291
+ <span class="badge badge-${ci.direction}">${label}</span>
2292
+ <div id="ci-plot-${groupIndex}" class="ci-plot-container"></div>
2293
+ `;
2294
+ }
2295
+ const defaultArgs = {
2296
+ worker: true,
2297
+ time: 5,
2298
+ warmup: 500,
2299
+ "pause-interval": 0,
2300
+ "pause-duration": 100
2301
+ };
2302
+ /** @return true if this CLI arg should be hidden from the report header */
2303
+ function shouldSkipArg(key, value, adaptive) {
2304
+ if (skipArgs.has(key) || value === void 0 || value === false) return true;
2305
+ if (defaultArgs[key] === value) return true;
2306
+ if (!key.includes("-") && key !== key.toLowerCase()) return true;
2307
+ if (key === "convergence" && !adaptive) return true;
2308
+ return false;
2309
+ }
2310
+ /** Reconstruct the CLI invocation string, omitting default/internal args */
2311
+ function formatCliArgs(args) {
2312
+ if (!args) return "bb bench";
2313
+ const parts = ["bb bench"];
2314
+ for (const [key, value] of Object.entries(args)) {
2315
+ if (shouldSkipArg(key, value, args.adaptive)) continue;
2316
+ parts.push(value === true ? `--${key}` : `--${key} ${value}`);
2317
+ }
2318
+ return parts.join(" ");
2319
+ }
2320
+ /** Generate complete HTML document with embedded data and visualizations */
2321
+ function generateHtmlDocument(data) {
2322
+ return `<!DOCTYPE html>
2323
+ <html lang="en">
2324
+ <head>
2325
+ <meta charset="UTF-8">
2326
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2327
+ <title>Benchmark Report - ${(/* @__PURE__ */ new Date()).toLocaleDateString()}</title>
2328
+ <style>
2329
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2330
+ body {
2331
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
2332
+ background: #f5f5f5;
2333
+ padding: 20px;
2334
+ line-height: 1.6;
2335
+ }
2336
+ .header {
2337
+ background: white;
2338
+ padding: 10px 15px;
2339
+ border-radius: 8px;
2340
+ margin-bottom: 20px;
2341
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
2342
+ display: flex;
2343
+ justify-content: space-between;
2344
+ align-items: center;
2345
+ }
2346
+ h1 { display: none; }
2347
+ h2 {
2348
+ color: #555;
2349
+ margin: 30px 0 20px;
2350
+ font-size: 20px;
2351
+ border-bottom: 2px solid #e0e0e0;
2352
+ padding-bottom: 10px;
2353
+ }
2354
+ .metadata { color: #666; font-size: 12px; }
2355
+ .cli-args {
2356
+ font-family: "SF Mono", Monaco, "Consolas", monospace;
2357
+ font-size: 11px;
2358
+ color: #555;
2359
+ background: #f0f0f0;
2360
+ padding: 6px 10px;
2361
+ border-radius: 4px;
2362
+ word-break: break-word;
2363
+ }
2364
+ .comparison-mode {
2365
+ background: #fff3cd;
2366
+ color: #856404;
2367
+ padding: 8px 12px;
2368
+ border-radius: 4px;
2369
+ display: inline-block;
2370
+ margin-top: 10px;
2371
+ font-weight: 500;
2372
+ }
2373
+ .plot-grid {
2374
+ display: grid;
2375
+ grid-template-columns: 1fr 1fr;
2376
+ gap: 20px;
2377
+ margin-bottom: 30px;
2378
+ }
2379
+ .plot-grid.second-row { grid-template-columns: 1fr; }
2380
+ .plot-container {
2381
+ background: white;
2382
+ padding: 20px;
2383
+ border-radius: 8px;
2384
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
2385
+ }
2386
+ .plot-container.full-width { grid-column: 1 / -1; }
2387
+ .plot-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; color: #333; }
2388
+ .plot-description { font-size: 14px; color: #666; margin-bottom: 15px; }
2389
+ .plot-area {
2390
+ display: flex;
2391
+ justify-content: center;
2392
+ align-items: center;
2393
+ min-height: 300px;
2394
+ }
2395
+ .plot-area svg { overflow: visible; }
2396
+ .plot-area svg g[aria-label="x-axis label"] text { font-size: 14px; }
2397
+ .summary-stats { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
2398
+ .stats-grid {
2399
+ display: grid;
2400
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2401
+ gap: 10px;
2402
+ margin-top: 10px;
2403
+ }
2404
+ .stat-item { background: white; padding: 10px; border-radius: 4px; text-align: center; }
2405
+ .stat-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
2406
+ .stat-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 4px; }
2407
+ .loading { color: #666; font-style: italic; padding: 20px; text-align: center; }
2408
+ .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 4px; margin: 10px 0; }
2409
+ .ci-faster { color: #22c55e; }
2410
+ .ci-slower { color: #ef4444; }
2411
+ .ci-uncertain { color: #6b7280; }
2412
+ .group-header {
2413
+ display: flex;
2414
+ align-items: center;
2415
+ gap: 12px;
2416
+ margin: 30px 0 20px;
2417
+ padding-bottom: 10px;
2418
+ border-bottom: 2px solid #e0e0e0;
2419
+ }
2420
+ .group-header h2 { margin: 0; border: none; padding: 0; }
2421
+ .badge {
2422
+ font-size: 12px;
2423
+ font-weight: 600;
2424
+ padding: 4px 10px;
2425
+ border-radius: 12px;
2426
+ text-transform: uppercase;
2427
+ letter-spacing: 0.5px;
2428
+ }
2429
+ .badge-faster { background: #dcfce7; color: #166534; }
2430
+ .badge-slower { background: #fee2e2; color: #991b1b; }
2431
+ .badge-uncertain { background: #dbeafe; color: #1e40af; }
2432
+ .version-info { font-size: 12px; color: #666; margin-top: 6px; }
2433
+ .header-right { text-align: right; }
2434
+ .ci-plot-container { display: inline-block; vertical-align: middle; margin-left: 8px; }
2435
+ .ci-plot-container svg { display: block; }
2436
+ </style>
2437
+ </head>
2438
+ <body>
2439
+ <div class="header">
2440
+ <div class="cli-args">${formatCliArgs(data.metadata.cliArgs)}</div>
2441
+ <div class="header-right">
2442
+ <div class="metadata">Generated: ${formatDateWithTimezone((/* @__PURE__ */ new Date()).toISOString())}</div>
2443
+ ${versionInfoHtml(data)}
2444
+ </div>
2445
+ </div>
2446
+
2447
+ ${data.groups.map((group, i) => `
2448
+ <div id="group-${i}">
2449
+ ${group.benchmarks.length > 0 ? `
2450
+ <div class="group-header">
2451
+ <h2>${group.name}</h2>
2452
+ ${comparisonBadge(group, i)}
2453
+ </div>
2454
+
2455
+ <div class="plot-grid">
2456
+ <div class="plot-container">
2457
+ <div class="plot-title">Time per Sample</div>
2458
+ <div class="plot-description">Execution time for each sample in collection order</div>
2459
+ <div id="sample-timeseries-${i}" class="plot-area">
2460
+ <div class="loading">Loading time series...</div>
2461
+ </div>
2462
+ </div>
2463
+
2464
+ <div class="plot-container">
2465
+ <div class="plot-title">Time Distribution</div>
2466
+ <div class="plot-description">Frequency distribution of execution times</div>
2467
+ <div id="histogram-${i}" class="plot-area">
2468
+ <div class="loading">Loading histogram...</div>
2469
+ </div>
2470
+ </div>
2471
+ </div>
2472
+
2473
+ <div id="stats-${i}"></div>
2474
+ ` : "<div class=\"error\">No benchmark data available for this group</div>"}
2475
+ </div>
2476
+ `).join("")}
2477
+
2478
+ <script type="importmap">
2479
+ {
2480
+ "imports": {
2481
+ "d3": "https://cdn.jsdelivr.net/npm/d3@7/+esm",
2482
+ "@observablehq/plot": "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"
2483
+ }
2484
+ }
2485
+ <\/script>
2486
+ <script type="module">
2487
+ import { renderPlots } from "./plots.js";
2488
+ const benchmarkData = ${JSON.stringify(data, null, 2)};
2489
+ renderPlots(benchmarkData);
2490
+ <\/script>
2491
+ </body>
2492
+ </html>`;
2493
+ }
2494
+
2495
+ //#endregion
2496
+ //#region src/html/HtmlReport.ts
2497
+ /** Generate HTML report from prepared data and optionally open in browser */
2498
+ async function generateHtmlReport(data, options) {
2499
+ const html = generateHtmlDocument(data);
2500
+ const reportDir = options.outputPath || await createReportDir();
2501
+ await mkdir(reportDir, { recursive: true });
2502
+ await writeFile(join(reportDir, "index.html"), html, "utf-8");
2503
+ const plots = await loadPlotsBundle();
2504
+ await writeFile(join(reportDir, "plots.js"), plots, "utf-8");
2505
+ await writeLatestRedirect(reportDir);
2506
+ let server;
2507
+ let closeServer;
2508
+ if (options.openBrowser) {
2509
+ const baseDir = dirname(reportDir);
2510
+ const reportName = reportDir.split("/").pop();
2511
+ const result = await startReportServer(baseDir, 7979, 7978, 7977);
2512
+ server = result.server;
2513
+ closeServer = () => result.server.close();
2514
+ const openUrl = `http://localhost:${result.port}/${reportName}/`;
2515
+ await open(openUrl);
2516
+ console.log(`Report opened in browser: ${openUrl}`);
2517
+ } else console.log(`Report saved to: ${reportDir}/`);
2518
+ return {
2519
+ reportDir,
2520
+ server,
2521
+ closeServer
2522
+ };
2523
+ }
2524
+ /** Start HTTP server for report directory, trying fallback ports if needed */
2525
+ async function startReportServer(baseDir, ...ports) {
2526
+ const mimeTypes = {
2527
+ ".html": "text/html",
2528
+ ".js": "application/javascript",
2529
+ ".css": "text/css",
2530
+ ".json": "application/json"
2531
+ };
2532
+ const server = createServer(async (req, res) => {
2533
+ const url = req.url || "/";
2534
+ const filePath = join(baseDir, url.endsWith("/") ? url + "index.html" : url);
2535
+ try {
2536
+ const content = await readFile(filePath);
2537
+ const mime = mimeTypes[extname(filePath)] || "application/octet-stream";
2538
+ res.setHeader("Content-Type", mime);
2539
+ res.end(content);
2540
+ } catch {
2541
+ res.statusCode = 404;
2542
+ res.end("Not found");
2543
+ }
2544
+ });
2545
+ for (const port of ports) try {
2546
+ return await tryListen(server, port);
2547
+ } catch {}
2548
+ return tryListen(server, 0);
2549
+ }
2550
+ /** Listen on a port, resolving with the actual port or rejecting on error */
2551
+ function tryListen(server, port) {
2552
+ return new Promise((resolve, reject) => {
2553
+ server.once("error", reject);
2554
+ server.listen(port, () => {
2555
+ server.removeListener("error", reject);
2556
+ const addr = server.address();
2557
+ resolve({
2558
+ server,
2559
+ port: typeof addr === "object" && addr ? addr.port : port
2560
+ });
2561
+ });
2562
+ });
2563
+ }
2564
+ /** Create a timestamped report directory under ./bench-report/ */
2565
+ async function createReportDir() {
2566
+ const base = "./bench-report";
2567
+ await mkdir(base, { recursive: true });
2568
+ return join(base, `report-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`);
2569
+ }
2570
+ /** Read the pre-built browser plots bundle from dist/ */
2571
+ async function loadPlotsBundle() {
2572
+ const thisDir = dirname(fileURLToPath(import.meta.url));
2573
+ const builtPath = join(thisDir, "browser/index.js");
2574
+ const devPath = join(thisDir, "../../dist/browser/index.js");
2575
+ try {
2576
+ return await readFile(builtPath, "utf-8");
2577
+ } catch {}
2578
+ return readFile(devPath, "utf-8");
2579
+ }
2580
+ /** Write an index.html in the parent dir that redirects to this report */
2581
+ async function writeLatestRedirect(reportDir) {
2582
+ const baseDir = dirname(reportDir);
2583
+ const reportName = reportDir.split("/").pop();
2584
+ const html = `<!DOCTYPE html>
2585
+ <html><head>
2586
+ <meta http-equiv="refresh" content="0; url=./${reportName}/">
2587
+ <script>location.href = "./${reportName}/";<\/script>
2588
+ </head><body>
2589
+ <a href="./${reportName}/">Latest report</a>
2590
+ </body></html>`;
2591
+ await writeFile(join(baseDir, "index.html"), html, "utf-8");
2592
+ }
2593
+
2594
+ //#endregion
2595
+ //#region src/matrix/MatrixFilter.ts
2596
+ /** Parse filter string: "case/variant", "case/", "/variant", or "case" */
2597
+ function parseMatrixFilter(filter) {
2598
+ if (filter.includes("/")) {
2599
+ const [casePart, variantPart] = filter.split("/", 2);
2600
+ return {
2601
+ case: casePart || void 0,
2602
+ variant: variantPart || void 0
2603
+ };
2604
+ }
2605
+ return { case: filter };
2606
+ }
2607
+ /** Apply filter to a matrix, merging with existing filters via intersection */
2608
+ async function filterMatrix(matrix, filter) {
2609
+ if (!filter || !filter.case && !filter.variant) return matrix;
2610
+ const caseList = await getFilteredCases(matrix, filter.case);
2611
+ const variantList = await getFilteredVariants(matrix, filter.variant);
2612
+ const filteredCases = caseList && matrix.filteredCases ? caseList.filter((c) => matrix.filteredCases.includes(c)) : caseList ?? matrix.filteredCases;
2613
+ const filteredVariants = variantList && matrix.filteredVariants ? variantList.filter((v) => matrix.filteredVariants.includes(v)) : variantList ?? matrix.filteredVariants;
2614
+ return {
2615
+ ...matrix,
2616
+ filteredCases,
2617
+ filteredVariants
2618
+ };
2619
+ }
2620
+ /** Get case IDs matching filter pattern */
2621
+ async function getFilteredCases(matrix, casePattern) {
2622
+ if (!casePattern) return void 0;
2623
+ const caseIds = matrix.casesModule ? (await loadCasesModule(matrix.casesModule)).cases : matrix.cases;
2624
+ if (!caseIds) return ["default"];
2625
+ const filtered = caseIds.filter((id) => matchPattern(id, casePattern));
2626
+ if (filtered.length === 0) throw new Error(`No cases match filter: "${casePattern}"`);
2627
+ return filtered;
2628
+ }
2629
+ /** Get variant IDs matching filter pattern */
2630
+ async function getFilteredVariants(matrix, variantPattern) {
2631
+ if (!variantPattern) return void 0;
2632
+ if (matrix.variants) {
2633
+ const ids = Object.keys(matrix.variants).filter((id) => matchPattern(id, variantPattern));
2634
+ if (ids.length === 0) throw new Error(`No variants match filter: "${variantPattern}"`);
2635
+ return ids;
2636
+ }
2637
+ if (matrix.variantDir) {
2638
+ const filtered = (await discoverVariants(matrix.variantDir)).filter((id) => matchPattern(id, variantPattern));
2639
+ if (filtered.length === 0) throw new Error(`No variants match filter: "${variantPattern}"`);
2640
+ return filtered;
2641
+ }
2642
+ throw new Error("BenchMatrix requires 'variants' or 'variantDir'");
2643
+ }
2644
+ /** Match id against pattern (case-insensitive substring) */
2645
+ function matchPattern(id, pattern) {
2646
+ return id.toLowerCase().includes(pattern.toLowerCase());
2647
+ }
2648
+
2649
+ //#endregion
2650
+ //#region src/table-util/ConvergenceFormatters.ts
2651
+ const { red } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? { red: (str) => str } : pico;
2652
+ const lowConfidence = 80;
2653
+ /** @return convergence percentage with color for low values */
2654
+ function formatConvergence(v) {
2655
+ if (typeof v !== "number") return "—";
2656
+ const pct = `${Math.round(v)}%`;
2657
+ return v < lowConfidence ? red(pct) : pct;
2658
+ }
2659
+
2660
+ //#endregion
2661
+ //#region src/StandardSections.ts
2662
+ /** Section: mean, p50, p99 timing with convergence */
2663
+ const timeSection = {
2664
+ extract: (results) => ({
2665
+ mean: results.time?.avg,
2666
+ p50: results.time?.p50,
2667
+ p99: results.time?.p99,
2668
+ convergence: results.convergence?.confidence
2669
+ }),
2670
+ columns: () => [{
2671
+ groupTitle: "time",
2672
+ columns: [
2673
+ {
2674
+ key: "mean",
2675
+ title: "mean",
2676
+ formatter: timeMs,
2677
+ comparable: true
2678
+ },
2679
+ {
2680
+ key: "p50",
2681
+ title: "p50",
2682
+ formatter: timeMs,
2683
+ comparable: true
2684
+ },
2685
+ {
2686
+ key: "p99",
2687
+ title: "p99",
2688
+ formatter: timeMs,
2689
+ comparable: true
2690
+ }
2691
+ ]
2692
+ }, { columns: [{
2693
+ key: "convergence",
2694
+ title: "conv%",
2695
+ formatter: formatConvergence
2696
+ }] }]
2697
+ };
2698
+ /** Section: GC time as fraction of total benchmark time (Node performance hooks) */
2699
+ const gcSection = {
2700
+ extract: (results) => {
2701
+ const { nodeGcTime, time, samples } = results;
2702
+ if (!nodeGcTime || !time?.avg) return { gc: void 0 };
2703
+ const totalBenchTime = time.avg * samples.length;
2704
+ if (totalBenchTime <= 0) return { gc: void 0 };
2705
+ const gcTime = nodeGcTime.inRun / totalBenchTime;
2706
+ return { gc: gcTime <= 1 ? gcTime : void 0 };
2707
+ },
2708
+ columns: () => [{
2709
+ groupTitle: "gc",
2710
+ columns: [{
2711
+ key: "gc",
2712
+ title: "mean",
2713
+ formatter: percent,
2714
+ comparable: true
2715
+ }]
2716
+ }]
2717
+ };
2718
+ /** Section: detailed GC stats from --trace-gc-nvp (allocation, promotion, pauses) */
2719
+ const gcStatsSection = {
2720
+ extract: (results) => {
2721
+ const { gcStats, samples } = results;
2722
+ if (!gcStats) return {};
2723
+ const iterations = samples.length || 1;
2724
+ const { totalAllocated, totalPromoted } = gcStats;
2725
+ const promoPercent = totalAllocated && totalAllocated > 0 ? (totalPromoted ?? 0) / totalAllocated : void 0;
2726
+ return {
2727
+ allocPerIter: totalAllocated != null ? totalAllocated / iterations : void 0,
2728
+ collected: gcStats.totalCollected || void 0,
2729
+ scavenges: gcStats.scavenges,
2730
+ fullGCs: gcStats.markCompacts,
2731
+ promoPercent,
2732
+ pausePerIter: gcStats.gcPauseTime / iterations
2733
+ };
2734
+ },
2735
+ columns: () => [{
2736
+ groupTitle: "gc",
2737
+ columns: [
2738
+ {
2739
+ key: "allocPerIter",
2740
+ title: "alloc/iter",
2741
+ formatter: formatBytes
2742
+ },
2743
+ {
2744
+ key: "collected",
2745
+ title: "collected",
2746
+ formatter: formatBytes
2747
+ },
2748
+ {
2749
+ key: "scavenges",
2750
+ title: "scav",
2751
+ formatter: integer
2752
+ },
2753
+ {
2754
+ key: "fullGCs",
2755
+ title: "full",
2756
+ formatter: integer
2757
+ },
2758
+ {
2759
+ key: "promoPercent",
2760
+ title: "promo%",
2761
+ formatter: percent
2762
+ },
2763
+ {
2764
+ key: "pausePerIter",
2765
+ title: "pause/iter",
2766
+ formatter: timeMs
2767
+ }
2768
+ ]
2769
+ }]
2770
+ };
2771
+ /** Browser GC section: only fields available from CDP tracing */
2772
+ const browserGcStatsSection = {
2773
+ extract: gcStatsSection.extract,
2774
+ columns: () => [{
2775
+ groupTitle: "gc",
2776
+ columns: [
2777
+ {
2778
+ key: "collected",
2779
+ title: "collected",
2780
+ formatter: formatBytes
2781
+ },
2782
+ {
2783
+ key: "scavenges",
2784
+ title: "scav",
2785
+ formatter: integer
2786
+ },
2787
+ {
2788
+ key: "fullGCs",
2789
+ title: "full",
2790
+ formatter: integer
2791
+ },
2792
+ {
2793
+ key: "pausePerIter",
2794
+ title: "pause",
2795
+ formatter: timeMs
2796
+ }
2797
+ ]
2798
+ }]
2799
+ };
2800
+ /** Section: CPU L1 cache miss rate and stall rate (requires @mitata/counters) */
2801
+ const cpuSection = {
2802
+ extract: (results) => ({
2803
+ cpuCacheMiss: results.cpuCacheMiss,
2804
+ cpuStall: results.cpuStall
2805
+ }),
2806
+ columns: () => [{
2807
+ groupTitle: "cpu",
2808
+ columns: [{
2809
+ key: "cpuCacheMiss",
2810
+ title: "L1 miss",
2811
+ formatter: percent
2812
+ }, {
2813
+ key: "cpuStall",
2814
+ title: "stalls",
2815
+ formatter: percentPrecision(2)
2816
+ }]
2817
+ }]
2818
+ };
2819
+ /** Section: number of sample iterations */
2820
+ const runsSection = {
2821
+ extract: (results) => ({ runs: results.samples.length }),
2822
+ columns: () => [{ columns: [{
2823
+ key: "runs",
2824
+ title: "runs",
2825
+ formatter: integer
2826
+ }] }]
2827
+ };
2828
+ /** Section: total sampling duration in seconds (brackets if >= 30s) */
2829
+ const totalTimeSection = {
2830
+ extract: (results) => ({ totalTime: results.totalTime }),
2831
+ columns: () => [{ columns: [{
2832
+ key: "totalTime",
2833
+ title: "time",
2834
+ formatter: (v) => {
2835
+ if (typeof v !== "number") return "";
2836
+ return v >= 30 ? `[${v.toFixed(1)}s]` : `${v.toFixed(1)}s`;
2837
+ }
2838
+ }] }]
2839
+ };
2840
+ /** Section: median, mean, p99, and convergence for adaptive mode */
2841
+ const adaptiveSection = {
2842
+ extract: (results) => ({
2843
+ median: results.time?.p50,
2844
+ mean: results.time?.avg,
2845
+ p99: results.time?.p99,
2846
+ convergence: results.convergence?.confidence
2847
+ }),
2848
+ columns: () => [{
2849
+ groupTitle: "time",
2850
+ columns: [
2851
+ {
2852
+ key: "median",
2853
+ title: "median",
2854
+ formatter: timeMs,
2855
+ comparable: true
2856
+ },
2857
+ {
2858
+ key: "mean",
2859
+ title: "mean",
2860
+ formatter: timeMs,
2861
+ comparable: true
2862
+ },
2863
+ {
2864
+ key: "p99",
2865
+ title: "p99",
2866
+ formatter: timeMs
2867
+ }
2868
+ ]
2869
+ }, { columns: [{
2870
+ key: "convergence",
2871
+ title: "conv%",
2872
+ formatter: formatConvergence
2873
+ }] }]
2874
+ };
2875
+ /** Build generic sections based on CLI flags */
2876
+ function buildGenericSections(args) {
2877
+ const sections = [];
2878
+ if (args["gc-stats"]) sections.push(gcStatsSection);
2879
+ sections.push(runsSection);
2880
+ return sections;
2881
+ }
2882
+ /** Section: V8 optimization tier distribution and deopt count */
2883
+ const optSection = {
2884
+ extract: (results) => {
2885
+ const opt = results.optStatus;
2886
+ if (!opt) return {};
2887
+ const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
2888
+ return {
2889
+ tiers: Object.entries(opt.byTier).sort((a, b) => b[1].count - a[1].count).map(([name, t]) => `${name}:${(t.count / total * 100).toFixed(0)}%`).join(" "),
2890
+ deopt: opt.deoptCount > 0 ? opt.deoptCount : void 0
2891
+ };
2892
+ },
2893
+ columns: () => [{
2894
+ groupTitle: "v8 opt",
2895
+ columns: [{
2896
+ key: "tiers",
2897
+ title: "tiers",
2898
+ formatter: (v) => typeof v === "string" ? v : ""
2899
+ }, {
2900
+ key: "deopt",
2901
+ title: "deopt",
2902
+ formatter: (v) => typeof v === "number" ? String(v) : ""
2903
+ }]
2904
+ }]
2905
+ };
2906
+
2907
+ //#endregion
2908
+ //#region src/matrix/MatrixReport.ts
2909
+ /** Format matrix results as one table per case */
2910
+ function reportMatrixResults(results, options) {
2911
+ const tables = buildCaseTables(results, options);
2912
+ return [`Matrix: ${results.name}`, ...tables].join("\n\n");
2913
+ }
2914
+ /** Build one table for each case showing all variants */
2915
+ function buildCaseTables(results, options) {
2916
+ if (results.variants.length === 0) return [];
2917
+ return results.variants[0].cases.map((c) => c.caseId).map((caseId) => buildCaseTable(results, caseId, options));
2918
+ }
2919
+ /** Build table for a single case showing all variants */
2920
+ function buildCaseTable(results, caseId, options) {
2921
+ const caseTitle = formatCaseTitle(results, caseId);
2922
+ if (options?.sections?.length) return buildSectionTable(results, caseId, options, caseTitle);
2923
+ const rows = buildCaseRows(results, caseId, options?.extraColumns);
2924
+ return `${caseTitle}\n${buildTable(buildColumns(rows.some((r) => r.diffCI), options), [{ results: rows }])}`;
2925
+ }
2926
+ /** Build table using ResultsMapper sections */
2927
+ function buildSectionTable(results, caseId, options, caseTitle) {
2928
+ const sections = options.sections;
2929
+ const variantTitle = options.variantTitle ?? "name";
2930
+ const rows = [];
2931
+ let hasBaseline = false;
2932
+ for (const variant of results.variants) {
2933
+ const caseResult = variant.cases.find((c) => c.caseId === caseId);
2934
+ if (!caseResult) continue;
2935
+ const row = { name: truncate(variant.id, 25) };
2936
+ for (const section of sections) Object.assign(row, section.extract(caseResult.measured, caseResult.metadata));
2937
+ if (caseResult.baseline) {
2938
+ hasBaseline = true;
2939
+ const { samples: base } = caseResult.baseline;
2940
+ row.diffCI = bootstrapDifferenceCI(base, caseResult.measured.samples);
2941
+ }
2942
+ rows.push(row);
2943
+ }
2944
+ return `${caseTitle}\n${buildTable(buildSectionColumns(sections, variantTitle, hasBaseline), [{ results: rows }])}`;
2945
+ }
2946
+ /** Build column groups from ResultsMapper sections */
2947
+ function buildSectionColumns(sections, variantTitle, hasBaseline) {
2948
+ const nameCol = { columns: [{
2949
+ key: "name",
2950
+ title: variantTitle
2951
+ }] };
2952
+ const sectionColumns = sections.flatMap((s) => s.columns());
2953
+ return [nameCol, ...hasBaseline ? injectDiffColumns(sectionColumns) : sectionColumns];
2954
+ }
2955
+ /** Build rows for all variants for a given case */
2956
+ function buildCaseRows(results, caseId, extraColumns) {
2957
+ return results.variants.flatMap((variant) => {
2958
+ const caseResult = variant.cases.find((c) => c.caseId === caseId);
2959
+ return caseResult ? [buildRow(variant.id, caseResult, extraColumns)] : [];
2960
+ });
2961
+ }
2962
+ /** Build a single row from case result */
2963
+ function buildRow(variantId, caseResult, extraColumns) {
2964
+ const { measured, baseline } = caseResult;
2965
+ const samples = measured.samples;
2966
+ const time = measured.time?.avg ?? average(samples);
2967
+ const row = {
2968
+ name: truncate(variantId, 25),
2969
+ time,
2970
+ samples: samples.length
2971
+ };
2972
+ if (baseline) row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
2973
+ if (extraColumns) for (const col of extraColumns) row[col.key] = col.extract(caseResult);
2974
+ return row;
2975
+ }
2976
+ /** Build column configuration */
2977
+ function buildColumns(hasBaseline, options) {
2978
+ const groups = [{ columns: [{
2979
+ key: "name",
2980
+ title: options?.variantTitle ?? "variant"
2981
+ }] }, { columns: [{
2982
+ key: "time",
2983
+ title: "time",
2984
+ formatter: duration
2985
+ }, ...hasBaseline ? [{
2986
+ key: "diffCI",
2987
+ title: "Δ% CI",
2988
+ formatter: formatDiff
2989
+ }] : []] }];
2990
+ const extraColumns = options?.extraColumns;
2991
+ if (extraColumns?.length) {
2992
+ const byGroup = /* @__PURE__ */ new Map();
2993
+ for (const col of extraColumns) {
2994
+ const group = byGroup.get(col.groupTitle) ?? [];
2995
+ group.push(col);
2996
+ byGroup.set(col.groupTitle, group);
2997
+ }
2998
+ for (const [groupTitle, cols] of byGroup) groups.push({
2999
+ groupTitle,
3000
+ columns: cols.map((col) => ({
3001
+ key: col.key,
3002
+ title: col.title,
3003
+ formatter: col.formatter ?? String
3004
+ }))
3005
+ });
3006
+ }
3007
+ return groups;
3008
+ }
3009
+ /** Format diff with CI, or "baseline" marker */
3010
+ function formatDiff(value) {
3011
+ if (!value) return null;
3012
+ return formatDiffWithCI(value);
3013
+ }
3014
+ /** Format case title with metadata if available */
3015
+ function formatCaseTitle(results, caseId) {
3016
+ const metadata = (results.variants[0]?.cases.find((c) => c.caseId === caseId))?.metadata;
3017
+ if (metadata && Object.keys(metadata).length > 0) return `${caseId} (${Object.entries(metadata).map(([k, v]) => `${v} ${k}`).join(", ")})`;
3018
+ return caseId;
3019
+ }
3020
+ /** GC statistics columns - derived from gcStatsSection for consistency */
3021
+ const gcStatsColumns = gcStatsSection.columns()[0].columns.map((col) => ({
3022
+ key: col.key,
3023
+ title: col.title,
3024
+ groupTitle: "GC",
3025
+ extract: (r) => gcStatsSection.extract(r.measured)[col.key],
3026
+ formatter: (v) => col.formatter?.(v) ?? "-"
3027
+ }));
3028
+ /** Format bytes with fallback to "-" for missing values */
3029
+ function formatBytesOrDash(value) {
3030
+ return formatBytes(value) ?? "-";
3031
+ }
3032
+ /** GC pause time column */
3033
+ const gcPauseColumn = {
3034
+ key: "gcPause",
3035
+ title: "pause",
3036
+ groupTitle: "GC",
3037
+ extract: (r) => r.measured.gcStats?.gcPauseTime,
3038
+ formatter: (v) => v != null ? `${v.toFixed(1)}ms` : "-"
3039
+ };
3040
+ /** Heap sampling total bytes column */
3041
+ const heapTotalColumn = {
3042
+ key: "heapTotal",
3043
+ title: "heap",
3044
+ extract: (r) => {
3045
+ const profile = r.measured.heapProfile;
3046
+ if (!profile?.head) return void 0;
3047
+ return totalProfileBytes(profile);
3048
+ },
3049
+ formatter: formatBytesOrDash
3050
+ };
3051
+
3052
+ //#endregion
3053
+ //#region src/cli/FilterBenchmarks.ts
3054
+ /** Filter benchmarks by name pattern */
3055
+ function filterBenchmarks(suite, filter, removeEmpty = true) {
3056
+ if (!filter) return suite;
3057
+ const regex = createFilterRegex(filter);
3058
+ const groups = suite.groups.map((group) => ({
3059
+ ...group,
3060
+ benchmarks: group.benchmarks.filter((bench) => regex.test(stripCaseSuffix(bench.name))),
3061
+ baseline: group.baseline && regex.test(stripCaseSuffix(group.baseline.name)) ? group.baseline : void 0
3062
+ })).filter((group) => !removeEmpty || group.benchmarks.length > 0);
3063
+ validateFilteredSuite(groups, filter);
3064
+ return {
3065
+ name: suite.name,
3066
+ groups
3067
+ };
3068
+ }
3069
+ /** Create regex from filter (literal unless regex-like) */
3070
+ function createFilterRegex(filter) {
3071
+ if (filter.startsWith("/") && filter.endsWith("/") || filter.includes("*") || filter.includes("?") || filter.includes("[") || filter.includes("|") || filter.startsWith("^") || filter.endsWith("$")) {
3072
+ const pattern = filter.startsWith("/") && filter.endsWith("/") ? filter.slice(1, -1) : filter;
3073
+ try {
3074
+ return new RegExp(pattern, "i");
3075
+ } catch {
3076
+ return new RegExp(escapeRegex(filter), "i");
3077
+ }
3078
+ }
3079
+ return new RegExp("^" + escapeRegex(filter), "i");
3080
+ }
3081
+ /** Strip case suffix like " [large]" from benchmark name for filtering */
3082
+ function stripCaseSuffix(name) {
3083
+ return name.replace(/ \[.*?\]$/, "");
3084
+ }
3085
+ /** Escape regex special characters */
3086
+ function escapeRegex(str) {
3087
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3088
+ }
3089
+ /** Ensure at least one benchmark matches filter */
3090
+ function validateFilteredSuite(groups, filter) {
3091
+ if (groups.every((g) => g.benchmarks.length === 0)) throw new Error(`No benchmarks match filter: "${filter}"`);
3092
+ }
3093
+
3094
+ //#endregion
3095
+ //#region src/cli/RunBenchCLI.ts
3096
+ /** Validate CLI argument combinations */
3097
+ function validateArgs(args) {
3098
+ if (args["gc-stats"] && !args.worker && !args.url) throw new Error("--gc-stats requires worker mode (the default). Remove --no-worker flag.");
3099
+ }
3100
+ /** Warn about Node-only flags that are ignored in browser mode. */
3101
+ function warnBrowserFlags(args) {
3102
+ const ignored = [];
3103
+ if (!args.worker) ignored.push("--no-worker");
3104
+ if (args.cpu) ignored.push("--cpu");
3105
+ if (args["trace-opt"]) ignored.push("--trace-opt");
3106
+ if (args.collect) ignored.push("--collect");
3107
+ if (args.adaptive) ignored.push("--adaptive");
3108
+ if (args.batches > 1) ignored.push("--batches");
3109
+ if (ignored.length) console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
3110
+ }
3111
+ /** Parse CLI with custom configuration */
3112
+ function parseBenchArgs(configureArgs) {
3113
+ return parseCliArgs(hideBin(process.argv), configureArgs);
3114
+ }
3115
+ /** Run suite with CLI arguments */
3116
+ async function runBenchmarks(suite, args) {
3117
+ validateArgs(args);
3118
+ const { filter, worker: useWorker, batches = 1 } = args;
3119
+ const options = cliToRunnerOptions(args);
3120
+ return runSuite({
3121
+ suite: filterBenchmarks(suite, filter),
3122
+ runner: "basic",
3123
+ options,
3124
+ useWorker,
3125
+ batches
3126
+ });
3127
+ }
3128
+ /** Execute all groups in suite */
3129
+ async function runSuite(params) {
3130
+ const { suite, runner, options, useWorker, batches } = params;
3131
+ const results = [];
3132
+ for (const group of suite.groups) results.push(await runGroup(group, runner, options, useWorker, batches));
3133
+ return results;
3134
+ }
3135
+ /** Execute group with shared setup, optionally batching to reduce ordering bias */
3136
+ async function runGroup(group, runner, options, useWorker, batches = 1) {
3137
+ const { name, benchmarks, baseline, setup, metadata } = group;
3138
+ const setupParams = await setup?.();
3139
+ validateBenchmarkParameters(group);
3140
+ const runParams = {
3141
+ runner,
3142
+ options,
3143
+ useWorker,
3144
+ params: setupParams,
3145
+ metadata
3146
+ };
3147
+ if (batches === 1) return runSingleBatch(name, benchmarks, baseline, runParams);
3148
+ return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
3149
+ }
3150
+ /** Run benchmarks in a single batch */
3151
+ async function runSingleBatch(name, benchmarks, baseline, runParams) {
3152
+ const baselineReport = baseline ? await runSingleBenchmark(baseline, runParams) : void 0;
3153
+ return {
3154
+ name,
3155
+ reports: await serialMap(benchmarks, (b) => runSingleBenchmark(b, runParams)),
3156
+ baseline: baselineReport
3157
+ };
3158
+ }
3159
+ /** Run benchmarks in multiple batches, alternating order to reduce bias */
3160
+ async function runMultipleBatches(name, benchmarks, baseline, runParams, batches) {
3161
+ const timePerBatch = (runParams.options.maxTime || 5e3) / batches;
3162
+ const batchParams = {
3163
+ ...runParams,
3164
+ options: {
3165
+ ...runParams.options,
3166
+ maxTime: timePerBatch
3167
+ }
3168
+ };
3169
+ const baselineBatches = [];
3170
+ const benchmarkBatches = /* @__PURE__ */ new Map();
3171
+ for (let i = 0; i < batches; i++) await runBatchIteration(benchmarks, baseline, batchParams, i % 2 === 1, baselineBatches, benchmarkBatches);
3172
+ const meta = runParams.metadata;
3173
+ return mergeBatchResults(name, benchmarks, baseline, baselineBatches, benchmarkBatches, meta);
3174
+ }
3175
+ /** Run one batch iteration in either order */
3176
+ async function runBatchIteration(benchmarks, baseline, runParams, reverseOrder, baselineBatches, benchmarkBatches) {
3177
+ const runBaseline = async () => {
3178
+ if (baseline) {
3179
+ const r = await runSingleBenchmark(baseline, runParams);
3180
+ baselineBatches.push(r.measuredResults);
3181
+ }
3182
+ };
3183
+ const runBenches = async () => {
3184
+ for (const b of benchmarks) {
3185
+ const r = await runSingleBenchmark(b, runParams);
3186
+ appendToMap(benchmarkBatches, b.name, r.measuredResults);
3187
+ }
3188
+ };
3189
+ if (reverseOrder) {
3190
+ await runBenches();
3191
+ await runBaseline();
3192
+ } else {
3193
+ await runBaseline();
3194
+ await runBenches();
3195
+ }
3196
+ }
3197
+ /** Merge batch results into final ReportGroup */
3198
+ function mergeBatchResults(name, benchmarks, baseline, baselineBatches, benchmarkBatches, metadata) {
3199
+ const mergedBaseline = baseline ? {
3200
+ name: baseline.name,
3201
+ measuredResults: mergeResults(baselineBatches),
3202
+ metadata
3203
+ } : void 0;
3204
+ return {
3205
+ name,
3206
+ reports: benchmarks.map((b) => ({
3207
+ name: b.name,
3208
+ measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
3209
+ metadata
3210
+ })),
3211
+ baseline: mergedBaseline
3212
+ };
3213
+ }
3214
+ /** Run single benchmark and create report */
3215
+ async function runSingleBenchmark(spec, runParams) {
3216
+ const { runner, options, useWorker, params, metadata } = runParams;
3217
+ const [result] = await runBenchmark({
3218
+ spec,
3219
+ runner,
3220
+ options,
3221
+ useWorker,
3222
+ params
3223
+ });
3224
+ return {
3225
+ name: spec.name,
3226
+ measuredResults: result,
3227
+ metadata
3228
+ };
3229
+ }
3230
+ /** Warn if parameterized benchmarks lack setup */
3231
+ function validateBenchmarkParameters(group) {
3232
+ const { name, setup, benchmarks, baseline } = group;
3233
+ if (setup) return;
3234
+ const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
3235
+ for (const benchmark of allBenchmarks) if (benchmark.fn.length > 0) console.warn(`Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`);
3236
+ }
3237
+ /** Merge multiple batch results into a single MeasuredResults */
3238
+ function mergeResults(results) {
3239
+ if (results.length === 0) throw new Error("Cannot merge empty results array");
3240
+ if (results.length === 1) return results[0];
3241
+ const allSamples = results.flatMap((r) => r.samples);
3242
+ const allWarmup = results.flatMap((r) => r.warmupSamples || []);
3243
+ const time = computeStats(allSamples);
3244
+ const convergence = checkConvergence(allSamples.map((s) => s * msToNs));
3245
+ let offset = 0;
3246
+ const allPausePoints = results.flatMap((r) => {
3247
+ const pts = (r.pausePoints ?? []).map((p) => ({
3248
+ sampleIndex: p.sampleIndex + offset,
3249
+ durationMs: p.durationMs
3250
+ }));
3251
+ offset += r.samples.length;
3252
+ return pts;
3253
+ });
3254
+ return {
3255
+ name: results[0].name,
3256
+ samples: allSamples,
3257
+ warmupSamples: allWarmup.length ? allWarmup : void 0,
3258
+ time,
3259
+ totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
3260
+ convergence,
3261
+ pausePoints: allPausePoints.length ? allPausePoints : void 0
3262
+ };
3263
+ }
3264
+ function appendToMap(map, key, value) {
3265
+ if (!map.has(key)) map.set(key, []);
3266
+ map.get(key).push(value);
3267
+ }
3268
+ /** Generate table with standard sections */
3269
+ function defaultReport(groups, args) {
3270
+ const { adaptive, "gc-stats": gcStats, "trace-opt": traceOpt } = args;
3271
+ const hasCpu = hasField(groups, "cpu");
3272
+ const hasOpt = hasField(groups, "optStatus");
3273
+ return reportResults(groups, buildReportSections(adaptive, gcStats, hasCpu, traceOpt && hasOpt));
3274
+ }
3275
+ /** Build report sections based on CLI options */
3276
+ function buildReportSections(adaptive, gcStats, hasCpuData, hasOptData) {
3277
+ const sections = adaptive ? [
3278
+ adaptiveSection,
3279
+ runsSection,
3280
+ totalTimeSection
3281
+ ] : [timeSection, runsSection];
3282
+ if (gcStats) sections.push(gcStatsSection);
3283
+ if (hasCpuData) sections.push(cpuSection);
3284
+ if (hasOptData) sections.push(optSection);
3285
+ return sections;
3286
+ }
3287
+ /** Run benchmarks, display table, and optionally generate HTML report */
3288
+ async function benchExports(suite, args) {
3289
+ const results = await runBenchmarks(suite, args);
3290
+ const report = defaultReport(results, args);
3291
+ console.log(report);
3292
+ await finishReports(results, args, suite.name);
3293
+ }
3294
+ /** Run browser profiling via Playwright + CDP, report with standard pipeline */
3295
+ async function browserBenchExports(args) {
3296
+ warnBrowserFlags(args);
3297
+ const url = args.url;
3298
+ const { iterations, time } = args;
3299
+ const result = await profileBrowser({
3300
+ url,
3301
+ heapSample: args["heap-sample"],
3302
+ heapOptions: {
3303
+ samplingInterval: args["heap-interval"],
3304
+ stackDepth: args["heap-depth"]
3305
+ },
3306
+ headless: args.headless,
3307
+ timeout: args.timeout,
3308
+ gcStats: args["gc-stats"],
3309
+ maxTime: iterations ? Number.MAX_SAFE_INTEGER : time * 1e3,
3310
+ maxIterations: iterations
3311
+ });
3312
+ const name = new URL(url).pathname.split("/").pop() || "browser";
3313
+ const hasSamples = result.samples && result.samples.length > 0;
3314
+ const results = browserResultGroups(name, result);
3315
+ if (hasSamples || result.wallTimeMs != null) console.log(reportResults(results, [timeSection, runsSection]));
3316
+ if (result.gcStats) console.log(reportResults(results, [browserGcStatsSection]));
3317
+ if (result.heapProfile) printHeapReports(results, {
3318
+ ...cliHeapReportOptions(args),
3319
+ isUserCode: isBrowserUserCode
3320
+ });
3321
+ await exportReports({
3322
+ results,
3323
+ args
3324
+ });
3325
+ }
3326
+ /** Wrap browser profile result as ReportGroup[] for the standard pipeline */
3327
+ function browserResultGroups(name, result) {
3328
+ const { gcStats, heapProfile } = result;
3329
+ let measured;
3330
+ if (result.samples && result.samples.length > 0) {
3331
+ const { samples } = result;
3332
+ const totalTime = result.wallTimeMs ? result.wallTimeMs / 1e3 : void 0;
3333
+ measured = {
3334
+ name,
3335
+ samples,
3336
+ time: computeStats(samples),
3337
+ totalTime,
3338
+ gcStats,
3339
+ heapProfile
3340
+ };
3341
+ } else {
3342
+ const wallMs = result.wallTimeMs ?? 0;
3343
+ measured = {
3344
+ name,
3345
+ samples: [wallMs],
3346
+ time: {
3347
+ min: wallMs,
3348
+ max: wallMs,
3349
+ avg: wallMs,
3350
+ p50: wallMs,
3351
+ p75: wallMs,
3352
+ p99: wallMs,
3353
+ p999: wallMs
3354
+ },
3355
+ gcStats,
3356
+ heapProfile
3357
+ };
3358
+ }
3359
+ return [{
3360
+ name,
3361
+ reports: [{
3362
+ name,
3363
+ measuredResults: measured
3364
+ }]
3365
+ }];
3366
+ }
3367
+ /** Print heap allocation reports for benchmarks with heap profiles */
3368
+ function printHeapReports(groups, options) {
3369
+ for (const group of groups) {
3370
+ const allReports = group.baseline ? [...group.reports, group.baseline] : group.reports;
3371
+ for (const report of allReports) {
3372
+ const { heapProfile } = report.measuredResults;
3373
+ if (!heapProfile) continue;
3374
+ console.log(dim(`\n─── Heap profile: ${report.name} ───`));
3375
+ const totalAll = totalProfileBytes(heapProfile);
3376
+ const sites = flattenProfile(heapProfile);
3377
+ const userSites = filterSites(sites, options.isUserCode);
3378
+ const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
3379
+ const aggregated = aggregateSites(options.userOnly ? userSites : sites);
3380
+ const extra = {
3381
+ totalAll,
3382
+ totalUserCode,
3383
+ sampleCount: heapProfile.samples?.length
3384
+ };
3385
+ console.log(formatHeapReport(aggregated, {
3386
+ ...options,
3387
+ ...extra
3388
+ }));
3389
+ }
3390
+ }
3391
+ }
3392
+ /** Run benchmarks and display table. Suite is optional with --url (browser mode). */
3393
+ async function runDefaultBench(suite, configureArgs) {
3394
+ const args = parseBenchArgs(configureArgs);
3395
+ if (args.url) await browserBenchExports(args);
3396
+ else if (suite) await benchExports(suite, args);
3397
+ else throw new Error("Either --url or a BenchSuite is required.");
3398
+ }
3399
+ /** Convert CLI args to runner options */
3400
+ function cliToRunnerOptions(args) {
3401
+ const { profile, collect, iterations } = args;
3402
+ if (profile) return {
3403
+ maxIterations: iterations ?? 1,
3404
+ warmupTime: 0,
3405
+ collect
3406
+ };
3407
+ if (args.adaptive) return createAdaptiveOptions(args);
3408
+ return {
3409
+ maxTime: iterations ? Number.POSITIVE_INFINITY : args.time * 1e3,
3410
+ maxIterations: iterations,
3411
+ ...cliCommonOptions(args)
3412
+ };
3413
+ }
3414
+ /** Create options for adaptive mode */
3415
+ function createAdaptiveOptions(args) {
3416
+ return {
3417
+ minTime: (args["min-time"] ?? 1) * 1e3,
3418
+ maxTime: defaultAdaptiveMaxTime * 1e3,
3419
+ targetConfidence: args.convergence,
3420
+ adaptive: true,
3421
+ ...cliCommonOptions(args)
3422
+ };
3423
+ }
3424
+ /** Runner/matrix options shared across all CLI modes */
3425
+ function cliCommonOptions(args) {
3426
+ const { collect, cpu, warmup } = args;
3427
+ const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
3428
+ const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
3429
+ const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
3430
+ const { "heap-sample": heapSample, "heap-interval": heapInterval } = args;
3431
+ const { "heap-depth": heapDepth } = args;
3432
+ return {
3433
+ collect,
3434
+ cpuCounters: cpu,
3435
+ warmup,
3436
+ traceOpt,
3437
+ noSettle,
3438
+ pauseFirst,
3439
+ pauseInterval,
3440
+ pauseDuration,
3441
+ gcStats,
3442
+ heapSample,
3443
+ heapInterval,
3444
+ heapDepth
3445
+ };
3446
+ }
3447
+ const { yellow, dim } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? {
3448
+ yellow: (s) => s,
3449
+ dim: (s) => s
3450
+ } : pico;
3451
+ /** Log V8 optimization tier distribution and deoptimizations */
3452
+ function reportOptStatus(groups) {
3453
+ const optData = groups.flatMap(({ reports, baseline }) => {
3454
+ return (baseline ? [...reports, baseline] : reports).filter((r) => r.measuredResults.optStatus).map((r) => ({
3455
+ name: r.name,
3456
+ opt: r.measuredResults.optStatus,
3457
+ samples: r.measuredResults.samples.length
3458
+ }));
3459
+ });
3460
+ if (optData.length === 0) return;
3461
+ console.log(dim("\nV8 optimization:"));
3462
+ for (const { name, opt, samples } of optData) {
3463
+ const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
3464
+ const tierParts = Object.entries(opt.byTier).sort((a, b) => b[1].count - a[1].count).map(([tier, info]) => `${tier} ${(info.count / total * 100).toFixed(0)}%`).join(", ");
3465
+ console.log(` ${name}: ${tierParts} ${dim(`(${samples} samples)`)}`);
3466
+ }
3467
+ const totalDeopts = optData.reduce((s, d) => s + d.opt.deoptCount, 0);
3468
+ if (totalDeopts > 0) console.log(yellow(` ⚠ ${totalDeopts} deoptimization${totalDeopts > 1 ? "s" : ""} detected`));
3469
+ }
3470
+ /** @return true if any result has the specified field with a defined value */
3471
+ function hasField(results, field) {
3472
+ return results.some(({ reports, baseline }) => {
3473
+ return (baseline ? [...reports, baseline] : reports).some(({ measuredResults }) => measuredResults[field] !== void 0);
3474
+ });
3475
+ }
3476
+ /** Print heap reports (if enabled) and export results */
3477
+ async function finishReports(results, args, suiteName, exportOptions) {
3478
+ if (args["heap-sample"]) printHeapReports(results, cliHeapReportOptions(args));
3479
+ await exportReports({
3480
+ results,
3481
+ args,
3482
+ suiteName,
3483
+ ...exportOptions
3484
+ });
3485
+ }
3486
+ /** Export reports (HTML, JSON, Perfetto) based on CLI args */
3487
+ async function exportReports(options) {
3488
+ const { results, args, sections, suiteName } = options;
3489
+ const { currentVersion, baselineVersion } = options;
3490
+ const openInBrowser = args.html && !args["export-html"];
3491
+ let closeServer;
3492
+ if (args.html || args["export-html"]) closeServer = (await generateHtmlReport(prepareHtmlData(results, {
3493
+ cliArgs: args,
3494
+ sections,
3495
+ currentVersion,
3496
+ baselineVersion
3497
+ }), {
3498
+ openBrowser: openInBrowser,
3499
+ outputPath: args["export-html"]
3500
+ })).closeServer;
3501
+ if (args.json) await exportBenchmarkJson(results, args.json, args, suiteName);
3502
+ if (args.perfetto) exportPerfettoTrace(results, args.perfetto, args);
3503
+ if (openInBrowser) {
3504
+ await waitForCtrlC();
3505
+ closeServer?.();
3506
+ }
3507
+ }
3508
+ /** Wait for Ctrl+C before exiting */
3509
+ function waitForCtrlC() {
3510
+ return new Promise((resolve) => {
3511
+ console.log(dim("\nPress Ctrl+C to exit"));
3512
+ process.on("SIGINT", () => {
3513
+ console.log();
3514
+ resolve();
3515
+ });
3516
+ });
3517
+ }
3518
+ /** Run matrix suite with CLI arguments.
3519
+ * no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
3520
+ * --all --filter ==> subset of all, --all ==> all cases/variants */
3521
+ async function runMatrixSuite(suite, args) {
3522
+ validateArgs(args);
3523
+ const filter = args.filter ? parseMatrixFilter(args.filter) : void 0;
3524
+ const options = cliToMatrixOptions(args);
3525
+ const results = [];
3526
+ for (const matrix of suite.matrices) {
3527
+ const casesModule = matrix.casesModule ? await loadCasesModule(matrix.casesModule) : void 0;
3528
+ let filtered = matrix;
3529
+ if (!args.all && casesModule) filtered = {
3530
+ ...matrix,
3531
+ filteredCases: casesModule.defaultCases,
3532
+ filteredVariants: casesModule.defaultVariants
3533
+ };
3534
+ if (filter) filtered = await filterMatrix(filtered, filter);
3535
+ const { filteredCases, filteredVariants } = filtered;
3536
+ results.push(await runMatrix(filtered, {
3537
+ ...options,
3538
+ filteredCases,
3539
+ filteredVariants
3540
+ }));
3541
+ }
3542
+ return results;
3543
+ }
3544
+ /** Convert CLI args to matrix run options */
3545
+ function cliToMatrixOptions(args) {
3546
+ const { time, iterations, worker } = args;
3547
+ return {
3548
+ iterations,
3549
+ maxTime: iterations ? void 0 : time * 1e3,
3550
+ useWorker: worker,
3551
+ ...cliCommonOptions(args)
3552
+ };
3553
+ }
3554
+ /** Generate report for matrix results. Uses same sections as regular benchmarks. */
3555
+ function defaultMatrixReport(results, reportOptions, args) {
3556
+ const options = args ? mergeMatrixDefaults(reportOptions, args, results) : reportOptions;
3557
+ return results.map((r) => reportMatrixResults(r, options)).join("\n\n");
3558
+ }
3559
+ /** @return HeapReportOptions from CLI args */
3560
+ function cliHeapReportOptions(args) {
3561
+ return {
3562
+ topN: args["heap-rows"],
3563
+ stackDepth: args["heap-stack"],
3564
+ verbose: args["heap-verbose"],
3565
+ userOnly: args["heap-user-only"]
3566
+ };
3567
+ }
3568
+ /** Apply default sections and extra columns for matrix reports */
3569
+ function mergeMatrixDefaults(reportOptions, args, results) {
3570
+ const result = { ...reportOptions };
3571
+ if (!result.sections?.length) {
3572
+ const groups = matrixToReportGroups(results);
3573
+ result.sections = buildReportSections(args.adaptive, args["gc-stats"], hasField(groups, "cpu"), args["trace-opt"] && hasField(groups, "optStatus"));
3574
+ }
3575
+ return result;
3576
+ }
3577
+ /** Run matrix suite with full CLI handling (parse, run, report, export) */
3578
+ async function runDefaultMatrixBench(suite, configureArgs, reportOptions) {
3579
+ await matrixBenchExports(suite, parseBenchArgs(configureArgs), reportOptions);
3580
+ }
3581
+ /** Convert MatrixResults to ReportGroup[] for export compatibility */
3582
+ function matrixToReportGroups(results) {
3583
+ return results.flatMap((matrix) => matrix.variants.flatMap((variant) => variant.cases.map((c) => {
3584
+ const { metadata } = c;
3585
+ const report = {
3586
+ name: variant.id,
3587
+ measuredResults: c.measured,
3588
+ metadata
3589
+ };
3590
+ const baseline = c.baseline ? {
3591
+ name: `${variant.id} (baseline)`,
3592
+ measuredResults: c.baseline,
3593
+ metadata
3594
+ } : void 0;
3595
+ return {
3596
+ name: `${variant.id} / ${c.caseId}`,
3597
+ reports: [report],
3598
+ baseline
3599
+ };
3600
+ })));
3601
+ }
3602
+ /** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
3603
+ async function serialMap(arr, fn) {
3604
+ const results = [];
3605
+ for (const item of arr) results.push(await fn(item));
3606
+ return results;
3607
+ }
3608
+ /** Run matrix benchmarks, display table, and generate exports */
3609
+ async function matrixBenchExports(suite, args, reportOptions, exportOptions) {
3610
+ const results = await runMatrixSuite(suite, args);
3611
+ const report = defaultMatrixReport(results, reportOptions, args);
3612
+ console.log(report);
3613
+ await finishReports(matrixToReportGroups(results), args, suite.name, exportOptions);
3614
+ }
3615
+
3616
+ //#endregion
3617
+ //#region src/GitUtils.ts
3618
+ /** Get current git version info. For dirty repos, uses most recent modified file date. */
3619
+ function getCurrentGitVersion() {
3620
+ try {
3621
+ const exec = (cmd) => execSync(cmd, { encoding: "utf-8" }).trim();
3622
+ const hash = exec("git rev-parse --short HEAD");
3623
+ const commitDate = exec("git log -1 --format=%aI");
3624
+ const dirty = exec("git status --porcelain").length > 0;
3625
+ return {
3626
+ hash,
3627
+ date: dirty ? getMostRecentModifiedDate(".") ?? commitDate : commitDate,
3628
+ dirty
3629
+ };
3630
+ } catch {
3631
+ return;
3632
+ }
3633
+ }
3634
+ /** Read baseline version from .baseline-version file */
3635
+ function getBaselineVersion(baselineDir = "_baseline") {
3636
+ const versionFile = join(baselineDir, ".baseline-version");
3637
+ if (!existsSync(versionFile)) return void 0;
3638
+ try {
3639
+ const content = readFileSync(versionFile, "utf-8");
3640
+ const data = JSON.parse(content);
3641
+ return {
3642
+ hash: data.hash,
3643
+ date: data.date
3644
+ };
3645
+ } catch {
3646
+ return;
3647
+ }
3648
+ }
3649
+ /** Format git version for display: "abc1234 (Jan 9, 2026, 3:45 PM)" or "abc1234*" if dirty */
3650
+ function formatGitVersion(version) {
3651
+ return `${version.dirty ? `${version.hash}*` : version.hash} (${formatDateWithTimezone(version.date)})`;
3652
+ }
3653
+ /** Get most recent modified file date in a directory (for dirty repos) */
3654
+ function getMostRecentModifiedDate(dir) {
3655
+ try {
3656
+ const modifiedFiles = execSync("git status --porcelain", {
3657
+ encoding: "utf-8",
3658
+ cwd: dir
3659
+ }).trim().split("\n").filter((line) => line.length > 0).map((line) => line.slice(3));
3660
+ if (modifiedFiles.length === 0) return void 0;
3661
+ let mostRecent = 0;
3662
+ for (const file of modifiedFiles) try {
3663
+ const filePath = join(dir, file);
3664
+ if (!existsSync(filePath)) continue;
3665
+ const mtime = statSync(filePath).mtimeMs;
3666
+ if (mtime > mostRecent) mostRecent = mtime;
3667
+ } catch {}
3668
+ return mostRecent > 0 ? new Date(mostRecent).toISOString() : void 0;
3669
+ } catch {
3670
+ return;
3671
+ }
3672
+ }
3673
+
3674
+ //#endregion
3675
+ export { timeSection as A, parseCliArgs as B, adaptiveSection as C, gcStatsSection as D, gcSection as E, generateHtmlReport as F, truncate as G, formatBytes as H, formatDateWithTimezone as I, average as J, isStatefulVariant as K, prepareHtmlData as L, formatConvergence as M, filterMatrix as N, optSection as O, parseMatrixFilter as P, exportPerfettoTrace as R, reportMatrixResults as S, cpuSection as T, integer as U, reportResults as V, timeMs as W, loadCasesModule as X, loadCaseData as Y, runDefaultMatrixBench as _, cliToMatrixOptions as a, gcStatsColumns as b, exportReports as c, matrixToReportGroups as d, parseBenchArgs as f, runDefaultBench as g, runBenchmarks as h, benchExports as i, totalTimeSection as j, runsSection as k, hasField as l, reportOptStatus as m, getBaselineVersion as n, defaultMatrixReport as o, printHeapReports as p, runMatrix as q, getCurrentGitVersion as r, defaultReport as s, formatGitVersion as t, matrixBenchExports as u, runMatrixSuite as v, buildGenericSections as w, heapTotalColumn as x, gcPauseColumn as y, defaultCliArgs as z };
3676
+ //# sourceMappingURL=src-CGuaC3Wo.mjs.map