benchforge 0.1.11 → 0.2.4

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 (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
@@ -1,393 +1,139 @@
1
1
  import { basename, resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
- import pico from "picocolors";
4
3
  import { hideBin } from "yargs/helpers";
5
- import type {
6
- MatrixResults,
7
- MatrixSuite,
8
- RunMatrixOptions,
9
- } from "../BenchMatrix.ts";
10
- import { runMatrix } from "../BenchMatrix.ts";
11
- import type { BenchGroup, BenchmarkSpec, BenchSuite } from "../Benchmark.ts";
12
- import type {
13
- BenchmarkReport,
14
- ReportGroup,
15
- ResultsMapper,
16
- } from "../BenchmarkReport.ts";
17
- import { groupReports, reportResults } from "../BenchmarkReport.ts";
18
- import type { BrowserProfileResult } from "../browser/BrowserHeapSampler.ts";
19
- import { exportBenchmarkJson } from "../export/JsonExport.ts";
20
- import { exportPerfettoTrace } from "../export/PerfettoExport.ts";
21
- import {
22
- exportAndLaunchSpeedscope,
23
- exportSpeedscope,
24
- } from "../export/SpeedscopeExport.ts";
25
- import type { GitVersion } from "../GitUtils.ts";
26
- import { prepareHtmlData } from "../HtmlDataPrep.ts";
27
- import {
28
- aggregateSites,
29
- filterSites,
30
- flattenProfile,
31
- formatHeapReport,
32
- formatRawSamples,
33
- type HeapReportOptions,
34
- isBrowserUserCode,
35
- } from "../heap-sample/HeapSampleReport.ts";
36
- import { resolveProfile } from "../heap-sample/ResolvedProfile.ts";
37
- import { generateHtmlReport } from "../html/index.ts";
38
- import type { MeasuredResults } from "../MeasuredResults.ts";
4
+ import type { MatrixResults, MatrixSuite } from "../matrix/BenchMatrix.ts";
5
+ import { runMatrix } from "../matrix/BenchMatrix.ts";
39
6
  import { loadCasesModule } from "../matrix/CaseLoader.ts";
40
7
  import {
41
8
  type FilteredMatrix,
42
9
  filterMatrix,
10
+ type MatrixFilter,
43
11
  parseMatrixFilter,
12
+ resolveCaseIds,
13
+ resolveVariantIds,
44
14
  } from "../matrix/MatrixFilter.ts";
45
- import {
46
- type MatrixReportOptions,
47
- reportMatrixResults,
48
- } from "../matrix/MatrixReport.ts";
49
- import { computeStats } from "../runners/BasicRunner.ts";
50
- import type { RunnerOptions } from "../runners/BenchRunner.ts";
51
- import type { KnownRunner } from "../runners/CreateRunner.ts";
52
- import { runBenchmark } from "../runners/RunnerOrchestrator.ts";
53
- import {
54
- adaptiveSection,
55
- browserGcStatsSection,
56
- cpuSection,
57
- gcStatsSection,
58
- optSection,
59
- runsSection,
60
- timeSection,
61
- totalTimeSection,
62
- } from "../StandardSections.ts";
15
+ import type { MatrixReportOptions } from "../matrix/MatrixReport.ts";
16
+ import type { ReportSection } from "../report/BenchmarkReport.ts";
17
+ import type { BenchSuite } from "../runners/BenchmarkSpec.ts";
18
+ import { browserBenchExports } from "./BrowserBench.ts";
63
19
  import {
64
20
  type Configure,
65
21
  type DefaultCliArgs,
66
- defaultAdaptiveMaxTime,
67
22
  parseCliArgs,
68
23
  } from "./CliArgs.ts";
69
- import { filterBenchmarks } from "./FilterBenchmarks.ts";
70
-
71
- export interface ExportOptions {
72
- results: ReportGroup[];
73
- args: DefaultCliArgs;
74
- sections?: any[];
75
- suiteName?: string;
76
- currentVersion?: GitVersion;
77
- baselineVersion?: GitVersion;
78
- }
24
+ import { finishReports, type MatrixExportOptions } from "./CliExport.ts";
25
+ import { cliToMatrixOptions, validateArgs } from "./CliOptions.ts";
26
+ import {
27
+ defaultMatrixReport,
28
+ defaultReport,
29
+ matrixToReportGroups,
30
+ withStatus,
31
+ } from "./CliReport.ts";
32
+ import { runBenchmarks } from "./SuiteRunner.ts";
79
33
 
80
- export interface MatrixExportOptions {
81
- sections?: any[];
82
- currentVersion?: GitVersion;
83
- baselineVersion?: GitVersion;
34
+ /** Options for running a BenchSuite: custom sections replace the CLI-derived defaults. */
35
+ export interface BenchExportsOptions {
36
+ sections?: ReportSection[];
84
37
  }
85
38
 
86
- type RunParams = {
87
- runner: KnownRunner;
88
- options: RunnerOptions;
89
- useWorker: boolean;
90
- params: unknown;
91
- metadata?: Record<string, any>;
92
- };
39
+ /** Top-level CLI dispatch: route to view, analyze, or default bench runner. */
40
+ export async function dispatchCli(): Promise<void> {
41
+ const argv = hideBin(process.argv);
42
+ const [command] = argv;
93
43
 
94
- type SuiteParams = {
95
- runner: KnownRunner;
96
- options: RunnerOptions;
97
- useWorker: boolean;
98
- suite: BenchSuite;
99
- batches: number;
100
- };
44
+ if (command === "view") {
45
+ const { viewArchive } = await import("./ViewerServer.ts");
46
+ return viewArchive(requireFile(argv[1], "view"));
47
+ }
48
+ if (command === "analyze") {
49
+ const { analyzeArchive } = await import("./AnalyzeArchive.ts");
50
+ return analyzeArchive(requireFile(argv[1], "analyze"));
51
+ }
52
+ await runDefaultBench(undefined, undefined, argv);
53
+ }
101
54
 
102
- const isTest = process.env.NODE_ENV === "test" || process.env.VITEST === "true";
103
- const { yellow, dim } = isTest
104
- ? { yellow: (s: string) => s, dim: (s: string) => s }
105
- : pico;
55
+ /** Run benchmarks and display results. Suite is optional with --url (browser mode). */
56
+ export async function runDefaultBench(
57
+ suite?: BenchSuite,
58
+ configureArgs?: Configure<any>,
59
+ argv?: string[],
60
+ opts?: BenchExportsOptions,
61
+ ): Promise<void> {
62
+ const args = parseBenchArgs(configureArgs, argv);
63
+ if (args.url) return browserBenchExports(args);
64
+ if (args.list && suite) return listSuite(suite);
65
+ if (suite) return benchExports(suite, args, opts);
66
+ if (args.file) return fileBenchExports(args.file, args);
67
+ throw new Error(
68
+ "Provide a benchmark file, --url for browser mode, or pass a BenchSuite directly.",
69
+ );
70
+ }
106
71
 
107
- /** Parse CLI with custom configuration */
72
+ /** Parse CLI args with optional custom yargs configuration. */
108
73
  export function parseBenchArgs<T = DefaultCliArgs>(
109
74
  configureArgs?: Configure<T>,
75
+ argv?: string[],
110
76
  ): T & DefaultCliArgs {
111
- const argv = hideBin(process.argv);
112
- return parseCliArgs(argv, configureArgs) as T & DefaultCliArgs;
113
- }
114
-
115
- /** Run suite with CLI arguments */
116
- export async function runBenchmarks(
117
- suite: BenchSuite,
118
- args: DefaultCliArgs,
119
- ): Promise<ReportGroup[]> {
120
- validateArgs(args);
121
- const { filter, worker: useWorker, batches = 1 } = args;
122
- const options = cliToRunnerOptions(args);
123
- const filtered = filterBenchmarks(suite, filter);
124
-
125
- return runSuite({
126
- suite: filtered,
127
- runner: "basic",
128
- options,
129
- useWorker,
130
- batches,
131
- });
132
- }
133
-
134
- /** Generate table with standard sections */
135
- export function defaultReport(
136
- groups: ReportGroup[],
137
- args: DefaultCliArgs,
138
- ): string {
139
- const { adaptive, "gc-stats": gcStats, "trace-opt": traceOpt } = args;
140
- const hasCpu = hasField(groups, "cpu");
141
- const hasOpt = hasField(groups, "optStatus");
142
- const sections = buildReportSections(
143
- adaptive,
144
- gcStats,
145
- hasCpu,
146
- traceOpt && hasOpt,
147
- );
148
- return reportResults(groups, sections);
77
+ const args = argv ?? hideBin(process.argv);
78
+ return parseCliArgs(args, configureArgs) as T & DefaultCliArgs;
149
79
  }
150
80
 
151
- /** Run benchmarks, display table, and optionally generate HTML report */
81
+ /** Run a BenchSuite and print results with standard reporting. */
152
82
  export async function benchExports(
153
83
  suite: BenchSuite,
154
84
  args: DefaultCliArgs,
85
+ opts?: BenchExportsOptions,
155
86
  ): Promise<void> {
156
87
  const results = await runBenchmarks(suite, args);
157
- const report = defaultReport(results, args);
158
- console.log(report);
159
- await finishReports(results, args, suite.name);
160
- }
161
-
162
- /** Run browser profiling via Playwright + CDP, report with standard pipeline */
163
- export async function browserBenchExports(args: DefaultCliArgs): Promise<void> {
164
- warnBrowserFlags(args);
165
-
166
- let profileBrowser: typeof import("../browser/BrowserHeapSampler.ts").profileBrowser;
167
- try {
168
- ({ profileBrowser } = await import("../browser/BrowserHeapSampler.ts"));
169
- } catch {
170
- throw new Error(
171
- "playwright is required for browser benchmarking (--url).\n\n" +
172
- "Quick start: npx benchforge-browser --url <your-url>\n\n" +
173
- "Or install manually:\n" +
174
- " npm install playwright\n" +
175
- " npx playwright install chromium",
176
- );
177
- }
178
-
179
- const url = args.url!;
180
- const { iterations, time } = args;
181
- const result = await profileBrowser({
182
- url,
183
- heapSample: needsHeapSample(args),
184
- heapOptions: {
185
- samplingInterval: args["heap-interval"],
186
- stackDepth: args["heap-depth"],
187
- },
188
- headless: args.headless,
189
- chromeArgs: args["chrome-args"]
190
- ?.flatMap(a => a.split(/\s+/))
191
- .map(stripQuotes)
192
- .filter(Boolean),
193
- timeout: args.timeout,
194
- gcStats: args["gc-stats"],
195
- maxTime: iterations ? Number.MAX_SAFE_INTEGER : time * 1000,
196
- maxIterations: iterations,
197
- });
198
-
199
- const name = new URL(url).pathname.split("/").pop() || "browser";
200
- const results = browserResultGroups(name, result);
201
- printBrowserReport(result, results, args);
202
- await exportReports({ results, args });
203
- }
204
-
205
- /** Print heap allocation reports for benchmarks with heap profiles */
206
- export function printHeapReports(
207
- groups: ReportGroup[],
208
- options: HeapReportOptions,
209
- ): void {
210
- for (const group of groups) {
211
- for (const report of groupReports(group)) {
212
- const { heapProfile } = report.measuredResults;
213
- if (!heapProfile) continue;
214
-
215
- console.log(dim(`\n─── Heap profile: ${report.name} ───`));
216
- const resolved = resolveProfile(heapProfile);
217
- const sites = flattenProfile(resolved);
218
- const userSites = filterSites(sites, options.isUserCode);
219
- const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
220
- const aggregated = aggregateSites(options.userOnly ? userSites : sites);
221
- const extra = {
222
- totalAll: resolved.totalBytes,
223
- totalUserCode,
224
- sampleCount: resolved.sortedSamples?.length,
225
- };
226
- console.log(formatHeapReport(aggregated, { ...options, ...extra }));
227
- if (options.raw) {
228
- console.log(dim(`\n─── Raw samples: ${report.name} ───`));
229
- console.log(formatRawSamples(resolved));
230
- }
231
- }
232
- }
88
+ console.log(
89
+ withStatus("computing report", () => defaultReport(results, args, opts)),
90
+ );
91
+ await finishReports(results, args, opts);
233
92
  }
234
93
 
235
- /** Run benchmarks and display table. Suite is optional with --url (browser mode). */
236
- export async function runDefaultBench(
237
- suite?: BenchSuite,
94
+ /** Run matrix suite with full CLI handling (parse, run, report, export). */
95
+ export async function runDefaultMatrixBench(
96
+ suite: MatrixSuite,
238
97
  configureArgs?: Configure<any>,
98
+ reportOptions?: MatrixReportOptions,
239
99
  ): Promise<void> {
240
100
  const args = parseBenchArgs(configureArgs);
241
- if (args.url) {
242
- await browserBenchExports(args);
243
- } else if (suite) {
244
- await benchExports(suite, args);
245
- } else if (args.file) {
246
- await fileBenchExports(args.file, args);
247
- } else {
248
- throw new Error(
249
- "Provide a benchmark file, --url for browser mode, or pass a BenchSuite directly.",
250
- );
251
- }
252
- }
253
-
254
- /** Convert CLI args to runner options */
255
- export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
256
- const { profile, collect, iterations } = args;
257
- if (profile)
258
- return { maxIterations: iterations ?? 1, warmupTime: 0, collect };
259
- if (args.adaptive) return createAdaptiveOptions(args);
260
-
261
- return {
262
- maxTime: iterations ? Number.POSITIVE_INFINITY : args.time * 1000,
263
- maxIterations: iterations,
264
- ...cliCommonOptions(args),
265
- };
266
- }
267
-
268
- /** Log V8 optimization tier distribution and deoptimizations */
269
- export function reportOptStatus(groups: ReportGroup[]): void {
270
- const optData = groups.flatMap(group => {
271
- return groupReports(group)
272
- .filter(r => r.measuredResults.optStatus)
273
- .map(r => ({
274
- name: r.name,
275
- opt: r.measuredResults.optStatus!,
276
- samples: r.measuredResults.samples.length,
277
- }));
278
- });
279
- if (optData.length === 0) return;
280
-
281
- console.log(dim("\nV8 optimization:"));
282
- for (const { name, opt, samples } of optData) {
283
- const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
284
- const tierParts = Object.entries(opt.byTier)
285
- .sort((a, b) => b[1].count - a[1].count)
286
- .map(
287
- ([tier, info]) => `${tier} ${((info.count / total) * 100).toFixed(0)}%`,
288
- )
289
- .join(", ");
290
- console.log(` ${name}: ${tierParts} ${dim(`(${samples} samples)`)}`);
291
- }
292
-
293
- const totalDeopts = optData.reduce((s, d) => s + d.opt.deoptCount, 0);
294
- if (totalDeopts > 0) {
295
- console.log(
296
- yellow(
297
- ` ⚠ ${totalDeopts} deoptimization${totalDeopts > 1 ? "s" : ""} detected`,
298
- ),
299
- );
300
- }
101
+ await matrixBenchExports(suite, args, reportOptions);
301
102
  }
302
103
 
303
- /** @return true if any result has the specified field with a defined value */
304
- export function hasField(
305
- results: ReportGroup[],
306
- field: keyof MeasuredResults,
307
- ): boolean {
308
- return results.some(group =>
309
- groupReports(group).some(
310
- ({ measuredResults }) => measuredResults[field] !== undefined,
311
- ),
104
+ /** Run a matrix suite, print results, and handle exports. */
105
+ export async function matrixBenchExports(
106
+ suite: MatrixSuite,
107
+ args: DefaultCliArgs,
108
+ reportOptions?: MatrixReportOptions,
109
+ exportOptions?: MatrixExportOptions,
110
+ ): Promise<void> {
111
+ const results = await runMatrixSuite(suite, args);
112
+ const report = withStatus("computing report", () =>
113
+ defaultMatrixReport(results, reportOptions, args),
312
114
  );
313
- }
314
-
315
- /** Export reports (HTML, JSON, Perfetto) based on CLI args */
316
- export async function exportReports(options: ExportOptions): Promise<void> {
317
- const { results, args, sections, suiteName } = options;
318
- const { currentVersion, baselineVersion } = options;
319
- const openInBrowser = args.html && !args["export-html"];
320
- let closeServer: (() => void) | undefined;
321
-
322
- if (args.html || args["export-html"]) {
323
- const htmlOpts = {
324
- cliArgs: args,
325
- sections,
326
- currentVersion,
327
- baselineVersion,
328
- };
329
- const reportData = prepareHtmlData(results, htmlOpts);
330
- const result = await generateHtmlReport(reportData, {
331
- openBrowser: openInBrowser,
332
- outputPath: args["export-html"],
333
- });
334
- closeServer = result.closeServer;
335
- }
336
-
337
- if (args.json) {
338
- await exportBenchmarkJson(results, args.json, args, suiteName);
339
- }
340
-
341
- if (args["export-perfetto"]) {
342
- exportPerfettoTrace(results, args["export-perfetto"], args);
343
- }
344
-
345
- if (args["export-speedscope"]) {
346
- exportSpeedscope(results, args["export-speedscope"]);
347
- }
348
-
349
- if (args.speedscope) {
350
- exportAndLaunchSpeedscope(results);
351
- }
115
+ console.log(report);
352
116
 
353
- // Keep process running when HTML report is opened in browser
354
- if (openInBrowser) {
355
- await waitForCtrlC();
356
- closeServer?.();
357
- }
117
+ const groups = matrixToReportGroups(results);
118
+ await finishReports(groups, args, exportOptions);
358
119
  }
359
120
 
360
- /** Run matrix suite with CLI arguments.
361
- * no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
362
- * --all --filter ==> subset of all, --all ==> all cases/variants */
121
+ /** Run matrix suite with CLI arguments. --filter narrows defaults, --all --filter narrows all. */
363
122
  export async function runMatrixSuite(
364
123
  suite: MatrixSuite,
365
124
  args: DefaultCliArgs,
366
125
  ): Promise<MatrixResults[]> {
126
+ if (args.list) {
127
+ await listMatrixSuite(suite);
128
+ return [];
129
+ }
367
130
  validateArgs(args);
368
131
  const filter = args.filter ? parseMatrixFilter(args.filter) : undefined;
369
132
  const options = cliToMatrixOptions(args);
370
133
 
371
134
  const results: MatrixResults[] = [];
372
135
  for (const matrix of suite.matrices) {
373
- const casesModule = matrix.casesModule
374
- ? await loadCasesModule(matrix.casesModule)
375
- : undefined;
376
-
377
- let filtered: FilteredMatrix<any> = matrix;
378
- if (!args.all && casesModule) {
379
- filtered = {
380
- ...matrix,
381
- filteredCases: casesModule.defaultCases,
382
- filteredVariants: casesModule.defaultVariants,
383
- };
384
- }
385
-
386
- // filter merges via intersection with defaults
387
- if (filter) {
388
- filtered = await filterMatrix(filtered, filter);
389
- }
390
-
136
+ const filtered = await applyMatrixFilters(matrix, args.all, filter);
391
137
  const { filteredCases, filteredVariants } = filtered;
392
138
  results.push(
393
139
  await runMatrix(filtered, {
@@ -400,555 +146,75 @@ export async function runMatrixSuite(
400
146
  return results;
401
147
  }
402
148
 
403
- /** Convert CLI args to matrix run options */
404
- export function cliToMatrixOptions(args: DefaultCliArgs): RunMatrixOptions {
405
- const { time, iterations, worker } = args;
406
- return {
407
- iterations,
408
- maxTime: iterations ? undefined : time * 1000,
409
- useWorker: worker,
410
- ...cliCommonOptions(args),
411
- };
412
- }
413
-
414
- /** Generate report for matrix results. Uses same sections as regular benchmarks. */
415
- export function defaultMatrixReport(
416
- results: MatrixResults[],
417
- reportOptions?: MatrixReportOptions,
418
- args?: DefaultCliArgs,
419
- ): string {
420
- const options = args
421
- ? mergeMatrixDefaults(reportOptions, args, results)
422
- : reportOptions;
423
- return results.map(r => reportMatrixResults(r, options)).join("\n\n");
424
- }
425
-
426
- /** Run matrix suite with full CLI handling (parse, run, report, export) */
427
- export async function runDefaultMatrixBench(
428
- suite: MatrixSuite,
429
- configureArgs?: Configure<any>,
430
- reportOptions?: MatrixReportOptions,
431
- ): Promise<void> {
432
- const args = parseBenchArgs(configureArgs);
433
- await matrixBenchExports(suite, args, reportOptions);
434
- }
435
-
436
- /** Convert MatrixResults to ReportGroup[] for export compatibility */
437
- export function matrixToReportGroups(results: MatrixResults[]): ReportGroup[] {
438
- return results.flatMap(matrix =>
439
- matrix.variants.flatMap(variant =>
440
- variant.cases.map(c => {
441
- const { metadata } = c;
442
- const report = {
443
- name: variant.id,
444
- measuredResults: c.measured,
445
- metadata,
446
- };
447
- const baseline = c.baseline
448
- ? {
449
- name: `${variant.id} (baseline)`,
450
- measuredResults: c.baseline,
451
- metadata,
452
- }
453
- : undefined;
454
- return {
455
- name: `${variant.id} / ${c.caseId}`,
456
- reports: [report],
457
- baseline,
458
- };
459
- }),
460
- ),
461
- );
149
+ /** Require a file argument for a subcommand, exiting with usage on missing. */
150
+ function requireFile(filePath: string | undefined, subcommand: string): string {
151
+ if (filePath) return filePath;
152
+ console.error(`Usage: benchforge ${subcommand} <file.benchforge>`);
153
+ process.exit(1);
462
154
  }
463
155
 
464
- /** Run matrix benchmarks, display table, and generate exports */
465
- export async function matrixBenchExports(
466
- suite: MatrixSuite,
467
- args: DefaultCliArgs,
468
- reportOptions?: MatrixReportOptions,
469
- exportOptions?: MatrixExportOptions,
470
- ): Promise<void> {
471
- const results = await runMatrixSuite(suite, args);
472
- const report = defaultMatrixReport(results, reportOptions, args);
473
- console.log(report);
474
-
475
- const reportGroups = matrixToReportGroups(results);
476
- await finishReports(reportGroups, args, suite.name, exportOptions);
477
- }
478
-
479
- /** Validate CLI argument combinations */
480
- function validateArgs(args: DefaultCliArgs): void {
481
- if (args["gc-stats"] && !args.worker && !args.url) {
482
- throw new Error(
483
- "--gc-stats requires worker mode (the default). Remove --no-worker flag.",
484
- );
485
- }
486
- }
487
-
488
- /** Execute all groups in suite */
489
- async function runSuite(params: SuiteParams): Promise<ReportGroup[]> {
490
- const { suite, runner, options, useWorker, batches } = params;
491
- const results: ReportGroup[] = [];
156
+ /** Print available benchmarks in a suite for --list. */
157
+ function listSuite(suite: BenchSuite): void {
492
158
  for (const group of suite.groups) {
493
- results.push(await runGroup(group, runner, options, useWorker, batches));
494
- }
495
- return results;
496
- }
497
-
498
- /** Build report sections based on CLI options */
499
- function buildReportSections(
500
- adaptive: boolean,
501
- gcStats: boolean,
502
- hasCpuData: boolean,
503
- hasOptData: boolean,
504
- ) {
505
- const sections = adaptive
506
- ? [adaptiveSection, totalTimeSection]
507
- : [timeSection];
508
-
509
- if (gcStats) sections.push(gcStatsSection);
510
- if (hasCpuData) sections.push(cpuSection);
511
- if (hasOptData) sections.push(optSection);
512
- sections.push(runsSection);
513
-
514
- return sections;
515
- }
516
-
517
- /** Print heap reports (if enabled) and export results */
518
- async function finishReports(
519
- results: ReportGroup[],
520
- args: DefaultCliArgs,
521
- suiteName?: string,
522
- exportOptions?: MatrixExportOptions,
523
- ): Promise<void> {
524
- if (needsHeapSample(args)) {
525
- printHeapReports(results, cliHeapReportOptions(args));
159
+ console.log(group.name);
160
+ for (const bench of group.benchmarks) console.log(` ${bench.name}`);
161
+ if (group.baseline) console.log(` ${group.baseline.name} (baseline)`);
526
162
  }
527
- await exportReports({ results, args, suiteName, ...exportOptions });
528
163
  }
529
164
 
530
- /** Warn about Node-only flags that are ignored in browser mode. */
531
- function warnBrowserFlags(args: DefaultCliArgs): void {
532
- const ignored: string[] = [];
533
- if (!args.worker) ignored.push("--no-worker");
534
- if (args.cpu) ignored.push("--cpu");
535
- if (args["trace-opt"]) ignored.push("--trace-opt");
536
- if (args.collect) ignored.push("--collect");
537
- if (args.adaptive) ignored.push("--adaptive");
538
- if (args.batches > 1) ignored.push("--batches");
539
- if (ignored.length) {
540
- console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
541
- }
542
- }
543
-
544
- /** @return true if any heap-related flag implies heap sampling */
545
- function needsHeapSample(args: DefaultCliArgs): boolean {
546
- return (
547
- args["heap-sample"] ||
548
- args.speedscope ||
549
- !!args["export-speedscope"] ||
550
- args["heap-raw"] ||
551
- args["heap-verbose"] ||
552
- args["heap-user-only"]
553
- );
554
- }
555
-
556
- /** Strip surrounding quotes from a chrome arg token.
557
- *
558
- * (Needed because --chrome-args values pass through yargs and spawn() without
559
- * shell processing, so literal quote characters reach Chrome/V8 unrecognized.)
560
- */
561
- function stripQuotes(s: string): string {
562
- /* (['"]): opening quote; (.*): content; \1: require same closing quote */
563
- const unquote = s.replace(/^(['"])(.*)\1$/s, "$2");
564
-
565
- /* value portion: --flag="--value" or --flag='--value'
566
- (-[^=]+=): flag name and =; (['"])(.*)\2: quoted value */
567
- const valueUnquote = unquote.replace(/^(-[^=]+=)(['"])(.*)\2$/s, "$1$3");
568
-
569
- return valueUnquote;
570
- }
571
-
572
- /** Wrap browser profile result as ReportGroup[] for the standard pipeline */
573
- function browserResultGroups(
574
- name: string,
575
- result: BrowserProfileResult,
576
- ): ReportGroup[] {
577
- const { gcStats, heapProfile } = result;
578
- let measured: MeasuredResults;
579
-
580
- // Bench function mode: multiple timing samples with real statistics
581
- if (result.samples && result.samples.length > 0) {
582
- const { samples } = result;
583
- const totalTime = result.wallTimeMs ? result.wallTimeMs / 1000 : undefined;
584
- measured = {
585
- name,
586
- samples,
587
- time: computeStats(samples),
588
- totalTime,
589
- gcStats,
590
- heapProfile,
591
- };
592
- } else {
593
- // Lap mode: 0 laps = single wall-clock, N laps handled above
594
- const wallMs = result.wallTimeMs ?? 0;
595
- const time = {
596
- min: wallMs,
597
- max: wallMs,
598
- avg: wallMs,
599
- p50: wallMs,
600
- p75: wallMs,
601
- p99: wallMs,
602
- p999: wallMs,
603
- };
604
- measured = { name, samples: [wallMs], time, gcStats, heapProfile };
605
- }
606
-
607
- return [{ name, reports: [{ name, measuredResults: measured }] }];
608
- }
609
-
610
- /** Print browser benchmark tables and heap reports */
611
- function printBrowserReport(
612
- result: BrowserProfileResult,
613
- results: ReportGroup[],
614
- args: DefaultCliArgs,
615
- ): void {
616
- const hasSamples = result.samples && result.samples.length > 0;
617
- const sections: ResultsMapper<any>[] = [];
618
- if (hasSamples || result.wallTimeMs != null) {
619
- sections.push(timeSection);
620
- }
621
- if (result.gcStats) {
622
- sections.push(browserGcStatsSection);
623
- }
624
- if (hasSamples || result.wallTimeMs != null) {
625
- sections.push(runsSection);
626
- }
627
- if (sections.length > 0) {
628
- console.log(reportResults(results, sections));
629
- }
630
- if (result.heapProfile) {
631
- printHeapReports(results, {
632
- ...cliHeapReportOptions(args),
633
- isUserCode: isBrowserUserCode,
634
- });
635
- }
636
- }
637
-
638
- /** Import a file and run it as a benchmark based on what it exports */
165
+ /** Import a file and run it as a benchmark based on what it exports. */
639
166
  async function fileBenchExports(
640
167
  filePath: string,
641
168
  args: DefaultCliArgs,
642
169
  ): Promise<void> {
643
170
  const fileUrl = pathToFileURL(resolve(filePath)).href;
644
- const mod = await import(fileUrl);
645
- const candidate = mod.default;
171
+ const { default: candidate } = await import(fileUrl);
646
172
 
647
173
  if (candidate && Array.isArray(candidate.matrices)) {
648
- // MatrixSuite export
649
- await matrixBenchExports(candidate as MatrixSuite, args);
650
- } else if (candidate && Array.isArray(candidate.groups)) {
651
- // BenchSuite export
652
- await benchExports(candidate as BenchSuite, args);
653
- } else if (typeof candidate === "function") {
654
- // Default function export: wrap as a single benchmark
655
- const name = basename(filePath).replace(/\.[^.]+$/, "");
656
- await benchExports(
657
- { name, groups: [{ name, benchmarks: [{ name, fn: candidate }] }] },
658
- args,
659
- );
174
+ if (args.list) return listMatrixSuite(candidate as MatrixSuite);
175
+ return matrixBenchExports(candidate as MatrixSuite, args);
660
176
  }
661
- // else: self-executing file already ran on import
662
- }
663
-
664
- /** Create options for adaptive mode */
665
- function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
666
- return {
667
- minTime: (args["min-time"] ?? 1) * 1000,
668
- maxTime: defaultAdaptiveMaxTime * 1000,
669
- targetConfidence: args.convergence,
670
- adaptive: true,
671
- ...cliCommonOptions(args),
672
- } as any;
673
- }
674
-
675
- /** Runner/matrix options shared across all CLI modes */
676
- function cliCommonOptions(args: DefaultCliArgs) {
677
- const { collect, cpu, warmup } = args;
678
- const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
679
- const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
680
- const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
681
- const heapSample = needsHeapSample(args);
682
- const { "heap-interval": heapInterval } = args;
683
- const { "heap-depth": heapDepth } = args;
684
- return {
685
- collect,
686
- cpuCounters: cpu,
687
- warmup,
688
- traceOpt,
689
- noSettle,
690
- pauseFirst,
691
- pauseInterval,
692
- pauseDuration,
693
- gcStats,
694
- heapSample,
695
- heapInterval,
696
- heapDepth,
697
- };
698
- }
699
-
700
- /** Wait for Ctrl+C before exiting */
701
- function waitForCtrlC(): Promise<void> {
702
- return new Promise(resolve => {
703
- console.log(dim("\nPress Ctrl+C to exit"));
704
- process.on("SIGINT", () => {
705
- console.log();
706
- resolve();
707
- });
708
- });
709
- }
710
-
711
- /** Apply default sections and extra columns for matrix reports */
712
- function mergeMatrixDefaults(
713
- reportOptions: MatrixReportOptions | undefined,
714
- args: DefaultCliArgs,
715
- results: MatrixResults[],
716
- ): MatrixReportOptions {
717
- const result: MatrixReportOptions = { ...reportOptions };
718
-
719
- if (!result.sections?.length) {
720
- const groups = matrixToReportGroups(results);
721
- result.sections = buildReportSections(
722
- args.adaptive,
723
- args["gc-stats"],
724
- hasField(groups, "cpu"),
725
- args["trace-opt"] && hasField(groups, "optStatus"),
726
- );
727
- }
728
-
729
- return result;
730
- }
731
-
732
- /** Execute group with shared setup, optionally batching to reduce ordering bias */
733
- async function runGroup(
734
- group: BenchGroup,
735
- runner: KnownRunner,
736
- options: RunnerOptions,
737
- useWorker: boolean,
738
- batches = 1,
739
- ): Promise<ReportGroup> {
740
- const { name, benchmarks, baseline, setup, metadata } = group;
741
- const setupParams = await setup?.();
742
- validateBenchmarkParameters(group);
743
-
744
- const runParams = {
745
- runner,
746
- options,
747
- useWorker,
748
- params: setupParams,
749
- metadata,
750
- };
751
- if (batches === 1) {
752
- return runSingleBatch(name, benchmarks, baseline, runParams);
177
+ if (candidate && Array.isArray(candidate.groups)) {
178
+ if (args.list) return listSuite(candidate as BenchSuite);
179
+ return benchExports(candidate as BenchSuite, args);
753
180
  }
754
- return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
755
- }
756
-
757
- /** @return HeapReportOptions from CLI args */
758
- function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
759
- return {
760
- topN: args["heap-rows"],
761
- stackDepth: args["heap-stack"],
762
- verbose: args["heap-verbose"],
763
- raw: args["heap-raw"],
764
- userOnly: args["heap-user-only"],
765
- };
766
- }
767
-
768
- /** Warn if parameterized benchmarks lack setup */
769
- function validateBenchmarkParameters(group: BenchGroup): void {
770
- const { name, setup, benchmarks, baseline } = group;
771
- if (setup) return;
772
-
773
- const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
774
- for (const benchmark of allBenchmarks) {
775
- if (benchmark.fn.length > 0) {
776
- console.warn(
777
- `Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`,
778
- );
779
- }
780
- }
781
- }
782
-
783
- /** Run benchmarks in a single batch */
784
- async function runSingleBatch(
785
- name: string,
786
- benchmarks: BenchmarkSpec[],
787
- baseline: BenchmarkSpec | undefined,
788
- runParams: RunParams,
789
- ): Promise<ReportGroup> {
790
- const baselineReport = baseline
791
- ? await runSingleBenchmark(baseline, runParams)
792
- : undefined;
793
- const reports = await serialMap(benchmarks, b =>
794
- runSingleBenchmark(b, runParams),
795
- );
796
- return { name, reports, baseline: baselineReport };
797
- }
798
-
799
- /** Run benchmarks in multiple batches, alternating order to reduce bias */
800
- async function runMultipleBatches(
801
- name: string,
802
- benchmarks: BenchmarkSpec[],
803
- baseline: BenchmarkSpec | undefined,
804
- runParams: RunParams,
805
- batches: number,
806
- ): Promise<ReportGroup> {
807
- const timePerBatch = (runParams.options.maxTime || 5000) / batches;
808
- const batchParams = {
809
- ...runParams,
810
- options: { ...runParams.options, maxTime: timePerBatch },
811
- };
812
- const baselineBatches: MeasuredResults[] = [];
813
- const benchmarkBatches = new Map<string, MeasuredResults[]>();
814
-
815
- for (let i = 0; i < batches; i++) {
816
- const reverseOrder = i % 2 === 1;
817
- await runBatchIteration(
818
- benchmarks,
819
- baseline,
820
- batchParams,
821
- reverseOrder,
822
- baselineBatches,
823
- benchmarkBatches,
824
- );
181
+ if (typeof candidate === "function") {
182
+ const name = basename(filePath).replace(/\.[^.]+$/, "");
183
+ const bench = { name, fn: candidate };
184
+ const suite = { name, groups: [{ name, benchmarks: [bench] }] };
185
+ return benchExports(suite, args);
825
186
  }
826
-
827
- const meta = runParams.metadata;
828
- return mergeBatchResults(
829
- name,
830
- benchmarks,
831
- baseline,
832
- baselineBatches,
833
- benchmarkBatches,
834
- meta,
835
- );
836
- }
837
-
838
- /** Run single benchmark and create report */
839
- async function runSingleBenchmark(
840
- spec: BenchmarkSpec,
841
- runParams: RunParams,
842
- ): Promise<BenchmarkReport> {
843
- const { runner, options, useWorker, params, metadata } = runParams;
844
- const benchmarkParams = { spec, runner, options, useWorker, params };
845
- const [result] = await runBenchmark(benchmarkParams);
846
- return { name: spec.name, measuredResults: result, metadata };
847
187
  }
848
188
 
849
- /** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
850
- async function serialMap<T, R>(
851
- arr: T[],
852
- fn: (item: T) => Promise<R>,
853
- ): Promise<R[]> {
854
- const results: R[] = [];
855
- for (const item of arr) {
856
- results.push(await fn(item));
857
- }
858
- return results;
859
- }
860
-
861
- /** Run one batch iteration in either order */
862
- async function runBatchIteration(
863
- benchmarks: BenchmarkSpec[],
864
- baseline: BenchmarkSpec | undefined,
865
- runParams: RunParams,
866
- reverseOrder: boolean,
867
- baselineBatches: MeasuredResults[],
868
- benchmarkBatches: Map<string, MeasuredResults[]>,
869
- ): Promise<void> {
870
- const runBaseline = async () => {
871
- if (baseline) {
872
- const r = await runSingleBenchmark(baseline, runParams);
873
- baselineBatches.push(r.measuredResults);
874
- }
875
- };
876
- const runBenches = async () => {
877
- for (const b of benchmarks) {
878
- const r = await runSingleBenchmark(b, runParams);
879
- appendToMap(benchmarkBatches, b.name, r.measuredResults);
189
+ /** Print available cases and variants in a matrix suite for --list. */
190
+ async function listMatrixSuite(suite: MatrixSuite): Promise<void> {
191
+ for (const matrix of suite.matrices) {
192
+ console.log(matrix.name);
193
+ const caseIds = await resolveCaseIds(matrix);
194
+ if (caseIds) {
195
+ console.log(" cases:");
196
+ for (const id of caseIds) console.log(` ${id}`);
880
197
  }
881
- };
882
-
883
- if (reverseOrder) {
884
- await runBenches();
885
- await runBaseline();
886
- } else {
887
- await runBaseline();
888
- await runBenches();
198
+ const variantIds = await resolveVariantIds(matrix);
199
+ console.log(" variants:");
200
+ for (const id of variantIds) console.log(` ${id}`);
889
201
  }
890
202
  }
891
203
 
892
- /** Merge batch results into final ReportGroup */
893
- function mergeBatchResults(
894
- name: string,
895
- benchmarks: BenchmarkSpec[],
896
- baseline: BenchmarkSpec | undefined,
897
- baselineBatches: MeasuredResults[],
898
- benchmarkBatches: Map<string, MeasuredResults[]>,
899
- metadata?: Record<string, unknown>,
900
- ): ReportGroup {
901
- const mergedBaseline = baseline
902
- ? {
903
- name: baseline.name,
904
- measuredResults: mergeResults(baselineBatches),
905
- metadata,
906
- }
204
+ /** --filter bypasses defaults (implies --all for the filtered dimension). */
205
+ async function applyMatrixFilters(
206
+ matrix: FilteredMatrix<any>,
207
+ runAll: boolean,
208
+ filter?: MatrixFilter,
209
+ ): Promise<FilteredMatrix<any>> {
210
+ const mod = matrix.casesModule
211
+ ? await loadCasesModule(matrix.casesModule)
907
212
  : undefined;
908
- const reports = benchmarks.map(b => ({
909
- name: b.name,
910
- measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
911
- metadata,
912
- }));
913
- return { name, reports, baseline: mergedBaseline };
914
- }
915
-
916
- function appendToMap(
917
- map: Map<string, MeasuredResults[]>,
918
- key: string,
919
- value: MeasuredResults,
920
- ) {
921
- if (!map.has(key)) map.set(key, []);
922
- map.get(key)!.push(value);
923
- }
924
-
925
- /** Merge multiple batch results into a single MeasuredResults */
926
- function mergeResults(results: MeasuredResults[]): MeasuredResults {
927
- if (results.length === 0) {
928
- throw new Error("Cannot merge empty results array");
213
+ let withDefaults = matrix;
214
+ if (!runAll && !filter && mod) {
215
+ const { defaultCases: filteredCases, defaultVariants: filteredVariants } =
216
+ mod;
217
+ withDefaults = { ...matrix, filteredCases, filteredVariants };
929
218
  }
930
- if (results.length === 1) return results[0];
931
-
932
- const allSamples = results.flatMap(r => r.samples);
933
- const allWarmup = results.flatMap(r => r.warmupSamples || []);
934
- const time = computeStats(allSamples);
935
-
936
- let offset = 0;
937
- const allPausePoints = results.flatMap(r => {
938
- const pts = (r.pausePoints ?? []).map(p => ({
939
- sampleIndex: p.sampleIndex + offset,
940
- durationMs: p.durationMs,
941
- }));
942
- offset += r.samples.length;
943
- return pts;
944
- });
945
-
946
- return {
947
- name: results[0].name,
948
- samples: allSamples,
949
- warmupSamples: allWarmup.length ? allWarmup : undefined,
950
- time,
951
- totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
952
- pausePoints: allPausePoints.length ? allPausePoints : undefined,
953
- };
219
+ return filter ? filterMatrix(withDefaults, filter) : withDefaults;
954
220
  }