benchforge 0.1.11 → 0.2.4

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