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,856 @@
1
+ import pico from "picocolors";
2
+ import { hideBin } from "yargs/helpers";
3
+ import type {
4
+ MatrixResults,
5
+ MatrixSuite,
6
+ RunMatrixOptions,
7
+ } from "../BenchMatrix.ts";
8
+ import { runMatrix } from "../BenchMatrix.ts";
9
+ import type { BenchGroup, BenchmarkSpec, BenchSuite } from "../Benchmark.ts";
10
+ import type { BenchmarkReport, ReportGroup } from "../BenchmarkReport.ts";
11
+ import { reportResults } from "../BenchmarkReport.ts";
12
+ import {
13
+ type BrowserProfileResult,
14
+ profileBrowser,
15
+ } from "../browser/BrowserHeapSampler.ts";
16
+ import { exportBenchmarkJson } from "../export/JsonExport.ts";
17
+ import { exportPerfettoTrace } from "../export/PerfettoExport.ts";
18
+ import type { GitVersion } from "../GitUtils.ts";
19
+ import { prepareHtmlData } from "../HtmlDataPrep.ts";
20
+ import {
21
+ aggregateSites,
22
+ filterSites,
23
+ flattenProfile,
24
+ formatHeapReport,
25
+ type HeapReportOptions,
26
+ isBrowserUserCode,
27
+ totalProfileBytes,
28
+ } from "../heap-sample/HeapSampleReport.ts";
29
+ import { generateHtmlReport } from "../html/index.ts";
30
+ import type { MeasuredResults } from "../MeasuredResults.ts";
31
+ import { loadCasesModule } from "../matrix/CaseLoader.ts";
32
+ import {
33
+ type FilteredMatrix,
34
+ filterMatrix,
35
+ parseMatrixFilter,
36
+ } from "../matrix/MatrixFilter.ts";
37
+ import {
38
+ type MatrixReportOptions,
39
+ reportMatrixResults,
40
+ } from "../matrix/MatrixReport.ts";
41
+ import { checkConvergence } from "../runners/AdaptiveWrapper.ts";
42
+ import { computeStats } from "../runners/BasicRunner.ts";
43
+ import type { RunnerOptions } from "../runners/BenchRunner.ts";
44
+ import type { KnownRunner } from "../runners/CreateRunner.ts";
45
+ import { runBenchmark } from "../runners/RunnerOrchestrator.ts";
46
+ import { msToNs } from "../runners/RunnerUtils.ts";
47
+ import {
48
+ adaptiveSection,
49
+ browserGcStatsSection,
50
+ cpuSection,
51
+ gcStatsSection,
52
+ optSection,
53
+ runsSection,
54
+ timeSection,
55
+ totalTimeSection,
56
+ } from "../StandardSections.ts";
57
+ import {
58
+ type Configure,
59
+ type DefaultCliArgs,
60
+ defaultAdaptiveMaxTime,
61
+ parseCliArgs,
62
+ } from "./CliArgs.ts";
63
+ import { filterBenchmarks } from "./FilterBenchmarks.ts";
64
+
65
+ /** Validate CLI argument combinations */
66
+ function validateArgs(args: DefaultCliArgs): void {
67
+ if (args["gc-stats"] && !args.worker && !args.url) {
68
+ throw new Error(
69
+ "--gc-stats requires worker mode (the default). Remove --no-worker flag.",
70
+ );
71
+ }
72
+ }
73
+
74
+ /** Warn about Node-only flags that are ignored in browser mode. */
75
+ function warnBrowserFlags(args: DefaultCliArgs): void {
76
+ const ignored: string[] = [];
77
+ if (!args.worker) ignored.push("--no-worker");
78
+ if (args.cpu) ignored.push("--cpu");
79
+ if (args["trace-opt"]) ignored.push("--trace-opt");
80
+ if (args.collect) ignored.push("--collect");
81
+ if (args.adaptive) ignored.push("--adaptive");
82
+ if (args.batches > 1) ignored.push("--batches");
83
+ if (ignored.length) {
84
+ console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
85
+ }
86
+ }
87
+
88
+ type RunParams = {
89
+ runner: KnownRunner;
90
+ options: RunnerOptions;
91
+ useWorker: boolean;
92
+ params: unknown;
93
+ metadata?: Record<string, any>;
94
+ };
95
+
96
+ type SuiteParams = {
97
+ runner: KnownRunner;
98
+ options: RunnerOptions;
99
+ useWorker: boolean;
100
+ suite: BenchSuite;
101
+ batches: number;
102
+ };
103
+
104
+ /** Parse CLI with custom configuration */
105
+ export function parseBenchArgs<T = DefaultCliArgs>(
106
+ configureArgs?: Configure<T>,
107
+ ): T & DefaultCliArgs {
108
+ const argv = hideBin(process.argv);
109
+ return parseCliArgs(argv, configureArgs) as T & DefaultCliArgs;
110
+ }
111
+
112
+ /** Run suite with CLI arguments */
113
+ export async function runBenchmarks(
114
+ suite: BenchSuite,
115
+ args: DefaultCliArgs,
116
+ ): Promise<ReportGroup[]> {
117
+ validateArgs(args);
118
+ const { filter, worker: useWorker, batches = 1 } = args;
119
+ const options = cliToRunnerOptions(args);
120
+ const filtered = filterBenchmarks(suite, filter);
121
+
122
+ return runSuite({
123
+ suite: filtered,
124
+ runner: "basic",
125
+ options,
126
+ useWorker,
127
+ batches,
128
+ });
129
+ }
130
+
131
+ /** Execute all groups in suite */
132
+ async function runSuite(params: SuiteParams): Promise<ReportGroup[]> {
133
+ const { suite, runner, options, useWorker, batches } = params;
134
+ const results: ReportGroup[] = [];
135
+ for (const group of suite.groups) {
136
+ results.push(await runGroup(group, runner, options, useWorker, batches));
137
+ }
138
+ return results;
139
+ }
140
+
141
+ /** Execute group with shared setup, optionally batching to reduce ordering bias */
142
+ async function runGroup(
143
+ group: BenchGroup,
144
+ runner: KnownRunner,
145
+ options: RunnerOptions,
146
+ useWorker: boolean,
147
+ batches = 1,
148
+ ): Promise<ReportGroup> {
149
+ const { name, benchmarks, baseline, setup, metadata } = group;
150
+ const setupParams = await setup?.();
151
+ validateBenchmarkParameters(group);
152
+
153
+ const runParams = {
154
+ runner,
155
+ options,
156
+ useWorker,
157
+ params: setupParams,
158
+ metadata,
159
+ };
160
+ if (batches === 1) {
161
+ return runSingleBatch(name, benchmarks, baseline, runParams);
162
+ }
163
+ return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
164
+ }
165
+
166
+ /** Run benchmarks in a single batch */
167
+ async function runSingleBatch(
168
+ name: string,
169
+ benchmarks: BenchmarkSpec[],
170
+ baseline: BenchmarkSpec | undefined,
171
+ runParams: RunParams,
172
+ ): Promise<ReportGroup> {
173
+ const baselineReport = baseline
174
+ ? await runSingleBenchmark(baseline, runParams)
175
+ : undefined;
176
+ const reports = await serialMap(benchmarks, b =>
177
+ runSingleBenchmark(b, runParams),
178
+ );
179
+ return { name, reports, baseline: baselineReport };
180
+ }
181
+
182
+ /** Run benchmarks in multiple batches, alternating order to reduce bias */
183
+ async function runMultipleBatches(
184
+ name: string,
185
+ benchmarks: BenchmarkSpec[],
186
+ baseline: BenchmarkSpec | undefined,
187
+ runParams: RunParams,
188
+ batches: number,
189
+ ): Promise<ReportGroup> {
190
+ const timePerBatch = (runParams.options.maxTime || 5000) / batches;
191
+ const batchParams = {
192
+ ...runParams,
193
+ options: { ...runParams.options, maxTime: timePerBatch },
194
+ };
195
+ const baselineBatches: MeasuredResults[] = [];
196
+ const benchmarkBatches = new Map<string, MeasuredResults[]>();
197
+
198
+ for (let i = 0; i < batches; i++) {
199
+ const reverseOrder = i % 2 === 1;
200
+ await runBatchIteration(
201
+ benchmarks,
202
+ baseline,
203
+ batchParams,
204
+ reverseOrder,
205
+ baselineBatches,
206
+ benchmarkBatches,
207
+ );
208
+ }
209
+
210
+ const meta = runParams.metadata;
211
+ return mergeBatchResults(
212
+ name,
213
+ benchmarks,
214
+ baseline,
215
+ baselineBatches,
216
+ benchmarkBatches,
217
+ meta,
218
+ );
219
+ }
220
+
221
+ /** Run one batch iteration in either order */
222
+ async function runBatchIteration(
223
+ benchmarks: BenchmarkSpec[],
224
+ baseline: BenchmarkSpec | undefined,
225
+ runParams: RunParams,
226
+ reverseOrder: boolean,
227
+ baselineBatches: MeasuredResults[],
228
+ benchmarkBatches: Map<string, MeasuredResults[]>,
229
+ ): Promise<void> {
230
+ const runBaseline = async () => {
231
+ if (baseline) {
232
+ const r = await runSingleBenchmark(baseline, runParams);
233
+ baselineBatches.push(r.measuredResults);
234
+ }
235
+ };
236
+ const runBenches = async () => {
237
+ for (const b of benchmarks) {
238
+ const r = await runSingleBenchmark(b, runParams);
239
+ appendToMap(benchmarkBatches, b.name, r.measuredResults);
240
+ }
241
+ };
242
+
243
+ if (reverseOrder) {
244
+ await runBenches();
245
+ await runBaseline();
246
+ } else {
247
+ await runBaseline();
248
+ await runBenches();
249
+ }
250
+ }
251
+
252
+ /** Merge batch results into final ReportGroup */
253
+ function mergeBatchResults(
254
+ name: string,
255
+ benchmarks: BenchmarkSpec[],
256
+ baseline: BenchmarkSpec | undefined,
257
+ baselineBatches: MeasuredResults[],
258
+ benchmarkBatches: Map<string, MeasuredResults[]>,
259
+ metadata?: Record<string, unknown>,
260
+ ): ReportGroup {
261
+ const mergedBaseline = baseline
262
+ ? {
263
+ name: baseline.name,
264
+ measuredResults: mergeResults(baselineBatches),
265
+ metadata,
266
+ }
267
+ : undefined;
268
+ const reports = benchmarks.map(b => ({
269
+ name: b.name,
270
+ measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
271
+ metadata,
272
+ }));
273
+ return { name, reports, baseline: mergedBaseline };
274
+ }
275
+
276
+ /** Run single benchmark and create report */
277
+ async function runSingleBenchmark(
278
+ spec: BenchmarkSpec,
279
+ runParams: RunParams,
280
+ ): Promise<BenchmarkReport> {
281
+ const { runner, options, useWorker, params, metadata } = runParams;
282
+ const benchmarkParams = { spec, runner, options, useWorker, params };
283
+ const [result] = await runBenchmark(benchmarkParams);
284
+ return { name: spec.name, measuredResults: result, metadata };
285
+ }
286
+
287
+ /** Warn if parameterized benchmarks lack setup */
288
+ function validateBenchmarkParameters(group: BenchGroup): void {
289
+ const { name, setup, benchmarks, baseline } = group;
290
+ if (setup) return;
291
+
292
+ const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
293
+ for (const benchmark of allBenchmarks) {
294
+ if (benchmark.fn.length > 0) {
295
+ console.warn(
296
+ `Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`,
297
+ );
298
+ }
299
+ }
300
+ }
301
+
302
+ /** Merge multiple batch results into a single MeasuredResults */
303
+ function mergeResults(results: MeasuredResults[]): MeasuredResults {
304
+ if (results.length === 0) {
305
+ throw new Error("Cannot merge empty results array");
306
+ }
307
+ if (results.length === 1) return results[0];
308
+
309
+ const allSamples = results.flatMap(r => r.samples);
310
+ const allWarmup = results.flatMap(r => r.warmupSamples || []);
311
+ const time = computeStats(allSamples);
312
+ const convergence = checkConvergence(allSamples.map(s => s * msToNs));
313
+
314
+ let offset = 0;
315
+ const allPausePoints = results.flatMap(r => {
316
+ const pts = (r.pausePoints ?? []).map(p => ({
317
+ sampleIndex: p.sampleIndex + offset,
318
+ durationMs: p.durationMs,
319
+ }));
320
+ offset += r.samples.length;
321
+ return pts;
322
+ });
323
+
324
+ return {
325
+ name: results[0].name,
326
+ samples: allSamples,
327
+ warmupSamples: allWarmup.length ? allWarmup : undefined,
328
+ time,
329
+ totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
330
+ convergence,
331
+ pausePoints: allPausePoints.length ? allPausePoints : undefined,
332
+ };
333
+ }
334
+
335
+ function appendToMap(
336
+ map: Map<string, MeasuredResults[]>,
337
+ key: string,
338
+ value: MeasuredResults,
339
+ ) {
340
+ if (!map.has(key)) map.set(key, []);
341
+ map.get(key)!.push(value);
342
+ }
343
+
344
+ /** Generate table with standard sections */
345
+ export function defaultReport(
346
+ groups: ReportGroup[],
347
+ args: DefaultCliArgs,
348
+ ): string {
349
+ const { adaptive, "gc-stats": gcStats, "trace-opt": traceOpt } = args;
350
+ const hasCpu = hasField(groups, "cpu");
351
+ const hasOpt = hasField(groups, "optStatus");
352
+ const sections = buildReportSections(
353
+ adaptive,
354
+ gcStats,
355
+ hasCpu,
356
+ traceOpt && hasOpt,
357
+ );
358
+ return reportResults(groups, sections);
359
+ }
360
+
361
+ /** Build report sections based on CLI options */
362
+ function buildReportSections(
363
+ adaptive: boolean,
364
+ gcStats: boolean,
365
+ hasCpuData: boolean,
366
+ hasOptData: boolean,
367
+ ) {
368
+ const sections = adaptive
369
+ ? [adaptiveSection, runsSection, totalTimeSection]
370
+ : [timeSection, runsSection];
371
+
372
+ if (gcStats) sections.push(gcStatsSection);
373
+ if (hasCpuData) sections.push(cpuSection);
374
+ if (hasOptData) sections.push(optSection);
375
+
376
+ return sections;
377
+ }
378
+
379
+ /** Run benchmarks, display table, and optionally generate HTML report */
380
+ export async function benchExports(
381
+ suite: BenchSuite,
382
+ args: DefaultCliArgs,
383
+ ): Promise<void> {
384
+ const results = await runBenchmarks(suite, args);
385
+ const report = defaultReport(results, args);
386
+ console.log(report);
387
+ await finishReports(results, args, suite.name);
388
+ }
389
+
390
+ /** Run browser profiling via Playwright + CDP, report with standard pipeline */
391
+ export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
392
+ warnBrowserFlags(args);
393
+ const url = args.url!;
394
+ const { iterations, time } = args;
395
+ const result = await profileBrowser({
396
+ url,
397
+ heapSample: args["heap-sample"],
398
+ heapOptions: {
399
+ samplingInterval: args["heap-interval"],
400
+ stackDepth: args["heap-depth"],
401
+ },
402
+ headless: args.headless,
403
+ timeout: args.timeout,
404
+ gcStats: args["gc-stats"],
405
+ maxTime: iterations ? Number.MAX_SAFE_INTEGER : time * 1000,
406
+ maxIterations: iterations,
407
+ });
408
+
409
+ const name = new URL(url).pathname.split("/").pop() || "browser";
410
+ const hasSamples = result.samples && result.samples.length > 0;
411
+ const results = browserResultGroups(name, result);
412
+
413
+ // Time report
414
+ if (hasSamples || result.wallTimeMs != null) {
415
+ console.log(reportResults(results, [timeSection, runsSection]));
416
+ }
417
+
418
+ // GC stats table
419
+ if (result.gcStats) {
420
+ console.log(reportResults(results, [browserGcStatsSection]));
421
+ }
422
+
423
+ // Heap allocation report
424
+ if (result.heapProfile) {
425
+ printHeapReports(results, {
426
+ ...cliHeapReportOptions(args),
427
+ isUserCode: isBrowserUserCode,
428
+ });
429
+ }
430
+
431
+ await exportReports({ results, args });
432
+ }
433
+
434
+ /** Wrap browser profile result as ReportGroup[] for the standard pipeline */
435
+ function browserResultGroups(
436
+ name: string,
437
+ result: BrowserProfileResult,
438
+ ): ReportGroup[] {
439
+ const { gcStats, heapProfile } = result;
440
+ let measured: MeasuredResults;
441
+
442
+ // Bench function mode: multiple timing samples with real statistics
443
+ if (result.samples && result.samples.length > 0) {
444
+ const { samples } = result;
445
+ const totalTime = result.wallTimeMs ? result.wallTimeMs / 1000 : undefined;
446
+ measured = {
447
+ name,
448
+ samples,
449
+ time: computeStats(samples),
450
+ totalTime,
451
+ gcStats,
452
+ heapProfile,
453
+ };
454
+ } else {
455
+ // Lap mode: 0 laps = single wall-clock, N laps handled above
456
+ const wallMs = result.wallTimeMs ?? 0;
457
+ const time = {
458
+ min: wallMs,
459
+ max: wallMs,
460
+ avg: wallMs,
461
+ p50: wallMs,
462
+ p75: wallMs,
463
+ p99: wallMs,
464
+ p999: wallMs,
465
+ };
466
+ measured = { name, samples: [wallMs], time, gcStats, heapProfile };
467
+ }
468
+
469
+ return [{ name, reports: [{ name, measuredResults: measured }] }];
470
+ }
471
+
472
+ /** Print heap allocation reports for benchmarks with heap profiles */
473
+ export function printHeapReports(
474
+ groups: ReportGroup[],
475
+ options: HeapReportOptions,
476
+ ): void {
477
+ for (const group of groups) {
478
+ const allReports = group.baseline
479
+ ? [...group.reports, group.baseline]
480
+ : group.reports;
481
+
482
+ for (const report of allReports) {
483
+ const { heapProfile } = report.measuredResults;
484
+ if (!heapProfile) continue;
485
+
486
+ console.log(dim(`\n─── Heap profile: ${report.name} ───`));
487
+ const totalAll = totalProfileBytes(heapProfile);
488
+ const sites = flattenProfile(heapProfile);
489
+ const userSites = filterSites(sites, options.isUserCode);
490
+ const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
491
+ const aggregated = aggregateSites(options.userOnly ? userSites : sites);
492
+ const extra = {
493
+ totalAll,
494
+ totalUserCode,
495
+ sampleCount: heapProfile.samples?.length,
496
+ };
497
+ console.log(formatHeapReport(aggregated, { ...options, ...extra }));
498
+ }
499
+ }
500
+ }
501
+
502
+ /** Run benchmarks and display table. Suite is optional with --url (browser mode). */
503
+ export async function runDefaultBench(
504
+ suite?: BenchSuite,
505
+ configureArgs?: Configure<any>,
506
+ ): Promise<void> {
507
+ const args = parseBenchArgs(configureArgs);
508
+ if (args.url) {
509
+ await browserBenchExports(args);
510
+ } else if (suite) {
511
+ await benchExports(suite, args);
512
+ } else {
513
+ throw new Error("Either --url or a BenchSuite is required.");
514
+ }
515
+ }
516
+
517
+ /** Convert CLI args to runner options */
518
+ export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
519
+ const { profile, collect, iterations } = args;
520
+ if (profile)
521
+ return { maxIterations: iterations ?? 1, warmupTime: 0, collect };
522
+ if (args.adaptive) return createAdaptiveOptions(args);
523
+
524
+ return {
525
+ maxTime: iterations ? Number.POSITIVE_INFINITY : args.time * 1000,
526
+ maxIterations: iterations,
527
+ ...cliCommonOptions(args),
528
+ };
529
+ }
530
+
531
+ /** Create options for adaptive mode */
532
+ function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
533
+ return {
534
+ minTime: (args["min-time"] ?? 1) * 1000,
535
+ maxTime: defaultAdaptiveMaxTime * 1000,
536
+ targetConfidence: args.convergence,
537
+ adaptive: true,
538
+ ...cliCommonOptions(args),
539
+ } as any;
540
+ }
541
+
542
+ /** Runner/matrix options shared across all CLI modes */
543
+ function cliCommonOptions(args: DefaultCliArgs) {
544
+ const { collect, cpu, warmup } = args;
545
+ const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
546
+ const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
547
+ const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
548
+ const { "heap-sample": heapSample, "heap-interval": heapInterval } = args;
549
+ const { "heap-depth": heapDepth } = args;
550
+ return {
551
+ collect,
552
+ cpuCounters: cpu,
553
+ warmup,
554
+ traceOpt,
555
+ noSettle,
556
+ pauseFirst,
557
+ pauseInterval,
558
+ pauseDuration,
559
+ gcStats,
560
+ heapSample,
561
+ heapInterval,
562
+ heapDepth,
563
+ };
564
+ }
565
+
566
+ const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
567
+ const { yellow, dim } = isTest
568
+ ? { yellow: (s: string) => s, dim: (s: string) => s }
569
+ : pico;
570
+
571
+ /** Log V8 optimization tier distribution and deoptimizations */
572
+ export function reportOptStatus(groups: ReportGroup[]): void {
573
+ const optData = groups.flatMap(({ reports, baseline }) => {
574
+ const all = baseline ? [...reports, baseline] : reports;
575
+ return all
576
+ .filter(r => r.measuredResults.optStatus)
577
+ .map(r => ({
578
+ name: r.name,
579
+ opt: r.measuredResults.optStatus!,
580
+ samples: r.measuredResults.samples.length,
581
+ }));
582
+ });
583
+ if (optData.length === 0) return;
584
+
585
+ console.log(dim("\nV8 optimization:"));
586
+ for (const { name, opt, samples } of optData) {
587
+ const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
588
+ const tierParts = Object.entries(opt.byTier)
589
+ .sort((a, b) => b[1].count - a[1].count)
590
+ .map(
591
+ ([tier, info]) => `${tier} ${((info.count / total) * 100).toFixed(0)}%`,
592
+ )
593
+ .join(", ");
594
+ console.log(` ${name}: ${tierParts} ${dim(`(${samples} samples)`)}`);
595
+ }
596
+
597
+ const totalDeopts = optData.reduce((s, d) => s + d.opt.deoptCount, 0);
598
+ if (totalDeopts > 0) {
599
+ console.log(
600
+ yellow(
601
+ ` ⚠ ${totalDeopts} deoptimization${totalDeopts > 1 ? "s" : ""} detected`,
602
+ ),
603
+ );
604
+ }
605
+ }
606
+
607
+ /** @return true if any result has the specified field with a defined value */
608
+ export function hasField(
609
+ results: ReportGroup[],
610
+ field: keyof MeasuredResults,
611
+ ): boolean {
612
+ return results.some(({ reports, baseline }) => {
613
+ const all = baseline ? [...reports, baseline] : reports;
614
+ return all.some(
615
+ ({ measuredResults }) => measuredResults[field] !== undefined,
616
+ );
617
+ });
618
+ }
619
+
620
+ export interface ExportOptions {
621
+ results: ReportGroup[];
622
+ args: DefaultCliArgs;
623
+ sections?: any[];
624
+ suiteName?: string;
625
+ currentVersion?: GitVersion;
626
+ baselineVersion?: GitVersion;
627
+ }
628
+
629
+ /** Print heap reports (if enabled) and export results */
630
+ async function finishReports(
631
+ results: ReportGroup[],
632
+ args: DefaultCliArgs,
633
+ suiteName?: string,
634
+ exportOptions?: MatrixExportOptions,
635
+ ): Promise<void> {
636
+ if (args["heap-sample"]) {
637
+ printHeapReports(results, cliHeapReportOptions(args));
638
+ }
639
+ await exportReports({ results, args, suiteName, ...exportOptions });
640
+ }
641
+
642
+ /** Export reports (HTML, JSON, Perfetto) based on CLI args */
643
+ export async function exportReports(options: ExportOptions): Promise<void> {
644
+ const { results, args, sections, suiteName } = options;
645
+ const { currentVersion, baselineVersion } = options;
646
+ const openInBrowser = args.html && !args["export-html"];
647
+ let closeServer: (() => void) | undefined;
648
+
649
+ if (args.html || args["export-html"]) {
650
+ const htmlOpts = {
651
+ cliArgs: args,
652
+ sections,
653
+ currentVersion,
654
+ baselineVersion,
655
+ };
656
+ const reportData = prepareHtmlData(results, htmlOpts);
657
+ const result = await generateHtmlReport(reportData, {
658
+ openBrowser: openInBrowser,
659
+ outputPath: args["export-html"],
660
+ });
661
+ closeServer = result.closeServer;
662
+ }
663
+
664
+ if (args.json) {
665
+ await exportBenchmarkJson(results, args.json, args, suiteName);
666
+ }
667
+
668
+ if (args.perfetto) {
669
+ exportPerfettoTrace(results, args.perfetto, args);
670
+ }
671
+
672
+ // Keep process running when HTML report is opened in browser
673
+ if (openInBrowser) {
674
+ await waitForCtrlC();
675
+ closeServer?.();
676
+ }
677
+ }
678
+
679
+ /** Wait for Ctrl+C before exiting */
680
+ function waitForCtrlC(): Promise<void> {
681
+ return new Promise(resolve => {
682
+ console.log(dim("\nPress Ctrl+C to exit"));
683
+ process.on("SIGINT", () => {
684
+ console.log();
685
+ resolve();
686
+ });
687
+ });
688
+ }
689
+
690
+ /** Run matrix suite with CLI arguments.
691
+ * no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
692
+ * --all --filter ==> subset of all, --all ==> all cases/variants */
693
+ export async function runMatrixSuite(
694
+ suite: MatrixSuite,
695
+ args: DefaultCliArgs,
696
+ ): Promise<MatrixResults[]> {
697
+ validateArgs(args);
698
+ const filter = args.filter ? parseMatrixFilter(args.filter) : undefined;
699
+ const options = cliToMatrixOptions(args);
700
+
701
+ const results: MatrixResults[] = [];
702
+ for (const matrix of suite.matrices) {
703
+ const casesModule = matrix.casesModule
704
+ ? await loadCasesModule(matrix.casesModule)
705
+ : undefined;
706
+
707
+ let filtered: FilteredMatrix<any> = matrix;
708
+ if (!args.all && casesModule) {
709
+ filtered = {
710
+ ...matrix,
711
+ filteredCases: casesModule.defaultCases,
712
+ filteredVariants: casesModule.defaultVariants,
713
+ };
714
+ }
715
+
716
+ // filter merges via intersection with defaults
717
+ if (filter) {
718
+ filtered = await filterMatrix(filtered, filter);
719
+ }
720
+
721
+ const { filteredCases, filteredVariants } = filtered;
722
+ results.push(
723
+ await runMatrix(filtered, {
724
+ ...options,
725
+ filteredCases,
726
+ filteredVariants,
727
+ }),
728
+ );
729
+ }
730
+ return results;
731
+ }
732
+
733
+ /** Convert CLI args to matrix run options */
734
+ export function cliToMatrixOptions(args: DefaultCliArgs): RunMatrixOptions {
735
+ const { time, iterations, worker } = args;
736
+ return {
737
+ iterations,
738
+ maxTime: iterations ? undefined : time * 1000,
739
+ useWorker: worker,
740
+ ...cliCommonOptions(args),
741
+ };
742
+ }
743
+
744
+ /** Generate report for matrix results. Uses same sections as regular benchmarks. */
745
+ export function defaultMatrixReport(
746
+ results: MatrixResults[],
747
+ reportOptions?: MatrixReportOptions,
748
+ args?: DefaultCliArgs,
749
+ ): string {
750
+ const options = args
751
+ ? mergeMatrixDefaults(reportOptions, args, results)
752
+ : reportOptions;
753
+ return results.map(r => reportMatrixResults(r, options)).join("\n\n");
754
+ }
755
+
756
+ /** @return HeapReportOptions from CLI args */
757
+ function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
758
+ return {
759
+ topN: args["heap-rows"],
760
+ stackDepth: args["heap-stack"],
761
+ verbose: args["heap-verbose"],
762
+ userOnly: args["heap-user-only"],
763
+ };
764
+ }
765
+
766
+ /** Apply default sections and extra columns for matrix reports */
767
+ function mergeMatrixDefaults(
768
+ reportOptions: MatrixReportOptions | undefined,
769
+ args: DefaultCliArgs,
770
+ results: MatrixResults[],
771
+ ): MatrixReportOptions {
772
+ const result: MatrixReportOptions = { ...reportOptions };
773
+
774
+ if (!result.sections?.length) {
775
+ const groups = matrixToReportGroups(results);
776
+ result.sections = buildReportSections(
777
+ args.adaptive,
778
+ args["gc-stats"],
779
+ hasField(groups, "cpu"),
780
+ args["trace-opt"] && hasField(groups, "optStatus"),
781
+ );
782
+ }
783
+
784
+ return result;
785
+ }
786
+
787
+ /** Run matrix suite with full CLI handling (parse, run, report, export) */
788
+ export async function runDefaultMatrixBench(
789
+ suite: MatrixSuite,
790
+ configureArgs?: Configure<any>,
791
+ reportOptions?: MatrixReportOptions,
792
+ ): Promise<void> {
793
+ const args = parseBenchArgs(configureArgs);
794
+ await matrixBenchExports(suite, args, reportOptions);
795
+ }
796
+
797
+ /** Convert MatrixResults to ReportGroup[] for export compatibility */
798
+ export function matrixToReportGroups(results: MatrixResults[]): ReportGroup[] {
799
+ return results.flatMap(matrix =>
800
+ matrix.variants.flatMap(variant =>
801
+ variant.cases.map(c => {
802
+ const { metadata } = c;
803
+ const report = {
804
+ name: variant.id,
805
+ measuredResults: c.measured,
806
+ metadata,
807
+ };
808
+ const baseline = c.baseline
809
+ ? {
810
+ name: `${variant.id} (baseline)`,
811
+ measuredResults: c.baseline,
812
+ metadata,
813
+ }
814
+ : undefined;
815
+ return {
816
+ name: `${variant.id} / ${c.caseId}`,
817
+ reports: [report],
818
+ baseline,
819
+ };
820
+ }),
821
+ ),
822
+ );
823
+ }
824
+
825
+ export interface MatrixExportOptions {
826
+ sections?: any[];
827
+ currentVersion?: GitVersion;
828
+ baselineVersion?: GitVersion;
829
+ }
830
+
831
+ /** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
832
+ async function serialMap<T, R>(
833
+ arr: T[],
834
+ fn: (item: T) => Promise<R>,
835
+ ): Promise<R[]> {
836
+ const results: R[] = [];
837
+ for (const item of arr) {
838
+ results.push(await fn(item));
839
+ }
840
+ return results;
841
+ }
842
+
843
+ /** Run matrix benchmarks, display table, and generate exports */
844
+ export async function matrixBenchExports(
845
+ suite: MatrixSuite,
846
+ args: DefaultCliArgs,
847
+ reportOptions?: MatrixReportOptions,
848
+ exportOptions?: MatrixExportOptions,
849
+ ): Promise<void> {
850
+ const results = await runMatrixSuite(suite, args);
851
+ const report = defaultMatrixReport(results, reportOptions, args);
852
+ console.log(report);
853
+
854
+ const reportGroups = matrixToReportGroups(results);
855
+ await finishReports(reportGroups, args, suite.name, exportOptions);
856
+ }