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