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,2873 +0,0 @@
1
- import { a as createAdaptiveWrapper, c as BasicRunner, i as createRunner, l as computeStats, n as getElapsed, o as average, r as getPerfNow, s as bootstrapDifferenceCI, t as debugWorkerTiming, u as discoverVariants } from "./TimingUtils-ClclVQ7E.mjs";
2
- import { n as parseGcLine, t as aggregateGcStats } from "./GcStats-ByEovUi1.mjs";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { fileURLToPath, pathToFileURL } from "node:url";
5
- import { execSync, fork, spawn } from "node:child_process";
6
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
- import path, { basename, dirname, extname, join, resolve } from "node:path";
8
- import pico from "picocolors";
9
- import { table } from "table";
10
- import yargs from "yargs";
11
- import { hideBin } from "yargs/helpers";
12
- import { createServer } from "node:http";
13
- import open from "open";
14
-
15
- //#region src/matrix/CaseLoader.ts
16
- /** Load a cases module by URL */
17
- async function loadCasesModule(moduleUrl) {
18
- const module = await import(moduleUrl);
19
- if (!Array.isArray(module.cases)) throw new Error(`Cases module at ${moduleUrl} must export 'cases' array`);
20
- return {
21
- cases: module.cases,
22
- defaultCases: module.defaultCases,
23
- defaultVariants: module.defaultVariants,
24
- loadCase: module.loadCase
25
- };
26
- }
27
- /** Load case data from a CasesModule or pass through the caseId */
28
- async function loadCaseData(casesModule, caseId) {
29
- if (casesModule?.loadCase) return casesModule.loadCase(caseId);
30
- return { data: caseId };
31
- }
32
-
33
- //#endregion
34
- //#region src/runners/RunnerOrchestrator.ts
35
- const logTiming = debugWorkerTiming ? (message) => console.log(`[RunnerOrchestrator] ${message}`) : () => {};
36
- /** Execute benchmarks directly or in worker process */
37
- async function runBenchmark({ spec, runner, options, useWorker = false, params }) {
38
- if (!useWorker) {
39
- const resolvedSpec = spec.modulePath ? await resolveModuleSpec(spec, params) : {
40
- spec,
41
- params
42
- };
43
- const base = await createRunner(runner);
44
- return (options.adaptive ? createAdaptiveWrapper(base, options) : base).runBench(resolvedSpec.spec, options, resolvedSpec.params);
45
- }
46
- return runInWorker({
47
- spec,
48
- runner,
49
- options,
50
- params
51
- });
52
- }
53
- /** Resolve modulePath/exportName to a real function for non-worker mode */
54
- async function resolveModuleSpec(spec, params) {
55
- const module = await import(spec.modulePath);
56
- const fn = spec.exportName ? module[spec.exportName] : module.default || module;
57
- if (typeof fn !== "function") {
58
- const name = spec.exportName || "default";
59
- throw new Error(`Export '${name}' from ${spec.modulePath} is not a function`);
60
- }
61
- let resolvedParams = params;
62
- if (spec.setupExportName) {
63
- const setupFn = module[spec.setupExportName];
64
- if (typeof setupFn !== "function") {
65
- const msg = `Setup export '${spec.setupExportName}' from ${spec.modulePath} is not a function`;
66
- throw new Error(msg);
67
- }
68
- resolvedParams = await setupFn(params);
69
- }
70
- return {
71
- spec: {
72
- ...spec,
73
- fn
74
- },
75
- params: resolvedParams
76
- };
77
- }
78
- /** Run benchmark in isolated worker process */
79
- async function runInWorker(workerParams) {
80
- const { spec, runner, options, params } = workerParams;
81
- const msg = createRunMessage(spec, runner, options, params);
82
- return runWorkerWithMessage(spec.name, options, msg);
83
- }
84
- /** Create worker process with timing logs */
85
- function createWorkerWithTiming(gcStats) {
86
- const workerStart = getPerfNow();
87
- const gcEvents = [];
88
- const worker = createWorkerProcess(gcStats);
89
- const createTime = getPerfNow();
90
- if (gcStats && worker.stdout) setupGcCapture(worker, gcEvents);
91
- logTiming(`Worker process created in ${getElapsed(workerStart, createTime).toFixed(1)}ms`);
92
- return {
93
- worker,
94
- createTime,
95
- gcEvents
96
- };
97
- }
98
- /** Capture and parse GC lines from stdout (V8's --trace-gc-nvp outputs to stdout) */
99
- function setupGcCapture(worker, gcEvents) {
100
- let buffer = "";
101
- worker.stdout.on("data", (data) => {
102
- buffer += data.toString();
103
- const lines = buffer.split("\n");
104
- buffer = lines.pop() || "";
105
- for (const line of lines) {
106
- const event = parseGcLine(line);
107
- if (event) gcEvents.push(event);
108
- else if (line.trim()) process.stdout.write(line + "\n");
109
- }
110
- });
111
- }
112
- /** Spawn worker, wire handlers, send message, return results */
113
- function runWorkerWithMessage(name, options, message) {
114
- const startTime = getPerfNow();
115
- const collectGcStats = options.gcStats ?? false;
116
- logTiming(`Starting worker for ${name}`);
117
- return new Promise((resolve, reject) => {
118
- const { worker, createTime, gcEvents } = createWorkerWithTiming(collectGcStats);
119
- setupWorkerHandlers(worker, name, createWorkerHandlers(name, startTime, gcEvents, resolve, reject));
120
- sendWorkerMessage(worker, message, createTime);
121
- });
122
- }
123
- /** Send message to worker with timing log */
124
- function sendWorkerMessage(worker, message, createTime) {
125
- const messageTime = getPerfNow();
126
- worker.send(message);
127
- logTiming(`Message sent to worker in ${getElapsed(createTime, messageTime).toFixed(1)}ms`);
128
- }
129
- /** Setup worker event handlers with cleanup */
130
- function setupWorkerHandlers(worker, specName, handlers) {
131
- const { resolve, reject } = handlers;
132
- const cleanup = createCleanup(worker, specName, reject);
133
- worker.on("message", createMessageHandler(specName, cleanup, resolve, reject));
134
- worker.on("error", createErrorHandler(specName, cleanup, reject));
135
- worker.on("exit", createExitHandler(specName, cleanup, reject));
136
- }
137
- /** Handle worker messages (results or errors) */
138
- function createMessageHandler(specName, cleanup, resolve, reject) {
139
- return (msg) => {
140
- cleanup();
141
- if (msg.type === "result") resolve(msg.results, msg.heapProfile);
142
- else if (msg.type === "error") {
143
- const error = /* @__PURE__ */ new Error(`Benchmark "${specName}" failed: ${msg.error}`);
144
- if (msg.stack) error.stack = msg.stack;
145
- reject(error);
146
- }
147
- };
148
- }
149
- /** Handle worker process errors */
150
- function createErrorHandler(specName, cleanup, reject) {
151
- return (error) => {
152
- cleanup();
153
- reject(/* @__PURE__ */ new Error(`Worker process failed for benchmark "${specName}": ${error.message}`));
154
- };
155
- }
156
- /** Handle worker process exit */
157
- function createExitHandler(specName, cleanup, reject) {
158
- return (code, _signal) => {
159
- if (code !== 0 && code !== null) {
160
- cleanup();
161
- const msg = `Worker exited with code ${code} for benchmark "${specName}"`;
162
- reject(new Error(msg));
163
- }
164
- };
165
- }
166
- /** Create cleanup for timeout and termination */
167
- function createCleanup(worker, specName, reject) {
168
- const timeoutId = setTimeout(() => {
169
- cleanup();
170
- reject(/* @__PURE__ */ new Error(`Benchmark "${specName}" timed out after 60 seconds`));
171
- }, 6e4);
172
- const cleanup = () => {
173
- clearTimeout(timeoutId);
174
- if (!worker.killed) worker.kill("SIGTERM");
175
- };
176
- return cleanup;
177
- }
178
- /** Create worker process with configuration */
179
- function createWorkerProcess(gcStats) {
180
- const workerPath = resolveWorkerPath();
181
- const execArgv = ["--expose-gc", "--allow-natives-syntax"];
182
- if (gcStats) execArgv.push("--trace-gc-nvp");
183
- return fork(workerPath, [], {
184
- execArgv,
185
- silent: gcStats,
186
- env: {
187
- ...process.env,
188
- NODE_OPTIONS: ""
189
- }
190
- });
191
- }
192
- /** Resolve WorkerScript path for dev (.ts) or dist (.mjs) */
193
- function resolveWorkerPath() {
194
- const dir = import.meta.dirname;
195
- const tsPath = path.join(dir, "WorkerScript.ts");
196
- if (existsSync(tsPath)) return tsPath;
197
- return path.join(dir, "runners", "WorkerScript.mjs");
198
- }
199
- /** @return handlers that attach GC stats and heap profile to results */
200
- function createWorkerHandlers(specName, startTime, gcEvents, resolve, reject) {
201
- return {
202
- resolve: (results, heapProfile) => {
203
- logTiming(`Total worker time for ${specName}: ${getElapsed(startTime).toFixed(1)}ms`);
204
- if (gcEvents?.length) {
205
- const gcStats = aggregateGcStats(gcEvents);
206
- for (const r of results) r.gcStats = gcStats;
207
- }
208
- if (heapProfile) for (const r of results) r.heapProfile = heapProfile;
209
- resolve(results);
210
- },
211
- reject
212
- };
213
- }
214
- /** Create message for worker execution */
215
- function createRunMessage(spec, runnerName, options, params) {
216
- const { fn, ...rest } = spec;
217
- const message = {
218
- type: "run",
219
- spec: rest,
220
- runnerName,
221
- options,
222
- params
223
- };
224
- if (spec.modulePath) {
225
- message.modulePath = spec.modulePath;
226
- message.exportName = spec.exportName;
227
- if (spec.setupExportName) message.setupExportName = spec.setupExportName;
228
- } else message.fnCode = fn.toString();
229
- return message;
230
- }
231
- /** Run a matrix variant benchmark in isolated worker process */
232
- async function runMatrixVariant(params) {
233
- const { variantDir, variantId, caseId, caseData, casesModule, runner, options } = params;
234
- const name = `${variantId}/${caseId}`;
235
- return runWorkerWithMessage(name, options, {
236
- type: "run",
237
- spec: {
238
- name,
239
- fn: () => {}
240
- },
241
- runnerName: runner,
242
- options,
243
- variantDir,
244
- variantId,
245
- caseId,
246
- caseData,
247
- casesModule
248
- });
249
- }
250
-
251
- //#endregion
252
- //#region src/BenchMatrix.ts
253
- /** @return true if variant is a StatefulVariant (has setup + run) */
254
- function isStatefulVariant(v) {
255
- return typeof v === "object" && "setup" in v && "run" in v;
256
- }
257
- /** Run a BenchMatrix with inline variants or variantDir */
258
- async function runMatrix(matrix, options = {}) {
259
- validateBaseline(matrix);
260
- const effectiveOptions = {
261
- ...matrix.defaults,
262
- ...options
263
- };
264
- if (matrix.variantDir) return runMatrixWithDir(matrix, effectiveOptions);
265
- if (matrix.variants) return runMatrixInline(matrix, effectiveOptions);
266
- throw new Error("BenchMatrix requires either 'variants' or 'variantDir'");
267
- }
268
- /** @throws if both baselineDir and baselineVariant are set */
269
- function validateBaseline(matrix) {
270
- const msg = "BenchMatrix cannot have both 'baselineDir' and 'baselineVariant'";
271
- if (matrix.baselineDir && matrix.baselineVariant) throw new Error(msg);
272
- }
273
- function buildRunnerOptions(options) {
274
- return {
275
- maxIterations: options.iterations,
276
- maxTime: options.maxTime ?? 1e3,
277
- warmup: options.warmup ?? 0,
278
- collect: options.collect,
279
- cpuCounters: options.cpuCounters,
280
- traceOpt: options.traceOpt,
281
- noSettle: options.noSettle,
282
- pauseFirst: options.pauseFirst,
283
- pauseInterval: options.pauseInterval,
284
- pauseDuration: options.pauseDuration,
285
- gcStats: options.gcStats,
286
- heapSample: options.heapSample,
287
- heapInterval: options.heapInterval,
288
- heapDepth: options.heapDepth
289
- };
290
- }
291
- /** Load cases module and resolve filtered case IDs */
292
- async function resolveCases(matrix, options) {
293
- const casesModule = matrix.casesModule ? await loadCasesModule(matrix.casesModule) : void 0;
294
- const allCaseIds = casesModule?.cases ?? matrix.cases ?? ["default"];
295
- return {
296
- casesModule,
297
- caseIds: options.filteredCases ?? allCaseIds
298
- };
299
- }
300
- /** Run matrix with inline variants (non-worker mode) */
301
- async function runMatrixInline(matrix, options) {
302
- const msg = "BenchMatrix with inline 'variants' cannot use 'baselineDir'. Use 'variantDir' instead.";
303
- if (matrix.baselineDir) throw new Error(msg);
304
- const { casesModule, caseIds } = await resolveCases(matrix, options);
305
- const runner = new BasicRunner();
306
- const runnerOpts = buildRunnerOptions(options);
307
- const variantEntries = options.filteredVariants ? Object.entries(matrix.variants).filter(([id]) => options.filteredVariants.includes(id)) : Object.entries(matrix.variants);
308
- const variants = [];
309
- for (const [variantId, variant] of variantEntries) {
310
- const cases = [];
311
- for (const caseId of caseIds) {
312
- const loaded = await loadCaseData(casesModule, caseId);
313
- const measured = await runVariant(variant, casesModule || matrix.cases ? loaded.data : void 0, variantId, runner, runnerOpts);
314
- cases.push({
315
- caseId,
316
- measured,
317
- metadata: loaded.metadata
318
- });
319
- }
320
- variants.push({
321
- id: variantId,
322
- cases
323
- });
324
- }
325
- if (matrix.baselineVariant) applyBaselineVariant(variants, matrix.baselineVariant);
326
- return {
327
- name: matrix.name,
328
- variants
329
- };
330
- }
331
- /** Run matrix with variantDir (worker mode for memory isolation) */
332
- async function runMatrixWithDir(matrix, options) {
333
- const allVariantIds = await discoverVariants(matrix.variantDir);
334
- if (allVariantIds.length === 0) throw new Error(`No variants found in ${matrix.variantDir}`);
335
- const variants = await runDirVariants(options.filteredVariants ?? allVariantIds, await createDirContext(matrix, options));
336
- if (matrix.baselineVariant) applyBaselineVariant(variants, matrix.baselineVariant);
337
- return {
338
- name: matrix.name,
339
- variants
340
- };
341
- }
342
- /** Create context for directory-based matrix execution */
343
- async function createDirContext(matrix, options) {
344
- const baselineIds = matrix.baselineDir ? await discoverVariants(matrix.baselineDir) : [];
345
- const { casesModule, caseIds } = await resolveCases(matrix, options);
346
- return {
347
- matrix,
348
- casesModule,
349
- baselineIds,
350
- caseIds,
351
- runnerOpts: buildRunnerOptions(options)
352
- };
353
- }
354
- /** Run all variants using worker processes */
355
- async function runDirVariants(variantIds, ctx) {
356
- const variants = [];
357
- for (const variantId of variantIds) {
358
- const cases = await runDirVariantCases(variantId, ctx);
359
- variants.push({
360
- id: variantId,
361
- cases
362
- });
363
- }
364
- return variants;
365
- }
366
- /** Run all cases for a single variant */
367
- async function runDirVariantCases(variantId, ctx) {
368
- const { matrix, casesModule, caseIds, runnerOpts } = ctx;
369
- const cases = [];
370
- for (const caseId of caseIds) {
371
- const caseData = !matrix.casesModule && matrix.cases ? caseId : void 0;
372
- const [measured] = await runMatrixVariant({
373
- variantDir: matrix.variantDir,
374
- variantId,
375
- caseId,
376
- caseData,
377
- casesModule: matrix.casesModule,
378
- runner: "basic",
379
- options: runnerOpts
380
- });
381
- const loaded = await loadCaseData(casesModule, caseId);
382
- const baseline = await runBaselineIfExists(variantId, caseId, caseData, ctx);
383
- const deltaPercent = baseline ? computeDeltaPercent(baseline, measured) : void 0;
384
- const metadata = loaded.metadata;
385
- cases.push({
386
- caseId,
387
- measured,
388
- metadata,
389
- baseline,
390
- deltaPercent
391
- });
392
- }
393
- return cases;
394
- }
395
- /** Run baseline variant if it exists in baselineDir */
396
- async function runBaselineIfExists(variantId, caseId, caseData, ctx) {
397
- const { matrix, baselineIds, runnerOpts } = ctx;
398
- if (!matrix.baselineDir || !baselineIds.includes(variantId)) return void 0;
399
- const [measured] = await runMatrixVariant({
400
- variantDir: matrix.baselineDir,
401
- variantId,
402
- caseId,
403
- caseData,
404
- casesModule: matrix.casesModule,
405
- runner: "basic",
406
- options: runnerOpts
407
- });
408
- return measured;
409
- }
410
- /** Compute delta percentage: (current - baseline) / baseline * 100 */
411
- function computeDeltaPercent(baseline, current) {
412
- const baseAvg = average(baseline.samples);
413
- if (baseAvg === 0) return 0;
414
- return (average(current.samples) - baseAvg) / baseAvg * 100;
415
- }
416
- /** Apply baselineVariant comparison - one variant is the reference for all others */
417
- function applyBaselineVariant(variants, baselineVariantId) {
418
- const baselineVariant = variants.find((v) => v.id === baselineVariantId);
419
- if (!baselineVariant) return;
420
- const baselineByCase = /* @__PURE__ */ new Map();
421
- for (const c of baselineVariant.cases) baselineByCase.set(c.caseId, c.measured);
422
- for (const variant of variants) {
423
- if (variant.id === baselineVariantId) continue;
424
- for (const caseResult of variant.cases) {
425
- const baseline = baselineByCase.get(caseResult.caseId);
426
- if (baseline) {
427
- caseResult.baseline = baseline;
428
- caseResult.deltaPercent = computeDeltaPercent(baseline, caseResult.measured);
429
- }
430
- }
431
- }
432
- }
433
- /** Run a single variant with case data */
434
- async function runVariant(variant, caseData, name, runner, options) {
435
- if (isStatefulVariant(variant)) {
436
- const state = await variant.setup(caseData);
437
- const [result] = await runner.runBench({
438
- name,
439
- fn: () => variant.run(state)
440
- }, options);
441
- return result;
442
- }
443
- const [result] = await runner.runBench({
444
- name,
445
- fn: () => variant(caseData)
446
- }, options);
447
- return result;
448
- }
449
-
450
- //#endregion
451
- //#region src/table-util/Formatters.ts
452
- const { red: red$1, green } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? {
453
- red: (str) => str,
454
- green: (str) => str
455
- } : pico;
456
- /** Format percentages with custom precision */
457
- function percentPrecision(precision) {
458
- return (x) => {
459
- if (typeof x !== "number") return null;
460
- return percent(x, precision);
461
- };
462
- }
463
- /** Format duration in milliseconds with appropriate units */
464
- function duration(ms) {
465
- if (typeof ms !== "number") return null;
466
- if (ms < .001) return `${(ms * 1e6).toFixed(0)}ns`;
467
- if (ms < 1) return `${(ms * 1e3).toFixed(1)}μs`;
468
- if (ms < 1e3) return `${ms.toFixed(2)}ms`;
469
- return `${(ms / 1e3).toFixed(2)}s`;
470
- }
471
- /** Format time in milliseconds, showing very small values with units */
472
- function timeMs(ms) {
473
- if (typeof ms !== "number") return null;
474
- if (ms < .001) return `${(ms * 1e6).toFixed(0)}ns`;
475
- if (ms < .01) return `${(ms * 1e3).toFixed(1)}μs`;
476
- if (ms >= 10) return ms.toFixed(0);
477
- return ms.toFixed(2);
478
- }
479
- /** Format integer with thousand separators */
480
- function integer(x) {
481
- if (typeof x !== "number") return null;
482
- return new Intl.NumberFormat("en-US").format(Math.round(x));
483
- }
484
- /** Format fraction as percentage (0.473 → 47.3%) */
485
- function percent(fraction, precision = 1) {
486
- if (typeof fraction !== "number") return null;
487
- return `${Math.abs(fraction * 100).toFixed(precision)}%`;
488
- }
489
- /** Format percentage difference between two values */
490
- function diffPercent(main, base) {
491
- if (typeof main !== "number" || typeof base !== "number") return " ";
492
- return coloredPercent(main - base, base);
493
- }
494
- /** Format fraction as colored +/- percentage */
495
- function coloredPercent(numerator, denominator, positiveIsGreen = true) {
496
- const fraction = numerator / denominator;
497
- if (Number.isNaN(fraction) || !Number.isFinite(fraction)) return " ";
498
- const positive = fraction >= 0;
499
- const percentStr = `${positive ? "+" : "-"}${percent(fraction)}`;
500
- return positive === positiveIsGreen ? green(percentStr) : red$1(percentStr);
501
- }
502
- /** Format bytes with appropriate units (B, KB, MB, GB) */
503
- function formatBytes(bytes) {
504
- if (typeof bytes !== "number") return null;
505
- if (bytes < 1024) return `${bytes.toFixed(0)}B`;
506
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
507
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
508
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
509
- }
510
- /** Format percentage difference with confidence interval */
511
- function formatDiffWithCI(value) {
512
- if (!isDifferenceCI(value)) return null;
513
- const { percent, ci, direction } = value;
514
- return colorByDirection(diffCIText(percent, ci), direction);
515
- }
516
- /** Format percentage difference with CI for throughput metrics (higher is better) */
517
- function formatDiffWithCIHigherIsBetter(value) {
518
- if (!isDifferenceCI(value)) return null;
519
- const { percent, ci, direction } = value;
520
- return colorByDirection(diffCIText(-percent, [-ci[1], -ci[0]]), direction);
521
- }
522
- /** @return formatted "pct [lo, hi]" text for a diff with CI */
523
- function diffCIText(pct, ci) {
524
- return `${formatBound(pct)} [${formatBound(ci[0])}, ${formatBound(ci[1])}]`;
525
- }
526
- /** @return text colored green for faster, red for slower */
527
- function colorByDirection(text, direction) {
528
- if (direction === "faster") return green(text);
529
- if (direction === "slower") return red$1(text);
530
- return text;
531
- }
532
- /** @return signed percentage string (e.g. "+1.2%", "-3.4%") */
533
- function formatBound(v) {
534
- return `${v >= 0 ? "+" : ""}${v.toFixed(1)}%`;
535
- }
536
- /** @return true if value is a DifferenceCI object */
537
- function isDifferenceCI(x) {
538
- return typeof x === "object" && x !== null && "ci" in x && "direction" in x;
539
- }
540
- /** @return truncated string with ellipsis if over maxLen */
541
- function truncate(str, maxLen = 30) {
542
- return str.length > maxLen ? str.slice(0, maxLen - 3) + "..." : str;
543
- }
544
-
545
- //#endregion
546
- //#region src/table-util/TableReport.ts
547
- const { bold } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? { bold: (str) => str } : pico;
548
- /** Build formatted table with column groups and baselines */
549
- function buildTable(columnGroups, resultGroups, nameKey = "name") {
550
- return createTable(columnGroups, flattenGroups(columnGroups, resultGroups, nameKey));
551
- }
552
- /** Convert columns and records to formatted table */
553
- function createTable(groups, records) {
554
- const dataRows = toRows(records, groups);
555
- const { headerRows, config } = setup(groups, dataRows);
556
- return table([...headerRows, ...dataRows], config);
557
- }
558
- /** Create header rows with group titles */
559
- function createGroupHeaders(groups, numColumns) {
560
- if (!groups.some((g) => g.groupTitle)) return [];
561
- return [groups.flatMap((g) => {
562
- return padWithBlanks(g.groupTitle ? [bold(g.groupTitle)] : [], g.columns.length);
563
- }), padWithBlanks([], numColumns)];
564
- }
565
- /** @return draw functions for horizontal/vertical table borders */
566
- function createLines(groups) {
567
- const { sectionBorders, headerBottom } = calcBorders(groups);
568
- function drawVerticalLine(index, size) {
569
- return index === 0 || index === size || sectionBorders.includes(index);
570
- }
571
- function drawHorizontalLine(index, size) {
572
- return index === 0 || index === size || index === headerBottom;
573
- }
574
- return {
575
- drawHorizontalLine,
576
- drawVerticalLine
577
- };
578
- }
579
- /** @return spanning cell configs for group title headers */
580
- function createSectionSpans(groups) {
581
- let col = 0;
582
- const alignment = "center";
583
- return groups.map((g) => {
584
- const colSpan = g.columns.length;
585
- const span = {
586
- row: 0,
587
- col,
588
- colSpan,
589
- alignment
590
- };
591
- col += colSpan;
592
- return span;
593
- });
594
- }
595
- /** @return bolded column title strings */
596
- function getTitles(groups) {
597
- return groups.flatMap((g) => g.columns.map((c) => bold(c.title || " ")));
598
- }
599
- /** @return array padded with blank strings to the given length */
600
- function padWithBlanks(arr, length) {
601
- if (arr.length >= length) return arr;
602
- return [...arr, ...Array(length - arr.length).fill(" ")];
603
- }
604
- /** Convert records to string arrays for table */
605
- function toRows(records, groups) {
606
- const allColumns = groups.flatMap((group) => group.columns);
607
- return records.map((record) => allColumns.map((col) => {
608
- const value = record[col.key];
609
- return col.formatter ? col.formatter(value) : value;
610
- })).map((row) => row.map((cell) => cell ?? " "));
611
- }
612
- /** Add comparison values for diff columns */
613
- function addComparisons(groups, mainRecord, baselineRecord) {
614
- const diffColumns = groups.flatMap((g) => g.columns).filter((col) => col.diffKey);
615
- const updatedMain = { ...mainRecord };
616
- for (const col of diffColumns) {
617
- const dcol = col;
618
- const diffKey = dcol.diffKey;
619
- const mainValue = mainRecord[diffKey];
620
- const baselineValue = baselineRecord[diffKey];
621
- const diffStr = (dcol.diffFormatter ?? diffPercent)(mainValue, baselineValue);
622
- updatedMain[col.key] = diffStr;
623
- }
624
- return updatedMain;
625
- }
626
- /** Flatten groups with spacing */
627
- function flattenGroups(columnGroups, resultGroups, nameKey) {
628
- return resultGroups.flatMap((group, i) => {
629
- const groupRecords = addBaseline(columnGroups, group, nameKey);
630
- return i === resultGroups.length - 1 ? groupRecords : [...groupRecords, {}];
631
- });
632
- }
633
- /** Process results with baseline comparisons */
634
- function addBaseline(columnGroups, group, nameKey) {
635
- const { results, baseline } = group;
636
- if (!baseline) return results;
637
- const diffResults = results.map((result) => addComparisons(columnGroups, result, baseline));
638
- const markedBaseline = {
639
- ...baseline,
640
- [nameKey]: `--> ${baseline[nameKey]}`
641
- };
642
- return [...diffResults, markedBaseline];
643
- }
644
- /** Calculate vertical lines between sections and header bottom position */
645
- function calcBorders(groups) {
646
- if (groups.length === 0) return {
647
- sectionBorders: [],
648
- headerBottom: 1
649
- };
650
- const sectionBorders = [];
651
- let border = 0;
652
- for (const g of groups) {
653
- border += g.columns.length;
654
- sectionBorders.push(border);
655
- }
656
- return {
657
- sectionBorders,
658
- headerBottom: 3
659
- };
660
- }
661
- /** Create headers and table configuration */
662
- function setup(groups, dataRows) {
663
- const titles = getTitles(groups);
664
- const numColumns = titles.length;
665
- return {
666
- headerRows: [...createGroupHeaders(groups, numColumns), titles],
667
- config: {
668
- spanningCells: createSectionSpans(groups),
669
- columns: calcColumnWidths(groups, titles, dataRows),
670
- ...createLines(groups)
671
- }
672
- };
673
- }
674
- /** Calculate column widths based on content, including group titles */
675
- function calcColumnWidths(groups, titles, dataRows) {
676
- const widths = [];
677
- for (let i = 0; i < titles.length; i++) {
678
- const titleW = cellWidth(titles[i]);
679
- const maxDataW = dataRows.reduce((max, row) => Math.max(max, cellWidth(row[i])), 0);
680
- widths.push(Math.max(titleW, maxDataW));
681
- }
682
- let colIndex = 0;
683
- for (const group of groups) {
684
- const groupW = cellWidth(group.groupTitle);
685
- if (groupW > 0) {
686
- const numCols = group.columns.length;
687
- const separatorWidth = (numCols - 1) * 3;
688
- const needed = groupW - widths.slice(colIndex, colIndex + numCols).reduce((a, b) => a + b, 0) - separatorWidth;
689
- if (needed > 0) widths[colIndex + numCols - 1] += needed;
690
- }
691
- colIndex += group.columns.length;
692
- }
693
- return Object.fromEntries(widths.map((w, i) => [i, {
694
- width: w,
695
- wrapWord: false
696
- }]));
697
- }
698
- const ansiEscapeRegex = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*m", "g");
699
- /** Get visible length of a cell value (strips ANSI escape codes) */
700
- function cellWidth(value) {
701
- if (value == null) return 0;
702
- return String(value).replace(ansiEscapeRegex, "").length;
703
- }
704
-
705
- //#endregion
706
- //#region src/BenchmarkReport.ts
707
- /** @return formatted table report with optional baseline comparisons */
708
- function reportResults(groups, sections) {
709
- const results = groups.map((group) => resultGroupValues(group, sections));
710
- return buildTable(createColumnGroups(sections, results.some((g) => g.baseline)), results);
711
- }
712
- /** @return values for report group */
713
- function resultGroupValues(group, sections) {
714
- const { reports, baseline } = group;
715
- const baselineSamples = baseline?.measuredResults.samples;
716
- return {
717
- results: reports.map((report) => {
718
- const row = {
719
- name: truncate(report.name),
720
- ...extractReportValues(report, sections)
721
- };
722
- if (baselineSamples && report.measuredResults.samples) row.diffCI = bootstrapDifferenceCI(baselineSamples, report.measuredResults.samples);
723
- return row;
724
- }),
725
- baseline: baseline && valuesForReports([baseline], sections)[0]
726
- };
727
- }
728
- /** @return rows with stats from sections */
729
- function valuesForReports(reports, sections) {
730
- return reports.map((report) => ({
731
- name: truncate(report.name),
732
- ...extractReportValues(report, sections)
733
- }));
734
- }
735
- /** @return merged statistics from all sections */
736
- function extractReportValues(report, sections) {
737
- const { measuredResults, metadata } = report;
738
- const entries = sections.flatMap((s) => Object.entries(s.extract(measuredResults, metadata)));
739
- return Object.fromEntries(entries);
740
- }
741
- /** @return column groups with diff columns if baseline exists */
742
- function createColumnGroups(sections, hasBaseline) {
743
- const nameColumn = { columns: [{
744
- key: "name",
745
- title: "name"
746
- }] };
747
- const groups = sections.flatMap((section) => section.columns());
748
- return [nameColumn, ...hasBaseline ? injectDiffColumns(groups) : groups];
749
- }
750
- /** @return groups with single CI column after first comparable field */
751
- function injectDiffColumns(reportGroups) {
752
- let ciAdded = false;
753
- return reportGroups.map((group) => ({
754
- groupTitle: group.groupTitle,
755
- columns: group.columns.flatMap((col) => {
756
- if (col.comparable && !ciAdded) {
757
- ciAdded = true;
758
- return [col, {
759
- title: "Δ% CI",
760
- key: "diffCI",
761
- formatter: col.higherIsBetter ? formatDiffWithCIHigherIsBetter : formatDiffWithCI
762
- }];
763
- }
764
- return [col];
765
- })
766
- }));
767
- }
768
-
769
- //#endregion
770
- //#region src/cli/CliArgs.ts
771
- const defaultAdaptiveMaxTime = 20;
772
- const cliOptions = {
773
- time: {
774
- type: "number",
775
- default: .642,
776
- requiresArg: true,
777
- describe: "test duration in seconds"
778
- },
779
- cpu: {
780
- type: "boolean",
781
- default: false,
782
- describe: "CPU counter measurements (requires root)"
783
- },
784
- collect: {
785
- type: "boolean",
786
- default: false,
787
- describe: "force GC after each iteration"
788
- },
789
- "gc-stats": {
790
- type: "boolean",
791
- default: false,
792
- describe: "collect GC statistics (Node: --trace-gc-nvp, browser: CDP tracing)"
793
- },
794
- profile: {
795
- type: "boolean",
796
- default: false,
797
- describe: "run once for profiling"
798
- },
799
- filter: {
800
- type: "string",
801
- requiresArg: true,
802
- describe: "filter benchmarks by regex or substring"
803
- },
804
- all: {
805
- type: "boolean",
806
- default: false,
807
- describe: "run all cases (ignore defaultCases)"
808
- },
809
- worker: {
810
- type: "boolean",
811
- default: true,
812
- describe: "run in worker process for isolation (default: true)"
813
- },
814
- adaptive: {
815
- type: "boolean",
816
- default: false,
817
- describe: "adaptive sampling (experimental)"
818
- },
819
- "min-time": {
820
- type: "number",
821
- default: 1,
822
- describe: "minimum time before adaptive convergence can stop"
823
- },
824
- convergence: {
825
- type: "number",
826
- default: 95,
827
- describe: "adaptive confidence threshold (0-100)"
828
- },
829
- warmup: {
830
- type: "number",
831
- default: 0,
832
- describe: "warmup iterations before measurement"
833
- },
834
- html: {
835
- type: "boolean",
836
- default: false,
837
- describe: "generate HTML report and open in browser"
838
- },
839
- "export-html": {
840
- type: "string",
841
- requiresArg: true,
842
- describe: "export HTML report to specified file"
843
- },
844
- json: {
845
- type: "string",
846
- requiresArg: true,
847
- describe: "export benchmark data to JSON file"
848
- },
849
- perfetto: {
850
- type: "string",
851
- requiresArg: true,
852
- describe: "export Perfetto trace file (view at ui.perfetto.dev)"
853
- },
854
- "trace-opt": {
855
- type: "boolean",
856
- default: false,
857
- describe: "trace V8 optimization tiers (requires --allow-natives-syntax)"
858
- },
859
- "skip-settle": {
860
- type: "boolean",
861
- default: false,
862
- describe: "skip post-warmup settle time (see V8 optimization cold start)"
863
- },
864
- "pause-first": {
865
- type: "number",
866
- describe: "iterations before first pause (then pause-interval applies)"
867
- },
868
- "pause-interval": {
869
- type: "number",
870
- default: 0,
871
- describe: "iterations between pauses for V8 optimization (0 to disable)"
872
- },
873
- "pause-duration": {
874
- type: "number",
875
- default: 100,
876
- describe: "pause duration in ms for V8 optimization"
877
- },
878
- batches: {
879
- type: "number",
880
- default: 1,
881
- describe: "divide time into N batches, alternating baseline/current order"
882
- },
883
- iterations: {
884
- type: "number",
885
- requiresArg: true,
886
- describe: "exact number of iterations (overrides --time)"
887
- },
888
- "heap-sample": {
889
- type: "boolean",
890
- default: false,
891
- describe: "heap sampling allocation attribution (includes garbage)"
892
- },
893
- "heap-interval": {
894
- type: "number",
895
- default: 32768,
896
- describe: "heap sampling interval in bytes"
897
- },
898
- "heap-depth": {
899
- type: "number",
900
- default: 64,
901
- describe: "heap sampling stack depth"
902
- },
903
- "heap-rows": {
904
- type: "number",
905
- default: 20,
906
- describe: "top allocation sites to show"
907
- },
908
- "heap-stack": {
909
- type: "number",
910
- default: 3,
911
- describe: "call stack depth to display"
912
- },
913
- "heap-verbose": {
914
- type: "boolean",
915
- default: false,
916
- describe: "verbose output with file:// paths and line numbers"
917
- },
918
- "heap-user-only": {
919
- type: "boolean",
920
- default: false,
921
- describe: "filter to user code only (hide node internals)"
922
- },
923
- url: {
924
- type: "string",
925
- requiresArg: true,
926
- describe: "page URL for browser profiling (enables browser mode)"
927
- },
928
- headless: {
929
- type: "boolean",
930
- default: true,
931
- describe: "run browser in headless mode"
932
- },
933
- timeout: {
934
- type: "number",
935
- default: 60,
936
- describe: "browser page timeout in seconds"
937
- },
938
- "chrome-args": {
939
- type: "string",
940
- array: true,
941
- requiresArg: true,
942
- describe: "extra Chromium flags"
943
- }
944
- };
945
- /** @return yargs with standard benchmark options */
946
- function defaultCliArgs(yargsInstance) {
947
- return yargsInstance.command("$0 [file]", "run benchmarks", (y) => {
948
- y.positional("file", {
949
- type: "string",
950
- describe: "benchmark file to run"
951
- });
952
- }).options(cliOptions).help().strict();
953
- }
954
- /** @return parsed command line arguments */
955
- function parseCliArgs(args, configure = defaultCliArgs) {
956
- return configure(yargs(args)).parseSync();
957
- }
958
-
959
- //#endregion
960
- //#region src/export/JsonExport.ts
961
- /** Export benchmark results to JSON file */
962
- async function exportBenchmarkJson(groups, outputPath, args, suiteName = "Benchmark Suite") {
963
- const jsonData = prepareJsonData(groups, args, suiteName);
964
- await writeFile(outputPath, JSON.stringify(jsonData, null, 2), "utf-8");
965
- console.log(`Benchmark data exported to: ${outputPath}`);
966
- }
967
- /** Convert ReportGroup data to JSON format */
968
- function prepareJsonData(groups, args, suiteName) {
969
- return {
970
- meta: {
971
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
972
- version: process.env.npm_package_version || "unknown",
973
- args: cleanCliArgs(args),
974
- environment: {
975
- node: process.version,
976
- platform: process.platform,
977
- arch: process.arch
978
- }
979
- },
980
- suites: [{
981
- name: suiteName,
982
- groups: groups.map(convertGroup)
983
- }]
984
- };
985
- }
986
- /** Convert a report group, mapping each report to the JSON result format */
987
- function convertGroup(group) {
988
- return {
989
- name: "Benchmark Group",
990
- baseline: group.baseline ? convertReport(group.baseline) : void 0,
991
- benchmarks: group.reports.map(convertReport)
992
- };
993
- }
994
- /** Extract measured stats and optional metrics into JSON result shape */
995
- function convertReport(report) {
996
- const { name, measuredResults: m } = report;
997
- const { time, heapSize, gcTime, cpu } = m;
998
- const minMaxMean = (s) => s ? {
999
- min: s.min,
1000
- max: s.max,
1001
- mean: s.avg
1002
- } : void 0;
1003
- return {
1004
- name,
1005
- status: "completed",
1006
- samples: m.samples || [],
1007
- time: {
1008
- ...minMaxMean(time),
1009
- p50: time.p50,
1010
- p75: time.p75,
1011
- p99: time.p99,
1012
- p999: time.p999
1013
- },
1014
- heapSize: minMaxMean(heapSize),
1015
- gcTime: minMaxMean(gcTime),
1016
- cpu: cpu ? {
1017
- instructions: cpu.instructions,
1018
- cycles: cpu.cycles,
1019
- cacheMisses: m.cpuCacheMiss,
1020
- branchMisses: cpu.branchMisses
1021
- } : void 0,
1022
- execution: {
1023
- iterations: m.samples?.length || 0,
1024
- totalTime: m.totalTime || 0,
1025
- warmupRuns: void 0
1026
- }
1027
- };
1028
- }
1029
- /** Clean CLI args for JSON export (remove undefined values) */
1030
- function cleanCliArgs(args) {
1031
- const toCamel = (k) => k.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
1032
- const entries = Object.entries(args).filter(([, v]) => v !== void 0 && v !== null).map(([k, v]) => [toCamel(k), v]);
1033
- return Object.fromEntries(entries);
1034
- }
1035
-
1036
- //#endregion
1037
- //#region src/export/PerfettoExport.ts
1038
- const pid = 1;
1039
- const tid = 1;
1040
- /** Export benchmark results to Perfetto-compatible trace file */
1041
- function exportPerfettoTrace(groups, outputPath, args) {
1042
- const absPath = resolve(outputPath);
1043
- writeTraceFile(absPath, mergeV8Trace(buildTraceEvents(groups, args)));
1044
- console.log(`Perfetto trace exported to: ${outputPath}`);
1045
- scheduleDeferredMerge(absPath);
1046
- }
1047
- /** Build trace events from benchmark results */
1048
- function buildTraceEvents(groups, args) {
1049
- const meta = (name, a) => ({
1050
- ph: "M",
1051
- ts: 0,
1052
- pid,
1053
- tid,
1054
- name,
1055
- args: a
1056
- });
1057
- const events = [
1058
- meta("process_name", { name: "wesl-bench" }),
1059
- meta("thread_name", { name: "MainThread" }),
1060
- meta("bench_settings", cleanArgs(args))
1061
- ];
1062
- for (const group of groups) for (const report of group.reports) {
1063
- const results = report.measuredResults;
1064
- events.push(...buildBenchmarkEvents(results));
1065
- }
1066
- return events;
1067
- }
1068
- function instant(ts, name, args) {
1069
- return {
1070
- ph: "i",
1071
- ts,
1072
- pid,
1073
- tid,
1074
- cat: "bench",
1075
- name,
1076
- s: "t",
1077
- args
1078
- };
1079
- }
1080
- function counter(ts, name, args) {
1081
- return {
1082
- ph: "C",
1083
- ts,
1084
- pid,
1085
- tid,
1086
- cat: "bench",
1087
- name,
1088
- args
1089
- };
1090
- }
1091
- /** Build events for a single benchmark run */
1092
- function buildBenchmarkEvents(results) {
1093
- const { samples, heapSamples, timestamps, pausePoints } = results;
1094
- if (!timestamps?.length) return [];
1095
- const events = [];
1096
- for (let i = 0; i < samples.length; i++) {
1097
- const ts = timestamps[i];
1098
- const ms = Math.round(samples[i] * 100) / 100;
1099
- events.push(instant(ts, results.name, {
1100
- n: i,
1101
- ms
1102
- }));
1103
- events.push(counter(ts, "duration", { ms }));
1104
- if (heapSamples?.[i] !== void 0) {
1105
- const MB = Math.round(heapSamples[i] / 1024 / 1024 * 10) / 10;
1106
- events.push(counter(ts, "heap", { MB }));
1107
- }
1108
- }
1109
- for (const pause of pausePoints ?? []) {
1110
- const ts = timestamps[pause.sampleIndex];
1111
- if (ts) events.push(instant(ts, "pause", { ms: pause.durationMs }));
1112
- }
1113
- return events;
1114
- }
1115
- /** Normalize timestamps so events start at 0 */
1116
- function normalizeTimestamps(events) {
1117
- const times = events.filter((e) => e.ts > 0).map((e) => e.ts);
1118
- if (times.length === 0) return;
1119
- const minTs = Math.min(...times);
1120
- for (const e of events) if (e.ts > 0) e.ts -= minTs;
1121
- }
1122
- /** Merge V8 trace events from a previous run, aligning timestamps */
1123
- function mergeV8Trace(customEvents) {
1124
- const v8Events = loadV8Events(readdirSync(".").filter((f) => f.startsWith("node_trace.") && f.endsWith(".log"))[0]);
1125
- normalizeTimestamps(customEvents);
1126
- if (!v8Events) return customEvents;
1127
- normalizeTimestamps(v8Events);
1128
- return [...v8Events, ...customEvents];
1129
- }
1130
- /** Load V8 trace events from file, or undefined if unavailable */
1131
- function loadV8Events(v8TracePath) {
1132
- if (!v8TracePath) return void 0;
1133
- try {
1134
- const v8Data = JSON.parse(readFileSync(v8TracePath, "utf-8"));
1135
- console.log(`Merged ${v8Data.traceEvents.length} V8 events from ${v8TracePath}`);
1136
- return v8Data.traceEvents;
1137
- } catch {
1138
- console.warn(`Could not parse V8 trace file: ${v8TracePath}`);
1139
- return;
1140
- }
1141
- }
1142
- /** Write trace events to JSON file */
1143
- function writeTraceFile(outputPath, events) {
1144
- const traceFile = { traceEvents: events };
1145
- writeFileSync(outputPath, JSON.stringify(traceFile));
1146
- }
1147
- /** Clean CLI args for metadata */
1148
- function cleanArgs(args) {
1149
- const skip = new Set(["_", "$0"]);
1150
- const entries = Object.entries(args).filter(([k, v]) => v !== void 0 && !skip.has(k));
1151
- return Object.fromEntries(entries);
1152
- }
1153
- /** Spawn a detached child to merge V8 trace after process exit */
1154
- function scheduleDeferredMerge(outputPath) {
1155
- const cwd = process.cwd();
1156
- const mergeScript = `
1157
- const { readdirSync, readFileSync, writeFileSync } = require('fs');
1158
- function normalize(events) {
1159
- const times = events.filter(e => e.ts > 0).map(e => e.ts);
1160
- if (!times.length) return;
1161
- const min = Math.min(...times);
1162
- for (const e of events) if (e.ts > 0) e.ts -= min;
1163
- }
1164
- setTimeout(() => {
1165
- const traceFiles = readdirSync('.').filter(f => f.startsWith('node_trace.') && f.endsWith('.log'));
1166
- if (traceFiles.length === 0) process.exit(0);
1167
- try {
1168
- const v8Data = JSON.parse(readFileSync(traceFiles[0], 'utf-8'));
1169
- const ourData = JSON.parse(readFileSync('${outputPath}', 'utf-8'));
1170
- normalize(v8Data.traceEvents);
1171
- const merged = { traceEvents: [...v8Data.traceEvents, ...ourData.traceEvents] };
1172
- writeFileSync('${outputPath}', JSON.stringify(merged));
1173
- console.log('Merged ' + v8Data.traceEvents.length + ' V8 events into ' + '${outputPath}');
1174
- } catch (e) { console.error('Merge failed:', e.message); }
1175
- }, 100);
1176
- `;
1177
- process.on("exit", () => {
1178
- spawn("node", ["-e", mergeScript], {
1179
- detached: true,
1180
- stdio: "inherit",
1181
- cwd
1182
- }).unref();
1183
- });
1184
- }
1185
-
1186
- //#endregion
1187
- //#region src/HtmlDataPrep.ts
1188
- /** Find higherIsBetter from first comparable column in sections */
1189
- function findHigherIsBetter(sections) {
1190
- return (sections?.flatMap((s) => s.columns().flatMap((g) => g.columns)))?.find((c) => c.comparable)?.higherIsBetter ?? false;
1191
- }
1192
- /** Flip CI percent for metrics where higher is better (e.g., lines/sec) */
1193
- function flipCI(ci) {
1194
- return {
1195
- percent: -ci.percent,
1196
- ci: [-ci.ci[1], -ci.ci[0]],
1197
- direction: ci.direction,
1198
- histogram: ci.histogram?.map((bin) => ({
1199
- x: -bin.x,
1200
- count: bin.count
1201
- }))
1202
- };
1203
- }
1204
- /** Prepare ReportData from benchmark results for HTML rendering */
1205
- function prepareHtmlData(groups, options) {
1206
- const { cliArgs, sections, currentVersion, baselineVersion } = options;
1207
- const higherIsBetter = findHigherIsBetter(sections);
1208
- return {
1209
- groups: groups.map((group) => prepareGroupData(group, sections, higherIsBetter)),
1210
- metadata: {
1211
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1212
- bencherVersion: process.env.npm_package_version || "unknown",
1213
- cliArgs,
1214
- gcTrackingEnabled: cliArgs?.["gc-stats"] === true,
1215
- currentVersion,
1216
- baselineVersion
1217
- }
1218
- };
1219
- }
1220
- /** @return group data with bootstrap CI comparisons against baseline */
1221
- function prepareGroupData(group, sections, higherIsBetter) {
1222
- const baselineSamples = group.baseline?.measuredResults.samples;
1223
- return {
1224
- name: group.name,
1225
- baseline: group.baseline ? prepareBenchmarkData(group.baseline, sections) : void 0,
1226
- benchmarks: group.reports.map((report) => {
1227
- const samples = report.measuredResults.samples;
1228
- const rawCI = baselineSamples && samples ? bootstrapDifferenceCI(baselineSamples, samples) : void 0;
1229
- const comparisonCI = rawCI && higherIsBetter ? flipCI(rawCI) : rawCI;
1230
- return {
1231
- ...prepareBenchmarkData(report, sections),
1232
- comparisonCI
1233
- };
1234
- })
1235
- };
1236
- }
1237
- /** @return benchmark data with samples, stats, and formatted section values */
1238
- function prepareBenchmarkData(report, sections) {
1239
- const { measuredResults } = report;
1240
- return {
1241
- name: report.name,
1242
- samples: measuredResults.samples,
1243
- warmupSamples: measuredResults.warmupSamples,
1244
- allocationSamples: measuredResults.allocationSamples,
1245
- heapSamples: measuredResults.heapSamples,
1246
- gcEvents: measuredResults.nodeGcTime?.events,
1247
- optSamples: measuredResults.optSamples,
1248
- pausePoints: measuredResults.pausePoints,
1249
- stats: measuredResults.time,
1250
- heapSize: measuredResults.heapSize,
1251
- sectionStats: sections ? extractSectionStats(report, sections) : void 0
1252
- };
1253
- }
1254
- /** @return formatted stats from all sections for tooltip display */
1255
- function extractSectionStats(report, sections) {
1256
- return sections.flatMap((section) => {
1257
- const vals = section.extract(report.measuredResults, report.metadata);
1258
- return section.columns().flatMap((g) => formatGroupStats(vals, g));
1259
- });
1260
- }
1261
- /** @return formatted stats for one column group, skipping undefined values */
1262
- function formatGroupStats(values, group) {
1263
- return group.columns.map((c) => formatColumnStat(values, c, group.groupTitle)).filter((s) => s !== void 0);
1264
- }
1265
- /** @return formatted stat for a single column, or undefined if empty/placeholder */
1266
- function formatColumnStat(values, col, groupTitle) {
1267
- const raw = values[col.key];
1268
- if (raw === void 0) return void 0;
1269
- const formatted = col.formatter ? col.formatter(raw) : String(raw);
1270
- if (!formatted || formatted === "—" || formatted === "") return void 0;
1271
- return {
1272
- label: col.title,
1273
- value: formatted,
1274
- groupTitle
1275
- };
1276
- }
1277
-
1278
- //#endregion
1279
- //#region src/heap-sample/HeapSampleReport.ts
1280
- /** Sum selfSize across all nodes in profile (before any filtering) */
1281
- function totalProfileBytes(profile) {
1282
- let total = 0;
1283
- function walk(node) {
1284
- total += node.selfSize;
1285
- for (const child of node.children || []) walk(child);
1286
- }
1287
- walk(profile.head);
1288
- return total;
1289
- }
1290
- /** Flatten profile tree into sorted list of allocation sites with call stacks */
1291
- function flattenProfile(profile) {
1292
- const sites = [];
1293
- function walk(node, stack) {
1294
- const { functionName, url, lineNumber, columnNumber } = node.callFrame;
1295
- const fn = functionName || "(anonymous)";
1296
- const col = columnNumber ?? 0;
1297
- const frame = {
1298
- fn,
1299
- url: url || "",
1300
- line: lineNumber + 1,
1301
- col
1302
- };
1303
- const newStack = [...stack, frame];
1304
- if (node.selfSize > 0) sites.push({
1305
- ...frame,
1306
- bytes: node.selfSize,
1307
- stack: newStack
1308
- });
1309
- for (const child of node.children || []) walk(child, newStack);
1310
- }
1311
- walk(profile.head, []);
1312
- return sites.sort((a, b) => b.bytes - a.bytes);
1313
- }
1314
- /** Check if site is user code (not node internals) */
1315
- function isNodeUserCode(site) {
1316
- if (!site.url) return false;
1317
- if (site.url.startsWith("node:")) return false;
1318
- if (site.url.includes("(native)")) return false;
1319
- if (site.url.includes("internal/")) return false;
1320
- return true;
1321
- }
1322
- /** Check if site is user code (not browser internals) */
1323
- function isBrowserUserCode(site) {
1324
- if (!site.url) return false;
1325
- if (site.url.startsWith("chrome-extension://")) return false;
1326
- if (site.url.startsWith("devtools://")) return false;
1327
- if (site.url.includes("(native)")) return false;
1328
- return true;
1329
- }
1330
- /** Filter sites to user code only */
1331
- function filterSites(sites, isUser = isNodeUserCode) {
1332
- return sites.filter(isUser);
1333
- }
1334
- /** Aggregate sites by location (combine same file:line:col) */
1335
- function aggregateSites(sites) {
1336
- const byLocation = /* @__PURE__ */ new Map();
1337
- for (const site of sites) {
1338
- const key = `${site.url}:${site.line}:${site.col}`;
1339
- const existing = byLocation.get(key);
1340
- if (existing) existing.bytes += site.bytes;
1341
- else byLocation.set(key, { ...site });
1342
- }
1343
- return [...byLocation.values()].sort((a, b) => b.bytes - a.bytes);
1344
- }
1345
- function fmtBytes(bytes) {
1346
- if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
1347
- if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1348
- return `${bytes} B`;
1349
- }
1350
- /** Format heap report for console output */
1351
- function formatHeapReport(sites, options) {
1352
- const { topN, stackDepth = 3, verbose = false } = options;
1353
- const { totalAll, totalUserCode, sampleCount } = options;
1354
- const isUser = options.isUserCode ?? isNodeUserCode;
1355
- const lines = [];
1356
- lines.push(`Heap allocation sites (top ${topN}, garbage included):`);
1357
- for (const site of sites.slice(0, topN)) if (verbose) formatVerboseSite(lines, site, stackDepth, isUser);
1358
- else formatCompactSite(lines, site, stackDepth, isUser);
1359
- lines.push("");
1360
- if (totalAll !== void 0) lines.push(`Total (all): ${fmtBytes(totalAll)}`);
1361
- if (totalUserCode !== void 0) lines.push(`Total (user-code): ${fmtBytes(totalUserCode)}`);
1362
- if (sampleCount !== void 0) lines.push(`Samples: ${sampleCount.toLocaleString()}`);
1363
- return lines.join("\n");
1364
- }
1365
- /** Compact single-line format: `49 MB fn1 <- fn2 <- fn3` */
1366
- function formatCompactSite(lines, site, stackDepth, isUser) {
1367
- const bytes = fmtBytes(site.bytes).padStart(10);
1368
- const fns = [site.fn];
1369
- if (site.stack && site.stack.length > 1) {
1370
- const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
1371
- for (const frame of callers) {
1372
- if (!frame.url || !isUser(frame)) continue;
1373
- fns.push(frame.fn);
1374
- }
1375
- }
1376
- const line = `${bytes} ${fns.join(" <- ")}`;
1377
- lines.push(isUser(site) ? line : pico.dim(line));
1378
- }
1379
- /** Verbose multi-line format with file:// paths and line numbers */
1380
- function formatVerboseSite(lines, site, stackDepth, isUser) {
1381
- const bytes = fmtBytes(site.bytes).padStart(10);
1382
- const loc = site.url ? `${site.url}:${site.line}:${site.col}` : "(unknown)";
1383
- const dimFn = isUser(site) ? (s) => s : pico.dim;
1384
- lines.push(dimFn(`${bytes} ${site.fn} ${loc}`));
1385
- if (site.stack && site.stack.length > 1) {
1386
- const callers = site.stack.slice(0, -1).reverse().slice(0, stackDepth);
1387
- for (const frame of callers) {
1388
- if (!frame.url || !isUser(frame)) continue;
1389
- const callerLoc = `${frame.url}:${frame.line}:${frame.col}`;
1390
- lines.push(dimFn(` <- ${frame.fn} ${callerLoc}`));
1391
- }
1392
- }
1393
- }
1394
-
1395
- //#endregion
1396
- //#region src/html/HtmlTemplate.ts
1397
- const skipArgs = new Set([
1398
- "_",
1399
- "$0",
1400
- "html",
1401
- "export-html"
1402
- ]);
1403
- /** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
1404
- function formatDateWithTimezone(isoDate) {
1405
- const date = new Date(isoDate);
1406
- return `${date.toLocaleString("en-US", {
1407
- month: "short",
1408
- day: "numeric",
1409
- year: "numeric",
1410
- hour: "numeric",
1411
- minute: "2-digit"
1412
- })} (${date.toISOString().replace(".000Z", "Z")})`;
1413
- }
1414
- /** Format relative time: "5m ago", "2h ago", "yesterday", "3 days ago" */
1415
- function formatRelativeTime(isoDate) {
1416
- const date = new Date(isoDate);
1417
- const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
1418
- const diffMins = Math.floor(diffMs / 6e4);
1419
- const diffHours = Math.floor(diffMs / 36e5);
1420
- const diffDays = Math.floor(diffMs / 864e5);
1421
- if (diffMins < 1) return "just now";
1422
- if (diffMins < 60) return `${diffMins}m ago`;
1423
- if (diffHours < 24) return `${diffHours}h ago`;
1424
- if (diffDays === 1) return "yesterday";
1425
- if (diffDays < 30) return `${diffDays} days ago`;
1426
- return date.toLocaleDateString("en-US", {
1427
- month: "short",
1428
- day: "numeric"
1429
- });
1430
- }
1431
- /** Format git version for display: "abc1234* (5m ago)" */
1432
- function formatVersion(version) {
1433
- if (!version || version.hash === "unknown") return "unknown";
1434
- const hashDisplay = version.dirty ? `${version.hash}*` : version.hash;
1435
- const timeDisplay = version.date ? formatRelativeTime(version.date) : "";
1436
- return timeDisplay ? `${hashDisplay} (${timeDisplay})` : hashDisplay;
1437
- }
1438
- /** Render current/baseline version info as an HTML div */
1439
- function versionInfoHtml(data) {
1440
- const { currentVersion, baselineVersion } = data.metadata;
1441
- if (!currentVersion && !baselineVersion) return "";
1442
- const parts = [];
1443
- if (currentVersion) parts.push(`Current: ${formatVersion(currentVersion)}`);
1444
- if (baselineVersion) parts.push(`Baseline: ${formatVersion(baselineVersion)}`);
1445
- return `<div class="version-info">${parts.join(" | ")}</div>`;
1446
- }
1447
- const badgeLabels = {
1448
- faster: "Faster",
1449
- slower: "Slower",
1450
- uncertain: "Inconclusive"
1451
- };
1452
- /** Render faster/slower/uncertain badge with CI plot container */
1453
- function comparisonBadge(group, groupIndex) {
1454
- const ci = group.benchmarks[0]?.comparisonCI;
1455
- if (!ci) return "";
1456
- const label = badgeLabels[ci.direction];
1457
- return `
1458
- <span class="badge badge-${ci.direction}">${label}</span>
1459
- <div id="ci-plot-${groupIndex}" class="ci-plot-container"></div>
1460
- `;
1461
- }
1462
- const defaultArgs = {
1463
- worker: true,
1464
- time: 5,
1465
- warmup: 500,
1466
- "pause-interval": 0,
1467
- "pause-duration": 100
1468
- };
1469
- /** @return true if this CLI arg should be hidden from the report header */
1470
- function shouldSkipArg(key, value, adaptive) {
1471
- if (skipArgs.has(key) || value === void 0 || value === false) return true;
1472
- if (defaultArgs[key] === value) return true;
1473
- if (!key.includes("-") && key !== key.toLowerCase()) return true;
1474
- if (key === "convergence" && !adaptive) return true;
1475
- return false;
1476
- }
1477
- /** Reconstruct the CLI invocation string, omitting default/internal args */
1478
- function formatCliArgs(args) {
1479
- if (!args) return "bb bench";
1480
- const parts = ["bb bench"];
1481
- for (const [key, value] of Object.entries(args)) {
1482
- if (shouldSkipArg(key, value, args.adaptive)) continue;
1483
- parts.push(value === true ? `--${key}` : `--${key} ${value}`);
1484
- }
1485
- return parts.join(" ");
1486
- }
1487
- /** Generate complete HTML document with embedded data and visualizations */
1488
- function generateHtmlDocument(data) {
1489
- return `<!DOCTYPE html>
1490
- <html lang="en">
1491
- <head>
1492
- <meta charset="UTF-8">
1493
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1494
- <title>Benchmark Report - ${(/* @__PURE__ */ new Date()).toLocaleDateString()}</title>
1495
- <style>
1496
- * { margin: 0; padding: 0; box-sizing: border-box; }
1497
- body {
1498
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1499
- background: #f5f5f5;
1500
- padding: 20px;
1501
- line-height: 1.6;
1502
- }
1503
- .header {
1504
- background: white;
1505
- padding: 10px 15px;
1506
- border-radius: 8px;
1507
- margin-bottom: 20px;
1508
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1509
- display: flex;
1510
- justify-content: space-between;
1511
- align-items: center;
1512
- }
1513
- h1 { display: none; }
1514
- h2 {
1515
- color: #555;
1516
- margin: 30px 0 20px;
1517
- font-size: 20px;
1518
- border-bottom: 2px solid #e0e0e0;
1519
- padding-bottom: 10px;
1520
- }
1521
- .metadata { color: #666; font-size: 12px; }
1522
- .cli-args {
1523
- font-family: "SF Mono", Monaco, "Consolas", monospace;
1524
- font-size: 11px;
1525
- color: #555;
1526
- background: #f0f0f0;
1527
- padding: 6px 10px;
1528
- border-radius: 4px;
1529
- word-break: break-word;
1530
- }
1531
- .comparison-mode {
1532
- background: #fff3cd;
1533
- color: #856404;
1534
- padding: 8px 12px;
1535
- border-radius: 4px;
1536
- display: inline-block;
1537
- margin-top: 10px;
1538
- font-weight: 500;
1539
- }
1540
- .plot-grid {
1541
- display: grid;
1542
- grid-template-columns: 1fr 1fr;
1543
- gap: 20px;
1544
- margin-bottom: 30px;
1545
- }
1546
- .plot-grid.second-row { grid-template-columns: 1fr; }
1547
- .plot-container {
1548
- background: white;
1549
- padding: 20px;
1550
- border-radius: 8px;
1551
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1552
- }
1553
- .plot-container.full-width { grid-column: 1 / -1; }
1554
- .plot-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; color: #333; }
1555
- .plot-description { font-size: 14px; color: #666; margin-bottom: 15px; }
1556
- .plot-area {
1557
- display: flex;
1558
- justify-content: center;
1559
- align-items: center;
1560
- min-height: 300px;
1561
- }
1562
- .plot-area svg { overflow: visible; }
1563
- .plot-area svg g[aria-label="x-axis label"] text { font-size: 14px; }
1564
- .summary-stats { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-top: 20px; }
1565
- .stats-grid {
1566
- display: grid;
1567
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1568
- gap: 10px;
1569
- margin-top: 10px;
1570
- }
1571
- .stat-item { background: white; padding: 10px; border-radius: 4px; text-align: center; }
1572
- .stat-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
1573
- .stat-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 4px; }
1574
- .loading { color: #666; font-style: italic; padding: 20px; text-align: center; }
1575
- .error { color: #d32f2f; background: #ffebee; padding: 15px; border-radius: 4px; margin: 10px 0; }
1576
- .ci-faster { color: #22c55e; }
1577
- .ci-slower { color: #ef4444; }
1578
- .ci-uncertain { color: #6b7280; }
1579
- .group-header {
1580
- display: flex;
1581
- align-items: center;
1582
- gap: 12px;
1583
- margin: 30px 0 20px;
1584
- padding-bottom: 10px;
1585
- border-bottom: 2px solid #e0e0e0;
1586
- }
1587
- .group-header h2 { margin: 0; border: none; padding: 0; }
1588
- .badge {
1589
- font-size: 12px;
1590
- font-weight: 600;
1591
- padding: 4px 10px;
1592
- border-radius: 12px;
1593
- text-transform: uppercase;
1594
- letter-spacing: 0.5px;
1595
- }
1596
- .badge-faster { background: #dcfce7; color: #166534; }
1597
- .badge-slower { background: #fee2e2; color: #991b1b; }
1598
- .badge-uncertain { background: #dbeafe; color: #1e40af; }
1599
- .version-info { font-size: 12px; color: #666; margin-top: 6px; }
1600
- .header-right { text-align: right; }
1601
- .ci-plot-container { display: inline-block; vertical-align: middle; margin-left: 8px; }
1602
- .ci-plot-container svg { display: block; }
1603
- </style>
1604
- </head>
1605
- <body>
1606
- <div class="header">
1607
- <div class="cli-args">${formatCliArgs(data.metadata.cliArgs)}</div>
1608
- <div class="header-right">
1609
- <div class="metadata">Generated: ${formatDateWithTimezone((/* @__PURE__ */ new Date()).toISOString())}</div>
1610
- ${versionInfoHtml(data)}
1611
- </div>
1612
- </div>
1613
-
1614
- ${data.groups.map((group, i) => `
1615
- <div id="group-${i}">
1616
- ${group.benchmarks.length > 0 ? `
1617
- <div class="group-header">
1618
- <h2>${group.name}</h2>
1619
- ${comparisonBadge(group, i)}
1620
- </div>
1621
-
1622
- <div class="plot-grid">
1623
- <div class="plot-container">
1624
- <div class="plot-title">Time per Sample</div>
1625
- <div class="plot-description">Execution time for each sample in collection order</div>
1626
- <div id="sample-timeseries-${i}" class="plot-area">
1627
- <div class="loading">Loading time series...</div>
1628
- </div>
1629
- </div>
1630
-
1631
- <div class="plot-container">
1632
- <div class="plot-title">Time Distribution</div>
1633
- <div class="plot-description">Frequency distribution of execution times</div>
1634
- <div id="histogram-${i}" class="plot-area">
1635
- <div class="loading">Loading histogram...</div>
1636
- </div>
1637
- </div>
1638
- </div>
1639
-
1640
- <div id="stats-${i}"></div>
1641
- ` : "<div class=\"error\">No benchmark data available for this group</div>"}
1642
- </div>
1643
- `).join("")}
1644
-
1645
- <script type="importmap">
1646
- {
1647
- "imports": {
1648
- "d3": "https://cdn.jsdelivr.net/npm/d3@7/+esm",
1649
- "@observablehq/plot": "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm"
1650
- }
1651
- }
1652
- <\/script>
1653
- <script type="module">
1654
- import { renderPlots } from "./plots.js";
1655
- const benchmarkData = ${JSON.stringify(data, null, 2)};
1656
- renderPlots(benchmarkData);
1657
- <\/script>
1658
- </body>
1659
- </html>`;
1660
- }
1661
-
1662
- //#endregion
1663
- //#region src/html/HtmlReport.ts
1664
- /** Generate HTML report from prepared data and optionally open in browser */
1665
- async function generateHtmlReport(data, options) {
1666
- const html = generateHtmlDocument(data);
1667
- const reportDir = options.outputPath || await createReportDir();
1668
- await mkdir(reportDir, { recursive: true });
1669
- await writeFile(join(reportDir, "index.html"), html, "utf-8");
1670
- const plots = await loadPlotsBundle();
1671
- await writeFile(join(reportDir, "plots.js"), plots, "utf-8");
1672
- await writeLatestRedirect(reportDir);
1673
- let server;
1674
- let closeServer;
1675
- if (options.openBrowser) {
1676
- const baseDir = dirname(reportDir);
1677
- const reportName = reportDir.split("/").pop();
1678
- const result = await startReportServer(baseDir, 7979, 7978, 7977);
1679
- server = result.server;
1680
- closeServer = () => result.server.close();
1681
- const openUrl = `http://localhost:${result.port}/${reportName}/`;
1682
- await open(openUrl);
1683
- console.log(`Report opened in browser: ${openUrl}`);
1684
- } else console.log(`Report saved to: ${reportDir}/`);
1685
- return {
1686
- reportDir,
1687
- server,
1688
- closeServer
1689
- };
1690
- }
1691
- /** Start HTTP server for report directory, trying fallback ports if needed */
1692
- async function startReportServer(baseDir, ...ports) {
1693
- const mimeTypes = {
1694
- ".html": "text/html",
1695
- ".js": "application/javascript",
1696
- ".css": "text/css",
1697
- ".json": "application/json"
1698
- };
1699
- const server = createServer(async (req, res) => {
1700
- const url = req.url || "/";
1701
- const filePath = join(baseDir, url.endsWith("/") ? url + "index.html" : url);
1702
- try {
1703
- const content = await readFile(filePath);
1704
- const mime = mimeTypes[extname(filePath)] || "application/octet-stream";
1705
- res.setHeader("Content-Type", mime);
1706
- res.end(content);
1707
- } catch {
1708
- res.statusCode = 404;
1709
- res.end("Not found");
1710
- }
1711
- });
1712
- for (const port of ports) try {
1713
- return await tryListen(server, port);
1714
- } catch {}
1715
- return tryListen(server, 0);
1716
- }
1717
- /** Listen on a port, resolving with the actual port or rejecting on error */
1718
- function tryListen(server, port) {
1719
- return new Promise((resolve, reject) => {
1720
- server.once("error", reject);
1721
- server.listen(port, () => {
1722
- server.removeListener("error", reject);
1723
- const addr = server.address();
1724
- resolve({
1725
- server,
1726
- port: typeof addr === "object" && addr ? addr.port : port
1727
- });
1728
- });
1729
- });
1730
- }
1731
- /** Create a timestamped report directory under ./bench-report/ */
1732
- async function createReportDir() {
1733
- const base = "./bench-report";
1734
- await mkdir(base, { recursive: true });
1735
- return join(base, `report-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`);
1736
- }
1737
- /** Read the pre-built browser plots bundle from dist/ */
1738
- async function loadPlotsBundle() {
1739
- const thisDir = dirname(fileURLToPath(import.meta.url));
1740
- const builtPath = join(thisDir, "browser/index.js");
1741
- const devPath = join(thisDir, "../../dist/browser/index.js");
1742
- try {
1743
- return await readFile(builtPath, "utf-8");
1744
- } catch {}
1745
- return readFile(devPath, "utf-8");
1746
- }
1747
- /** Write an index.html in the parent dir that redirects to this report */
1748
- async function writeLatestRedirect(reportDir) {
1749
- const baseDir = dirname(reportDir);
1750
- const reportName = reportDir.split("/").pop();
1751
- const html = `<!DOCTYPE html>
1752
- <html><head>
1753
- <meta http-equiv="refresh" content="0; url=./${reportName}/">
1754
- <script>location.href = "./${reportName}/";<\/script>
1755
- </head><body>
1756
- <a href="./${reportName}/">Latest report</a>
1757
- </body></html>`;
1758
- await writeFile(join(baseDir, "index.html"), html, "utf-8");
1759
- }
1760
-
1761
- //#endregion
1762
- //#region src/matrix/MatrixFilter.ts
1763
- /** Parse filter string: "case/variant", "case/", "/variant", or "case" */
1764
- function parseMatrixFilter(filter) {
1765
- if (filter.includes("/")) {
1766
- const [casePart, variantPart] = filter.split("/", 2);
1767
- return {
1768
- case: casePart || void 0,
1769
- variant: variantPart || void 0
1770
- };
1771
- }
1772
- return { case: filter };
1773
- }
1774
- /** Apply filter to a matrix, merging with existing filters via intersection */
1775
- async function filterMatrix(matrix, filter) {
1776
- if (!filter || !filter.case && !filter.variant) return matrix;
1777
- const caseList = await getFilteredCases(matrix, filter.case);
1778
- const variantList = await getFilteredVariants(matrix, filter.variant);
1779
- const filteredCases = caseList && matrix.filteredCases ? caseList.filter((c) => matrix.filteredCases.includes(c)) : caseList ?? matrix.filteredCases;
1780
- const filteredVariants = variantList && matrix.filteredVariants ? variantList.filter((v) => matrix.filteredVariants.includes(v)) : variantList ?? matrix.filteredVariants;
1781
- return {
1782
- ...matrix,
1783
- filteredCases,
1784
- filteredVariants
1785
- };
1786
- }
1787
- /** Get case IDs matching filter pattern */
1788
- async function getFilteredCases(matrix, casePattern) {
1789
- if (!casePattern) return void 0;
1790
- const caseIds = matrix.casesModule ? (await loadCasesModule(matrix.casesModule)).cases : matrix.cases;
1791
- if (!caseIds) return ["default"];
1792
- const filtered = caseIds.filter((id) => matchPattern(id, casePattern));
1793
- if (filtered.length === 0) throw new Error(`No cases match filter: "${casePattern}"`);
1794
- return filtered;
1795
- }
1796
- /** Get variant IDs matching filter pattern */
1797
- async function getFilteredVariants(matrix, variantPattern) {
1798
- if (!variantPattern) return void 0;
1799
- if (matrix.variants) {
1800
- const ids = Object.keys(matrix.variants).filter((id) => matchPattern(id, variantPattern));
1801
- if (ids.length === 0) throw new Error(`No variants match filter: "${variantPattern}"`);
1802
- return ids;
1803
- }
1804
- if (matrix.variantDir) {
1805
- const filtered = (await discoverVariants(matrix.variantDir)).filter((id) => matchPattern(id, variantPattern));
1806
- if (filtered.length === 0) throw new Error(`No variants match filter: "${variantPattern}"`);
1807
- return filtered;
1808
- }
1809
- throw new Error("BenchMatrix requires 'variants' or 'variantDir'");
1810
- }
1811
- /** Match id against pattern (case-insensitive substring) */
1812
- function matchPattern(id, pattern) {
1813
- return id.toLowerCase().includes(pattern.toLowerCase());
1814
- }
1815
-
1816
- //#endregion
1817
- //#region src/table-util/ConvergenceFormatters.ts
1818
- const { red } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? { red: (str) => str } : pico;
1819
- const lowConfidence = 80;
1820
- /** @return convergence percentage with color for low values */
1821
- function formatConvergence(v) {
1822
- if (typeof v !== "number") return "—";
1823
- const pct = `${Math.round(v)}%`;
1824
- return v < lowConfidence ? red(pct) : pct;
1825
- }
1826
-
1827
- //#endregion
1828
- //#region src/StandardSections.ts
1829
- /** Section: mean, p50, p99 timing */
1830
- const timeSection = {
1831
- extract: (results) => ({
1832
- mean: results.time?.avg,
1833
- p50: results.time?.p50,
1834
- p99: results.time?.p99
1835
- }),
1836
- columns: () => [{
1837
- groupTitle: "time",
1838
- columns: [
1839
- {
1840
- key: "mean",
1841
- title: "mean",
1842
- formatter: timeMs,
1843
- comparable: true
1844
- },
1845
- {
1846
- key: "p50",
1847
- title: "p50",
1848
- formatter: timeMs,
1849
- comparable: true
1850
- },
1851
- {
1852
- key: "p99",
1853
- title: "p99",
1854
- formatter: timeMs,
1855
- comparable: true
1856
- }
1857
- ]
1858
- }]
1859
- };
1860
- /** Section: GC time as fraction of total benchmark time (Node performance hooks) */
1861
- const gcSection = {
1862
- extract: (results) => {
1863
- const { nodeGcTime, time, samples } = results;
1864
- if (!nodeGcTime || !time?.avg) return { gc: void 0 };
1865
- const totalBenchTime = time.avg * samples.length;
1866
- if (totalBenchTime <= 0) return { gc: void 0 };
1867
- const gcTime = nodeGcTime.inRun / totalBenchTime;
1868
- return { gc: gcTime <= 1 ? gcTime : void 0 };
1869
- },
1870
- columns: () => [{
1871
- groupTitle: "gc",
1872
- columns: [{
1873
- key: "gc",
1874
- title: "mean",
1875
- formatter: percent,
1876
- comparable: true
1877
- }]
1878
- }]
1879
- };
1880
- /** Section: detailed GC stats from --trace-gc-nvp (allocation, promotion, pauses) */
1881
- const gcStatsSection = {
1882
- extract: (results) => {
1883
- const { gcStats, samples } = results;
1884
- if (!gcStats) return {};
1885
- const iterations = samples.length || 1;
1886
- const { totalAllocated, totalPromoted } = gcStats;
1887
- const promoPercent = totalAllocated && totalAllocated > 0 ? (totalPromoted ?? 0) / totalAllocated : void 0;
1888
- return {
1889
- allocPerIter: totalAllocated != null ? totalAllocated / iterations : void 0,
1890
- collected: gcStats.totalCollected || void 0,
1891
- scavenges: gcStats.scavenges,
1892
- fullGCs: gcStats.markCompacts,
1893
- promoPercent,
1894
- pausePerIter: gcStats.gcPauseTime / iterations
1895
- };
1896
- },
1897
- columns: () => [{
1898
- groupTitle: "gc",
1899
- columns: [
1900
- {
1901
- key: "allocPerIter",
1902
- title: "alloc/iter",
1903
- formatter: formatBytes
1904
- },
1905
- {
1906
- key: "collected",
1907
- title: "collected",
1908
- formatter: formatBytes
1909
- },
1910
- {
1911
- key: "scavenges",
1912
- title: "scav",
1913
- formatter: integer
1914
- },
1915
- {
1916
- key: "fullGCs",
1917
- title: "full",
1918
- formatter: integer
1919
- },
1920
- {
1921
- key: "promoPercent",
1922
- title: "promo%",
1923
- formatter: percent
1924
- },
1925
- {
1926
- key: "pausePerIter",
1927
- title: "pause/iter",
1928
- formatter: timeMs
1929
- }
1930
- ]
1931
- }]
1932
- };
1933
- /** Browser GC section: only fields available from CDP tracing */
1934
- const browserGcStatsSection = {
1935
- extract: gcStatsSection.extract,
1936
- columns: () => [{
1937
- groupTitle: "gc",
1938
- columns: [
1939
- {
1940
- key: "collected",
1941
- title: "collected",
1942
- formatter: formatBytes
1943
- },
1944
- {
1945
- key: "scavenges",
1946
- title: "scav",
1947
- formatter: integer
1948
- },
1949
- {
1950
- key: "fullGCs",
1951
- title: "full",
1952
- formatter: integer
1953
- },
1954
- {
1955
- key: "pausePerIter",
1956
- title: "pause",
1957
- formatter: timeMs
1958
- }
1959
- ]
1960
- }]
1961
- };
1962
- /** Section: CPU L1 cache miss rate and stall rate (requires @mitata/counters) */
1963
- const cpuSection = {
1964
- extract: (results) => ({
1965
- cpuCacheMiss: results.cpuCacheMiss,
1966
- cpuStall: results.cpuStall
1967
- }),
1968
- columns: () => [{
1969
- groupTitle: "cpu",
1970
- columns: [{
1971
- key: "cpuCacheMiss",
1972
- title: "L1 miss",
1973
- formatter: percent
1974
- }, {
1975
- key: "cpuStall",
1976
- title: "stalls",
1977
- formatter: percentPrecision(2)
1978
- }]
1979
- }]
1980
- };
1981
- /** Section: number of sample iterations */
1982
- const runsSection = {
1983
- extract: (results) => ({ runs: results.samples.length }),
1984
- columns: () => [{ columns: [{
1985
- key: "runs",
1986
- title: "runs",
1987
- formatter: integer
1988
- }] }]
1989
- };
1990
- /** Section: total sampling duration in seconds (brackets if >= 30s) */
1991
- const totalTimeSection = {
1992
- extract: (results) => ({ totalTime: results.totalTime }),
1993
- columns: () => [{ columns: [{
1994
- key: "totalTime",
1995
- title: "time",
1996
- formatter: (v) => {
1997
- if (typeof v !== "number") return "";
1998
- return v >= 30 ? `[${v.toFixed(1)}s]` : `${v.toFixed(1)}s`;
1999
- }
2000
- }] }]
2001
- };
2002
- /** Section: median, mean, p99, and convergence for adaptive mode */
2003
- const adaptiveSection = {
2004
- extract: (results) => ({
2005
- median: results.time?.p50,
2006
- mean: results.time?.avg,
2007
- p99: results.time?.p99,
2008
- convergence: results.convergence?.confidence
2009
- }),
2010
- columns: () => [{
2011
- groupTitle: "time",
2012
- columns: [
2013
- {
2014
- key: "median",
2015
- title: "median",
2016
- formatter: timeMs,
2017
- comparable: true
2018
- },
2019
- {
2020
- key: "mean",
2021
- title: "mean",
2022
- formatter: timeMs,
2023
- comparable: true
2024
- },
2025
- {
2026
- key: "p99",
2027
- title: "p99",
2028
- formatter: timeMs
2029
- }
2030
- ]
2031
- }, { columns: [{
2032
- key: "convergence",
2033
- title: "conv%",
2034
- formatter: formatConvergence
2035
- }] }]
2036
- };
2037
- /** Build generic sections based on CLI flags */
2038
- function buildGenericSections(args) {
2039
- const sections = [];
2040
- if (args["gc-stats"]) sections.push(gcStatsSection);
2041
- sections.push(runsSection);
2042
- return sections;
2043
- }
2044
- /** Section: V8 optimization tier distribution and deopt count */
2045
- const optSection = {
2046
- extract: (results) => {
2047
- const opt = results.optStatus;
2048
- if (!opt) return {};
2049
- const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
2050
- return {
2051
- tiers: Object.entries(opt.byTier).sort((a, b) => b[1].count - a[1].count).map(([name, t]) => `${name}:${(t.count / total * 100).toFixed(0)}%`).join(" "),
2052
- deopt: opt.deoptCount > 0 ? opt.deoptCount : void 0
2053
- };
2054
- },
2055
- columns: () => [{
2056
- groupTitle: "v8 opt",
2057
- columns: [{
2058
- key: "tiers",
2059
- title: "tiers",
2060
- formatter: (v) => typeof v === "string" ? v : ""
2061
- }, {
2062
- key: "deopt",
2063
- title: "deopt",
2064
- formatter: (v) => typeof v === "number" ? String(v) : ""
2065
- }]
2066
- }]
2067
- };
2068
-
2069
- //#endregion
2070
- //#region src/matrix/MatrixReport.ts
2071
- /** Format matrix results as one table per case */
2072
- function reportMatrixResults(results, options) {
2073
- const tables = buildCaseTables(results, options);
2074
- return [`Matrix: ${results.name}`, ...tables].join("\n\n");
2075
- }
2076
- /** Build one table for each case showing all variants */
2077
- function buildCaseTables(results, options) {
2078
- if (results.variants.length === 0) return [];
2079
- return results.variants[0].cases.map((c) => c.caseId).map((caseId) => buildCaseTable(results, caseId, options));
2080
- }
2081
- /** Build table for a single case showing all variants */
2082
- function buildCaseTable(results, caseId, options) {
2083
- const caseTitle = formatCaseTitle(results, caseId);
2084
- if (options?.sections?.length) return buildSectionTable(results, caseId, options, caseTitle);
2085
- const rows = buildCaseRows(results, caseId, options?.extraColumns);
2086
- return `${caseTitle}\n${buildTable(buildColumns(rows.some((r) => r.diffCI), options), [{ results: rows }])}`;
2087
- }
2088
- /** Build table using ResultsMapper sections */
2089
- function buildSectionTable(results, caseId, options, caseTitle) {
2090
- const sections = options.sections;
2091
- const variantTitle = options.variantTitle ?? "name";
2092
- const rows = [];
2093
- let hasBaseline = false;
2094
- for (const variant of results.variants) {
2095
- const caseResult = variant.cases.find((c) => c.caseId === caseId);
2096
- if (!caseResult) continue;
2097
- const row = { name: truncate(variant.id, 25) };
2098
- for (const section of sections) Object.assign(row, section.extract(caseResult.measured, caseResult.metadata));
2099
- if (caseResult.baseline) {
2100
- hasBaseline = true;
2101
- const { samples: base } = caseResult.baseline;
2102
- row.diffCI = bootstrapDifferenceCI(base, caseResult.measured.samples);
2103
- }
2104
- rows.push(row);
2105
- }
2106
- return `${caseTitle}\n${buildTable(buildSectionColumns(sections, variantTitle, hasBaseline), [{ results: rows }])}`;
2107
- }
2108
- /** Build column groups from ResultsMapper sections */
2109
- function buildSectionColumns(sections, variantTitle, hasBaseline) {
2110
- const nameCol = { columns: [{
2111
- key: "name",
2112
- title: variantTitle
2113
- }] };
2114
- const sectionColumns = sections.flatMap((s) => s.columns());
2115
- return [nameCol, ...hasBaseline ? injectDiffColumns(sectionColumns) : sectionColumns];
2116
- }
2117
- /** Build rows for all variants for a given case */
2118
- function buildCaseRows(results, caseId, extraColumns) {
2119
- return results.variants.flatMap((variant) => {
2120
- const caseResult = variant.cases.find((c) => c.caseId === caseId);
2121
- return caseResult ? [buildRow(variant.id, caseResult, extraColumns)] : [];
2122
- });
2123
- }
2124
- /** Build a single row from case result */
2125
- function buildRow(variantId, caseResult, extraColumns) {
2126
- const { measured, baseline } = caseResult;
2127
- const samples = measured.samples;
2128
- const time = measured.time?.avg ?? average(samples);
2129
- const row = {
2130
- name: truncate(variantId, 25),
2131
- time,
2132
- samples: samples.length
2133
- };
2134
- if (baseline) row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
2135
- if (extraColumns) for (const col of extraColumns) row[col.key] = col.extract(caseResult);
2136
- return row;
2137
- }
2138
- /** Build column configuration */
2139
- function buildColumns(hasBaseline, options) {
2140
- const groups = [{ columns: [{
2141
- key: "name",
2142
- title: options?.variantTitle ?? "variant"
2143
- }] }, { columns: [{
2144
- key: "time",
2145
- title: "time",
2146
- formatter: duration
2147
- }, ...hasBaseline ? [{
2148
- key: "diffCI",
2149
- title: "Δ% CI",
2150
- formatter: formatDiff
2151
- }] : []] }];
2152
- const extraColumns = options?.extraColumns;
2153
- if (extraColumns?.length) {
2154
- const byGroup = /* @__PURE__ */ new Map();
2155
- for (const col of extraColumns) {
2156
- const group = byGroup.get(col.groupTitle) ?? [];
2157
- group.push(col);
2158
- byGroup.set(col.groupTitle, group);
2159
- }
2160
- for (const [groupTitle, cols] of byGroup) groups.push({
2161
- groupTitle,
2162
- columns: cols.map((col) => ({
2163
- key: col.key,
2164
- title: col.title,
2165
- formatter: col.formatter ?? String
2166
- }))
2167
- });
2168
- }
2169
- return groups;
2170
- }
2171
- /** Format diff with CI, or "baseline" marker */
2172
- function formatDiff(value) {
2173
- if (!value) return null;
2174
- return formatDiffWithCI(value);
2175
- }
2176
- /** Format case title with metadata if available */
2177
- function formatCaseTitle(results, caseId) {
2178
- const metadata = (results.variants[0]?.cases.find((c) => c.caseId === caseId))?.metadata;
2179
- if (metadata && Object.keys(metadata).length > 0) return `${caseId} (${Object.entries(metadata).map(([k, v]) => `${v} ${k}`).join(", ")})`;
2180
- return caseId;
2181
- }
2182
- /** GC statistics columns - derived from gcStatsSection for consistency */
2183
- const gcStatsColumns = gcStatsSection.columns()[0].columns.map((col) => ({
2184
- key: col.key,
2185
- title: col.title,
2186
- groupTitle: "GC",
2187
- extract: (r) => gcStatsSection.extract(r.measured)[col.key],
2188
- formatter: (v) => col.formatter?.(v) ?? "-"
2189
- }));
2190
- /** Format bytes with fallback to "-" for missing values */
2191
- function formatBytesOrDash(value) {
2192
- return formatBytes(value) ?? "-";
2193
- }
2194
- /** GC pause time column */
2195
- const gcPauseColumn = {
2196
- key: "gcPause",
2197
- title: "pause",
2198
- groupTitle: "GC",
2199
- extract: (r) => r.measured.gcStats?.gcPauseTime,
2200
- formatter: (v) => v != null ? `${v.toFixed(1)}ms` : "-"
2201
- };
2202
- /** Heap sampling total bytes column */
2203
- const heapTotalColumn = {
2204
- key: "heapTotal",
2205
- title: "heap",
2206
- extract: (r) => {
2207
- const profile = r.measured.heapProfile;
2208
- if (!profile?.head) return void 0;
2209
- return totalProfileBytes(profile);
2210
- },
2211
- formatter: formatBytesOrDash
2212
- };
2213
-
2214
- //#endregion
2215
- //#region src/cli/FilterBenchmarks.ts
2216
- /** Filter benchmarks by name pattern */
2217
- function filterBenchmarks(suite, filter, removeEmpty = true) {
2218
- if (!filter) return suite;
2219
- const regex = createFilterRegex(filter);
2220
- const groups = suite.groups.map((group) => ({
2221
- ...group,
2222
- benchmarks: group.benchmarks.filter((bench) => regex.test(stripCaseSuffix(bench.name))),
2223
- baseline: group.baseline && regex.test(stripCaseSuffix(group.baseline.name)) ? group.baseline : void 0
2224
- })).filter((group) => !removeEmpty || group.benchmarks.length > 0);
2225
- validateFilteredSuite(groups, filter);
2226
- return {
2227
- name: suite.name,
2228
- groups
2229
- };
2230
- }
2231
- /** Create regex from filter (literal unless regex-like) */
2232
- function createFilterRegex(filter) {
2233
- if (filter.startsWith("/") && filter.endsWith("/") || filter.includes("*") || filter.includes("?") || filter.includes("[") || filter.includes("|") || filter.startsWith("^") || filter.endsWith("$")) {
2234
- const pattern = filter.startsWith("/") && filter.endsWith("/") ? filter.slice(1, -1) : filter;
2235
- try {
2236
- return new RegExp(pattern, "i");
2237
- } catch {
2238
- return new RegExp(escapeRegex(filter), "i");
2239
- }
2240
- }
2241
- return new RegExp("^" + escapeRegex(filter), "i");
2242
- }
2243
- /** Strip case suffix like " [large]" from benchmark name for filtering */
2244
- function stripCaseSuffix(name) {
2245
- return name.replace(/ \[.*?\]$/, "");
2246
- }
2247
- /** Escape regex special characters */
2248
- function escapeRegex(str) {
2249
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2250
- }
2251
- /** Ensure at least one benchmark matches filter */
2252
- function validateFilteredSuite(groups, filter) {
2253
- if (groups.every((g) => g.benchmarks.length === 0)) throw new Error(`No benchmarks match filter: "${filter}"`);
2254
- }
2255
-
2256
- //#endregion
2257
- //#region src/cli/RunBenchCLI.ts
2258
- /** Validate CLI argument combinations */
2259
- function validateArgs(args) {
2260
- if (args["gc-stats"] && !args.worker && !args.url) throw new Error("--gc-stats requires worker mode (the default). Remove --no-worker flag.");
2261
- }
2262
- /** Warn about Node-only flags that are ignored in browser mode. */
2263
- function warnBrowserFlags(args) {
2264
- const ignored = [];
2265
- if (!args.worker) ignored.push("--no-worker");
2266
- if (args.cpu) ignored.push("--cpu");
2267
- if (args["trace-opt"]) ignored.push("--trace-opt");
2268
- if (args.collect) ignored.push("--collect");
2269
- if (args.adaptive) ignored.push("--adaptive");
2270
- if (args.batches > 1) ignored.push("--batches");
2271
- if (ignored.length) console.warn(yellow(`Ignored in browser mode: ${ignored.join(", ")}`));
2272
- }
2273
- /** Parse CLI with custom configuration */
2274
- function parseBenchArgs(configureArgs) {
2275
- return parseCliArgs(hideBin(process.argv), configureArgs);
2276
- }
2277
- /** Run suite with CLI arguments */
2278
- async function runBenchmarks(suite, args) {
2279
- validateArgs(args);
2280
- const { filter, worker: useWorker, batches = 1 } = args;
2281
- const options = cliToRunnerOptions(args);
2282
- return runSuite({
2283
- suite: filterBenchmarks(suite, filter),
2284
- runner: "basic",
2285
- options,
2286
- useWorker,
2287
- batches
2288
- });
2289
- }
2290
- /** Execute all groups in suite */
2291
- async function runSuite(params) {
2292
- const { suite, runner, options, useWorker, batches } = params;
2293
- const results = [];
2294
- for (const group of suite.groups) results.push(await runGroup(group, runner, options, useWorker, batches));
2295
- return results;
2296
- }
2297
- /** Execute group with shared setup, optionally batching to reduce ordering bias */
2298
- async function runGroup(group, runner, options, useWorker, batches = 1) {
2299
- const { name, benchmarks, baseline, setup, metadata } = group;
2300
- const setupParams = await setup?.();
2301
- validateBenchmarkParameters(group);
2302
- const runParams = {
2303
- runner,
2304
- options,
2305
- useWorker,
2306
- params: setupParams,
2307
- metadata
2308
- };
2309
- if (batches === 1) return runSingleBatch(name, benchmarks, baseline, runParams);
2310
- return runMultipleBatches(name, benchmarks, baseline, runParams, batches);
2311
- }
2312
- /** Run benchmarks in a single batch */
2313
- async function runSingleBatch(name, benchmarks, baseline, runParams) {
2314
- const baselineReport = baseline ? await runSingleBenchmark(baseline, runParams) : void 0;
2315
- return {
2316
- name,
2317
- reports: await serialMap(benchmarks, (b) => runSingleBenchmark(b, runParams)),
2318
- baseline: baselineReport
2319
- };
2320
- }
2321
- /** Run benchmarks in multiple batches, alternating order to reduce bias */
2322
- async function runMultipleBatches(name, benchmarks, baseline, runParams, batches) {
2323
- const timePerBatch = (runParams.options.maxTime || 5e3) / batches;
2324
- const batchParams = {
2325
- ...runParams,
2326
- options: {
2327
- ...runParams.options,
2328
- maxTime: timePerBatch
2329
- }
2330
- };
2331
- const baselineBatches = [];
2332
- const benchmarkBatches = /* @__PURE__ */ new Map();
2333
- for (let i = 0; i < batches; i++) await runBatchIteration(benchmarks, baseline, batchParams, i % 2 === 1, baselineBatches, benchmarkBatches);
2334
- const meta = runParams.metadata;
2335
- return mergeBatchResults(name, benchmarks, baseline, baselineBatches, benchmarkBatches, meta);
2336
- }
2337
- /** Run one batch iteration in either order */
2338
- async function runBatchIteration(benchmarks, baseline, runParams, reverseOrder, baselineBatches, benchmarkBatches) {
2339
- const runBaseline = async () => {
2340
- if (baseline) {
2341
- const r = await runSingleBenchmark(baseline, runParams);
2342
- baselineBatches.push(r.measuredResults);
2343
- }
2344
- };
2345
- const runBenches = async () => {
2346
- for (const b of benchmarks) {
2347
- const r = await runSingleBenchmark(b, runParams);
2348
- appendToMap(benchmarkBatches, b.name, r.measuredResults);
2349
- }
2350
- };
2351
- if (reverseOrder) {
2352
- await runBenches();
2353
- await runBaseline();
2354
- } else {
2355
- await runBaseline();
2356
- await runBenches();
2357
- }
2358
- }
2359
- /** Merge batch results into final ReportGroup */
2360
- function mergeBatchResults(name, benchmarks, baseline, baselineBatches, benchmarkBatches, metadata) {
2361
- const mergedBaseline = baseline ? {
2362
- name: baseline.name,
2363
- measuredResults: mergeResults(baselineBatches),
2364
- metadata
2365
- } : void 0;
2366
- return {
2367
- name,
2368
- reports: benchmarks.map((b) => ({
2369
- name: b.name,
2370
- measuredResults: mergeResults(benchmarkBatches.get(b.name) || []),
2371
- metadata
2372
- })),
2373
- baseline: mergedBaseline
2374
- };
2375
- }
2376
- /** Run single benchmark and create report */
2377
- async function runSingleBenchmark(spec, runParams) {
2378
- const { runner, options, useWorker, params, metadata } = runParams;
2379
- const [result] = await runBenchmark({
2380
- spec,
2381
- runner,
2382
- options,
2383
- useWorker,
2384
- params
2385
- });
2386
- return {
2387
- name: spec.name,
2388
- measuredResults: result,
2389
- metadata
2390
- };
2391
- }
2392
- /** Warn if parameterized benchmarks lack setup */
2393
- function validateBenchmarkParameters(group) {
2394
- const { name, setup, benchmarks, baseline } = group;
2395
- if (setup) return;
2396
- const allBenchmarks = baseline ? [...benchmarks, baseline] : benchmarks;
2397
- for (const benchmark of allBenchmarks) if (benchmark.fn.length > 0) console.warn(`Benchmark "${benchmark.name}" in group "${name}" expects parameters but no setup() provided.`);
2398
- }
2399
- /** Merge multiple batch results into a single MeasuredResults */
2400
- function mergeResults(results) {
2401
- if (results.length === 0) throw new Error("Cannot merge empty results array");
2402
- if (results.length === 1) return results[0];
2403
- const allSamples = results.flatMap((r) => r.samples);
2404
- const allWarmup = results.flatMap((r) => r.warmupSamples || []);
2405
- const time = computeStats(allSamples);
2406
- let offset = 0;
2407
- const allPausePoints = results.flatMap((r) => {
2408
- const pts = (r.pausePoints ?? []).map((p) => ({
2409
- sampleIndex: p.sampleIndex + offset,
2410
- durationMs: p.durationMs
2411
- }));
2412
- offset += r.samples.length;
2413
- return pts;
2414
- });
2415
- return {
2416
- name: results[0].name,
2417
- samples: allSamples,
2418
- warmupSamples: allWarmup.length ? allWarmup : void 0,
2419
- time,
2420
- totalTime: results.reduce((sum, r) => sum + (r.totalTime || 0), 0),
2421
- pausePoints: allPausePoints.length ? allPausePoints : void 0
2422
- };
2423
- }
2424
- function appendToMap(map, key, value) {
2425
- if (!map.has(key)) map.set(key, []);
2426
- map.get(key).push(value);
2427
- }
2428
- /** Generate table with standard sections */
2429
- function defaultReport(groups, args) {
2430
- const { adaptive, "gc-stats": gcStats, "trace-opt": traceOpt } = args;
2431
- const hasCpu = hasField(groups, "cpu");
2432
- const hasOpt = hasField(groups, "optStatus");
2433
- return reportResults(groups, buildReportSections(adaptive, gcStats, hasCpu, traceOpt && hasOpt));
2434
- }
2435
- /** Build report sections based on CLI options */
2436
- function buildReportSections(adaptive, gcStats, hasCpuData, hasOptData) {
2437
- const sections = adaptive ? [adaptiveSection, totalTimeSection] : [timeSection];
2438
- if (gcStats) sections.push(gcStatsSection);
2439
- if (hasCpuData) sections.push(cpuSection);
2440
- if (hasOptData) sections.push(optSection);
2441
- sections.push(runsSection);
2442
- return sections;
2443
- }
2444
- /** Run benchmarks, display table, and optionally generate HTML report */
2445
- async function benchExports(suite, args) {
2446
- const results = await runBenchmarks(suite, args);
2447
- const report = defaultReport(results, args);
2448
- console.log(report);
2449
- await finishReports(results, args, suite.name);
2450
- }
2451
- /** Run browser profiling via Playwright + CDP, report with standard pipeline */
2452
- async function browserBenchExports(args) {
2453
- warnBrowserFlags(args);
2454
- let profileBrowser;
2455
- try {
2456
- ({profileBrowser} = await import("./BrowserHeapSampler-DCeL42RE.mjs"));
2457
- } catch {
2458
- throw new Error("playwright is required for browser benchmarking (--url).\n\nQuick start: npx benchforge-browser --url <your-url>\n\nOr install manually:\n npm install playwright\n npx playwright install chromium");
2459
- }
2460
- const url = args.url;
2461
- const { iterations, time } = args;
2462
- const result = await profileBrowser({
2463
- url,
2464
- heapSample: args["heap-sample"],
2465
- heapOptions: {
2466
- samplingInterval: args["heap-interval"],
2467
- stackDepth: args["heap-depth"]
2468
- },
2469
- headless: args.headless,
2470
- chromeArgs: args["chrome-args"]?.flatMap((a) => a.split(/\s+/)).map(stripQuotes).filter(Boolean),
2471
- timeout: args.timeout,
2472
- gcStats: args["gc-stats"],
2473
- maxTime: iterations ? Number.MAX_SAFE_INTEGER : time * 1e3,
2474
- maxIterations: iterations
2475
- });
2476
- const results = browserResultGroups(new URL(url).pathname.split("/").pop() || "browser", result);
2477
- printBrowserReport(result, results, args);
2478
- await exportReports({
2479
- results,
2480
- args
2481
- });
2482
- }
2483
- /** Print browser benchmark tables and heap reports */
2484
- function printBrowserReport(result, results, args) {
2485
- const hasSamples = result.samples && result.samples.length > 0;
2486
- const sections = [];
2487
- if (hasSamples || result.wallTimeMs != null) sections.push(timeSection);
2488
- if (result.gcStats) sections.push(browserGcStatsSection);
2489
- if (hasSamples || result.wallTimeMs != null) sections.push(runsSection);
2490
- if (sections.length > 0) console.log(reportResults(results, sections));
2491
- if (result.heapProfile) printHeapReports(results, {
2492
- ...cliHeapReportOptions(args),
2493
- isUserCode: isBrowserUserCode
2494
- });
2495
- }
2496
- /** Wrap browser profile result as ReportGroup[] for the standard pipeline */
2497
- function browserResultGroups(name, result) {
2498
- const { gcStats, heapProfile } = result;
2499
- let measured;
2500
- if (result.samples && result.samples.length > 0) {
2501
- const { samples } = result;
2502
- const totalTime = result.wallTimeMs ? result.wallTimeMs / 1e3 : void 0;
2503
- measured = {
2504
- name,
2505
- samples,
2506
- time: computeStats(samples),
2507
- totalTime,
2508
- gcStats,
2509
- heapProfile
2510
- };
2511
- } else {
2512
- const wallMs = result.wallTimeMs ?? 0;
2513
- measured = {
2514
- name,
2515
- samples: [wallMs],
2516
- time: {
2517
- min: wallMs,
2518
- max: wallMs,
2519
- avg: wallMs,
2520
- p50: wallMs,
2521
- p75: wallMs,
2522
- p99: wallMs,
2523
- p999: wallMs
2524
- },
2525
- gcStats,
2526
- heapProfile
2527
- };
2528
- }
2529
- return [{
2530
- name,
2531
- reports: [{
2532
- name,
2533
- measuredResults: measured
2534
- }]
2535
- }];
2536
- }
2537
- /** Print heap allocation reports for benchmarks with heap profiles */
2538
- function printHeapReports(groups, options) {
2539
- for (const group of groups) {
2540
- const allReports = group.baseline ? [...group.reports, group.baseline] : group.reports;
2541
- for (const report of allReports) {
2542
- const { heapProfile } = report.measuredResults;
2543
- if (!heapProfile) continue;
2544
- console.log(dim(`\n─── Heap profile: ${report.name} ───`));
2545
- const totalAll = totalProfileBytes(heapProfile);
2546
- const sites = flattenProfile(heapProfile);
2547
- const userSites = filterSites(sites, options.isUserCode);
2548
- const totalUserCode = userSites.reduce((sum, s) => sum + s.bytes, 0);
2549
- const aggregated = aggregateSites(options.userOnly ? userSites : sites);
2550
- const extra = {
2551
- totalAll,
2552
- totalUserCode,
2553
- sampleCount: heapProfile.samples?.length
2554
- };
2555
- console.log(formatHeapReport(aggregated, {
2556
- ...options,
2557
- ...extra
2558
- }));
2559
- }
2560
- }
2561
- }
2562
- /** Run benchmarks and display table. Suite is optional with --url (browser mode). */
2563
- async function runDefaultBench(suite, configureArgs) {
2564
- const args = parseBenchArgs(configureArgs);
2565
- if (args.url) await browserBenchExports(args);
2566
- else if (suite) await benchExports(suite, args);
2567
- else if (args.file) await fileBenchExports(args.file, args);
2568
- else throw new Error("Provide a benchmark file, --url for browser mode, or pass a BenchSuite directly.");
2569
- }
2570
- /** Import a file and run it as a benchmark based on what it exports */
2571
- async function fileBenchExports(filePath, args) {
2572
- const candidate = (await import(pathToFileURL(resolve(filePath)).href)).default;
2573
- if (candidate && Array.isArray(candidate.groups)) await benchExports(candidate, args);
2574
- else if (typeof candidate === "function") {
2575
- const name = basename(filePath).replace(/\.[^.]+$/, "");
2576
- await benchExports({
2577
- name,
2578
- groups: [{
2579
- name,
2580
- benchmarks: [{
2581
- name,
2582
- fn: candidate
2583
- }]
2584
- }]
2585
- }, args);
2586
- }
2587
- }
2588
- /** Convert CLI args to runner options */
2589
- function cliToRunnerOptions(args) {
2590
- const { profile, collect, iterations } = args;
2591
- if (profile) return {
2592
- maxIterations: iterations ?? 1,
2593
- warmupTime: 0,
2594
- collect
2595
- };
2596
- if (args.adaptive) return createAdaptiveOptions(args);
2597
- return {
2598
- maxTime: iterations ? Number.POSITIVE_INFINITY : args.time * 1e3,
2599
- maxIterations: iterations,
2600
- ...cliCommonOptions(args)
2601
- };
2602
- }
2603
- /** Create options for adaptive mode */
2604
- function createAdaptiveOptions(args) {
2605
- return {
2606
- minTime: (args["min-time"] ?? 1) * 1e3,
2607
- maxTime: defaultAdaptiveMaxTime * 1e3,
2608
- targetConfidence: args.convergence,
2609
- adaptive: true,
2610
- ...cliCommonOptions(args)
2611
- };
2612
- }
2613
- /** Runner/matrix options shared across all CLI modes */
2614
- function cliCommonOptions(args) {
2615
- const { collect, cpu, warmup } = args;
2616
- const { "trace-opt": traceOpt, "skip-settle": noSettle } = args;
2617
- const { "pause-first": pauseFirst, "pause-interval": pauseInterval } = args;
2618
- const { "pause-duration": pauseDuration, "gc-stats": gcStats } = args;
2619
- const { "heap-sample": heapSample, "heap-interval": heapInterval } = args;
2620
- const { "heap-depth": heapDepth } = args;
2621
- return {
2622
- collect,
2623
- cpuCounters: cpu,
2624
- warmup,
2625
- traceOpt,
2626
- noSettle,
2627
- pauseFirst,
2628
- pauseInterval,
2629
- pauseDuration,
2630
- gcStats,
2631
- heapSample,
2632
- heapInterval,
2633
- heapDepth
2634
- };
2635
- }
2636
- const { yellow, dim } = process.env.NODE_ENV === "test" || process.env.VITEST === "true" ? {
2637
- yellow: (s) => s,
2638
- dim: (s) => s
2639
- } : pico;
2640
- /** Log V8 optimization tier distribution and deoptimizations */
2641
- function reportOptStatus(groups) {
2642
- const optData = groups.flatMap(({ reports, baseline }) => {
2643
- return (baseline ? [...reports, baseline] : reports).filter((r) => r.measuredResults.optStatus).map((r) => ({
2644
- name: r.name,
2645
- opt: r.measuredResults.optStatus,
2646
- samples: r.measuredResults.samples.length
2647
- }));
2648
- });
2649
- if (optData.length === 0) return;
2650
- console.log(dim("\nV8 optimization:"));
2651
- for (const { name, opt, samples } of optData) {
2652
- const total = Object.values(opt.byTier).reduce((s, t) => s + t.count, 0);
2653
- const tierParts = Object.entries(opt.byTier).sort((a, b) => b[1].count - a[1].count).map(([tier, info]) => `${tier} ${(info.count / total * 100).toFixed(0)}%`).join(", ");
2654
- console.log(` ${name}: ${tierParts} ${dim(`(${samples} samples)`)}`);
2655
- }
2656
- const totalDeopts = optData.reduce((s, d) => s + d.opt.deoptCount, 0);
2657
- if (totalDeopts > 0) console.log(yellow(` ⚠ ${totalDeopts} deoptimization${totalDeopts > 1 ? "s" : ""} detected`));
2658
- }
2659
- /** @return true if any result has the specified field with a defined value */
2660
- function hasField(results, field) {
2661
- return results.some(({ reports, baseline }) => {
2662
- return (baseline ? [...reports, baseline] : reports).some(({ measuredResults }) => measuredResults[field] !== void 0);
2663
- });
2664
- }
2665
- /** Print heap reports (if enabled) and export results */
2666
- async function finishReports(results, args, suiteName, exportOptions) {
2667
- if (args["heap-sample"]) printHeapReports(results, cliHeapReportOptions(args));
2668
- await exportReports({
2669
- results,
2670
- args,
2671
- suiteName,
2672
- ...exportOptions
2673
- });
2674
- }
2675
- /** Export reports (HTML, JSON, Perfetto) based on CLI args */
2676
- async function exportReports(options) {
2677
- const { results, args, sections, suiteName } = options;
2678
- const { currentVersion, baselineVersion } = options;
2679
- const openInBrowser = args.html && !args["export-html"];
2680
- let closeServer;
2681
- if (args.html || args["export-html"]) closeServer = (await generateHtmlReport(prepareHtmlData(results, {
2682
- cliArgs: args,
2683
- sections,
2684
- currentVersion,
2685
- baselineVersion
2686
- }), {
2687
- openBrowser: openInBrowser,
2688
- outputPath: args["export-html"]
2689
- })).closeServer;
2690
- if (args.json) await exportBenchmarkJson(results, args.json, args, suiteName);
2691
- if (args.perfetto) exportPerfettoTrace(results, args.perfetto, args);
2692
- if (openInBrowser) {
2693
- await waitForCtrlC();
2694
- closeServer?.();
2695
- }
2696
- }
2697
- /** Wait for Ctrl+C before exiting */
2698
- function waitForCtrlC() {
2699
- return new Promise((resolve) => {
2700
- console.log(dim("\nPress Ctrl+C to exit"));
2701
- process.on("SIGINT", () => {
2702
- console.log();
2703
- resolve();
2704
- });
2705
- });
2706
- }
2707
- /** Run matrix suite with CLI arguments.
2708
- * no options ==> defaultCases/defaultVariants, --filter ==> subset of defaults,
2709
- * --all --filter ==> subset of all, --all ==> all cases/variants */
2710
- async function runMatrixSuite(suite, args) {
2711
- validateArgs(args);
2712
- const filter = args.filter ? parseMatrixFilter(args.filter) : void 0;
2713
- const options = cliToMatrixOptions(args);
2714
- const results = [];
2715
- for (const matrix of suite.matrices) {
2716
- const casesModule = matrix.casesModule ? await loadCasesModule(matrix.casesModule) : void 0;
2717
- let filtered = matrix;
2718
- if (!args.all && casesModule) filtered = {
2719
- ...matrix,
2720
- filteredCases: casesModule.defaultCases,
2721
- filteredVariants: casesModule.defaultVariants
2722
- };
2723
- if (filter) filtered = await filterMatrix(filtered, filter);
2724
- const { filteredCases, filteredVariants } = filtered;
2725
- results.push(await runMatrix(filtered, {
2726
- ...options,
2727
- filteredCases,
2728
- filteredVariants
2729
- }));
2730
- }
2731
- return results;
2732
- }
2733
- /** Convert CLI args to matrix run options */
2734
- function cliToMatrixOptions(args) {
2735
- const { time, iterations, worker } = args;
2736
- return {
2737
- iterations,
2738
- maxTime: iterations ? void 0 : time * 1e3,
2739
- useWorker: worker,
2740
- ...cliCommonOptions(args)
2741
- };
2742
- }
2743
- /** Generate report for matrix results. Uses same sections as regular benchmarks. */
2744
- function defaultMatrixReport(results, reportOptions, args) {
2745
- const options = args ? mergeMatrixDefaults(reportOptions, args, results) : reportOptions;
2746
- return results.map((r) => reportMatrixResults(r, options)).join("\n\n");
2747
- }
2748
- /** @return HeapReportOptions from CLI args */
2749
- function cliHeapReportOptions(args) {
2750
- return {
2751
- topN: args["heap-rows"],
2752
- stackDepth: args["heap-stack"],
2753
- verbose: args["heap-verbose"],
2754
- userOnly: args["heap-user-only"]
2755
- };
2756
- }
2757
- /** Apply default sections and extra columns for matrix reports */
2758
- function mergeMatrixDefaults(reportOptions, args, results) {
2759
- const result = { ...reportOptions };
2760
- if (!result.sections?.length) {
2761
- const groups = matrixToReportGroups(results);
2762
- result.sections = buildReportSections(args.adaptive, args["gc-stats"], hasField(groups, "cpu"), args["trace-opt"] && hasField(groups, "optStatus"));
2763
- }
2764
- return result;
2765
- }
2766
- /** Run matrix suite with full CLI handling (parse, run, report, export) */
2767
- async function runDefaultMatrixBench(suite, configureArgs, reportOptions) {
2768
- await matrixBenchExports(suite, parseBenchArgs(configureArgs), reportOptions);
2769
- }
2770
- /** Convert MatrixResults to ReportGroup[] for export compatibility */
2771
- function matrixToReportGroups(results) {
2772
- return results.flatMap((matrix) => matrix.variants.flatMap((variant) => variant.cases.map((c) => {
2773
- const { metadata } = c;
2774
- const report = {
2775
- name: variant.id,
2776
- measuredResults: c.measured,
2777
- metadata
2778
- };
2779
- const baseline = c.baseline ? {
2780
- name: `${variant.id} (baseline)`,
2781
- measuredResults: c.baseline,
2782
- metadata
2783
- } : void 0;
2784
- return {
2785
- name: `${variant.id} / ${c.caseId}`,
2786
- reports: [report],
2787
- baseline
2788
- };
2789
- })));
2790
- }
2791
- /** Strip surrounding quotes from a chrome arg token.
2792
- *
2793
- * (Needed because --chrome-args values pass through yargs and spawn() without
2794
- * shell processing, so literal quote characters reach Chrome/V8 unrecognized.)
2795
- */
2796
- function stripQuotes(s) {
2797
- return s.replace(/^(['"])(.*)\1$/s, "$2").replace(/^(-[^=]+=)(['"])(.*)\2$/s, "$1$3");
2798
- }
2799
- /** Sequential map - like Promise.all(arr.map(fn)) but runs one at a time */
2800
- async function serialMap(arr, fn) {
2801
- const results = [];
2802
- for (const item of arr) results.push(await fn(item));
2803
- return results;
2804
- }
2805
- /** Run matrix benchmarks, display table, and generate exports */
2806
- async function matrixBenchExports(suite, args, reportOptions, exportOptions) {
2807
- const results = await runMatrixSuite(suite, args);
2808
- const report = defaultMatrixReport(results, reportOptions, args);
2809
- console.log(report);
2810
- await finishReports(matrixToReportGroups(results), args, suite.name, exportOptions);
2811
- }
2812
-
2813
- //#endregion
2814
- //#region src/GitUtils.ts
2815
- /** Get current git version info. For dirty repos, uses most recent modified file date. */
2816
- function getCurrentGitVersion() {
2817
- try {
2818
- const exec = (cmd) => execSync(cmd, { encoding: "utf-8" }).trim();
2819
- const hash = exec("git rev-parse --short HEAD");
2820
- const commitDate = exec("git log -1 --format=%aI");
2821
- const dirty = exec("git status --porcelain").length > 0;
2822
- return {
2823
- hash,
2824
- date: dirty ? getMostRecentModifiedDate(".") ?? commitDate : commitDate,
2825
- dirty
2826
- };
2827
- } catch {
2828
- return;
2829
- }
2830
- }
2831
- /** Read baseline version from .baseline-version file */
2832
- function getBaselineVersion(baselineDir = "_baseline") {
2833
- const versionFile = join(baselineDir, ".baseline-version");
2834
- if (!existsSync(versionFile)) return void 0;
2835
- try {
2836
- const content = readFileSync(versionFile, "utf-8");
2837
- const data = JSON.parse(content);
2838
- return {
2839
- hash: data.hash,
2840
- date: data.date
2841
- };
2842
- } catch {
2843
- return;
2844
- }
2845
- }
2846
- /** Format git version for display: "abc1234 (Jan 9, 2026, 3:45 PM)" or "abc1234*" if dirty */
2847
- function formatGitVersion(version) {
2848
- return `${version.dirty ? `${version.hash}*` : version.hash} (${formatDateWithTimezone(version.date)})`;
2849
- }
2850
- /** Get most recent modified file date in a directory (for dirty repos) */
2851
- function getMostRecentModifiedDate(dir) {
2852
- try {
2853
- const modifiedFiles = execSync("git status --porcelain", {
2854
- encoding: "utf-8",
2855
- cwd: dir
2856
- }).trim().split("\n").filter((line) => line.length > 0).map((line) => line.slice(3));
2857
- if (modifiedFiles.length === 0) return void 0;
2858
- let mostRecent = 0;
2859
- for (const file of modifiedFiles) try {
2860
- const filePath = join(dir, file);
2861
- if (!existsSync(filePath)) continue;
2862
- const mtime = statSync(filePath).mtimeMs;
2863
- if (mtime > mostRecent) mostRecent = mtime;
2864
- } catch {}
2865
- return mostRecent > 0 ? new Date(mostRecent).toISOString() : void 0;
2866
- } catch {
2867
- return;
2868
- }
2869
- }
2870
-
2871
- //#endregion
2872
- export { timeSection as A, parseCliArgs as B, adaptiveSection as C, gcStatsSection as D, gcSection as E, generateHtmlReport as F, truncate as G, formatBytes as H, formatDateWithTimezone as I, loadCaseData as J, isStatefulVariant as K, prepareHtmlData as L, formatConvergence as M, filterMatrix as N, optSection as O, parseMatrixFilter as P, exportPerfettoTrace as R, reportMatrixResults as S, cpuSection as T, integer as U, reportResults as V, timeMs as W, loadCasesModule as Y, runDefaultMatrixBench as _, cliToMatrixOptions as a, gcStatsColumns as b, exportReports as c, matrixToReportGroups as d, parseBenchArgs as f, runDefaultBench as g, runBenchmarks as h, benchExports as i, totalTimeSection as j, runsSection as k, hasField as l, reportOptStatus as m, getBaselineVersion as n, defaultMatrixReport as o, printHeapReports as p, runMatrix as q, getCurrentGitVersion as r, defaultReport as s, formatGitVersion as t, matrixBenchExports as u, runMatrixSuite as v, buildGenericSections as w, heapTotalColumn as x, gcPauseColumn as y, defaultCliArgs as z };
2873
- //# sourceMappingURL=src-Cf_LXwlp.mjs.map