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
@@ -3,73 +3,186 @@ import yargs from "yargs";
3
3
 
4
4
  export type Configure<T> = (yargs: Argv) => Argv<T>;
5
5
 
6
- /** CLI args type inferred from cliOptions, plus optional file positional */
6
+ /** CLI args type inferred from cliOptions, plus optional file positional. */
7
7
  export type DefaultCliArgs = InferredOptionTypes<typeof cliOptions> & {
8
8
  file?: string;
9
9
  };
10
10
 
11
- export const defaultAdaptiveMaxTime = 20;
12
-
13
11
  // biome-ignore format: compact option definitions
14
12
  const cliOptions = {
15
- time: { type: "number", default: 0.642, requiresArg: true, describe: "test duration in seconds" },
16
- cpu: { type: "boolean", default: false, describe: "CPU counter measurements (requires root)" },
17
- collect: { type: "boolean", default: false, describe: "force GC after each iteration" },
18
- "gc-stats": { type: "boolean", default: false, describe: "collect GC statistics (Node: --trace-gc-nvp, browser: CDP tracing)" },
19
- profile: { type: "boolean", default: false, describe: "run once for profiling" },
20
- filter: { type: "string", requiresArg: true, describe: "filter benchmarks by regex or substring" },
21
- all: { type: "boolean", default: false, describe: "run all cases (ignore defaultCases)" },
22
- worker: { type: "boolean", default: true, describe: "run in worker process for isolation (default: true)" },
23
- adaptive: { type: "boolean", default: false, describe: "adaptive sampling (experimental)" },
24
- "min-time": { type: "number", default: 1, describe: "minimum time before adaptive convergence can stop" },
25
- convergence: { type: "number", default: 95, describe: "adaptive confidence threshold (0-100)" },
26
- warmup: { type: "number", default: 0, describe: "warmup iterations before measurement" },
27
- html: { type: "boolean", default: false, describe: "generate HTML report and open in browser" },
28
- "export-html": { type: "string", requiresArg: true, describe: "export HTML report to specified file" },
29
- json: { type: "string", requiresArg: true, describe: "export benchmark data to JSON file" },
30
- "export-perfetto": { type: "string", requiresArg: true, describe: "export Perfetto trace file (view at ui.perfetto.dev)" },
31
- speedscope: { type: "boolean", default: false, describe: "open heap profile in speedscope (via npx)" },
32
- "export-speedscope": { type: "string", requiresArg: true, describe: "export heap profile as speedscope JSON" },
33
- "trace-opt": { type: "boolean", default: false, describe: "trace V8 optimization tiers (requires --allow-natives-syntax)" },
34
- "skip-settle": { type: "boolean", default: false, describe: "skip post-warmup settle time (see V8 optimization cold start)" },
35
- "pause-first": { type: "number", describe: "iterations before first pause (then pause-interval applies)" },
13
+ duration: { type: "number", requiresArg: true, describe: "duration per batch in seconds (default: 0.642)" },
14
+ iterations: { type: "number", requiresArg: true, describe: "iterations per batch (page loads for page-load mode, inner loop for bench)" },
15
+ warmup: { type: "number", default: 0, describe: "warmup iterations before measurement" },
16
+ filter: { type: "string", requiresArg: true, describe: "filter by name/regex. Matrix: case/variant, case/, /variant" },
17
+ all: { type: "boolean", default: false, describe: "run all cases (ignore defaultCases)" },
18
+ list: { type: "boolean", default: false, describe: "list available benchmarks (or matrix cases/variants)" },
19
+ worker: { type: "boolean", default: true, describe: "run in worker process for isolation (default: true)" },
20
+ batches: { type: "number", default: 1, describe: "divide time into N batches, alternating baseline/current order" },
21
+ "warmup-batch": { type: "boolean", default: false, describe: "include first batch in results (normally dropped to avoid OS cache warmup)" },
22
+ "equiv-margin": { type: "number", default: 2, describe: "equivalence margin % for baseline comparison (0 to disable)" },
23
+ "no-batch-trim": { type: "boolean", default: false, describe: "disable Tukey trimming of outlier batches" },
24
+ "pause-first": { type: "number", describe: "iterations before first pause (then pause-interval applies)" },
36
25
  "pause-interval": { type: "number", default: 0, describe: "iterations between pauses for V8 optimization (0 to disable)" },
37
26
  "pause-duration": { type: "number", default: 100, describe: "pause duration in ms for V8 optimization" },
38
- batches: { type: "number", default: 1, describe: "divide time into N batches, alternating baseline/current order" },
39
- iterations: { type: "number", requiresArg: true, describe: "exact number of iterations (overrides --time)" },
40
- "heap-sample": { type: "boolean", default: false, describe: "heap sampling allocation attribution (includes garbage)" },
41
- "heap-interval": { type: "number", default: 32768, describe: "heap sampling interval in bytes" },
42
- "heap-depth": { type: "number", default: 64, describe: "heap sampling stack depth" },
43
- "heap-rows": { type: "number", default: 20, describe: "top allocation sites to show" },
44
- "heap-stack": { type: "number", default: 3, describe: "call stack depth to display" },
45
- "heap-verbose": { type: "boolean", default: false, describe: "verbose output with file:// paths and line numbers" },
46
- "heap-raw": { type: "boolean", default: false, describe: "dump every raw heap sample (ordinal, size, stack)" },
47
- "heap-user-only": { type: "boolean", default: false, describe: "filter to user code only (hide node internals)" },
27
+ "gc-stats": { type: "boolean", default: false, describe: "collect GC statistics (Node: --trace-gc-nvp, browser: CDP tracing)" },
28
+ "gc-force": { type: "boolean", default: false, describe: "force GC after each iteration" },
29
+ adaptive: { type: "boolean", default: false, describe: "adaptive sampling (experimental)" },
30
+ "min-time": { type: "number", default: 1, describe: "minimum time before adaptive convergence can stop" },
31
+ convergence: { type: "number", default: 95, describe: "adaptive confidence threshold (0-100)" },
32
+ alloc: { type: "boolean", default: false, describe: "allocation sampling attribution (includes garbage)" },
33
+ "alloc-interval": { type: "number", default: 32768, describe: "allocation sampling interval in bytes" },
34
+ "alloc-depth": { type: "number", default: 64, describe: "allocation sampling stack depth" },
35
+ "alloc-rows": { type: "number", default: 20, describe: "top allocation sites to show" },
36
+ "alloc-stack": { type: "number", default: 3, describe: "call stack depth to display" },
37
+ "alloc-verbose": { type: "boolean", default: false, describe: "verbose output with file:// paths and line numbers" },
38
+ "alloc-raw": { type: "boolean", default: false, describe: "dump every raw allocation sample (ordinal, size, stack)" },
39
+ "alloc-user-only":{ type: "boolean", default: false, describe: "filter to user code only (hide node internals)" },
40
+ profile: { type: "boolean", default: false, alias: "time-sample", describe: "V8 CPU time sampling profiler" },
41
+ "profile-interval":{ type: "number", default: 1000, alias: "time-interval", describe: "CPU sampling interval in microseconds" },
42
+ "call-counts": { type: "boolean", default: false, describe: "collect per-function execution counts via V8 precise coverage" },
43
+ stats: { type: "string", default: "mean,p50,p99", describe: "timing columns: mean|median|min|max|p<N> (e.g. mean,p70,p99)" },
44
+ view: { type: "boolean", default: false, alias: "html", describe: "open viewer in browser" },
45
+ "view-serve": { type: "boolean", default: false, describe: "start viewer server without opening browser (reload an existing tab)" },
46
+ "export-perfetto":{ type: "string", requiresArg: true, describe: "export Perfetto trace file (view at ui.perfetto.dev)" },
47
+ "export-profile": { type: "string", requiresArg: true, alias: "export-time", describe: "export CPU profile as .cpuprofile (V8/Chrome DevTools format)" },
48
+ archive: { type: "string", describe: "archive profile + sources to .benchforge file" },
49
+ editor: { type: "string", default: "vscode", describe: "editor for source links: vscode, cursor, or custom://scheme" },
50
+ inspect: { type: "boolean", default: false, describe: "run once for external profiler attach" },
51
+ "trace-opt": { type: "boolean", default: false, describe: "trace V8 optimization tiers (requires --allow-natives-syntax)" },
52
+ "pause-warmup": { type: "number", default: 0, requiresArg: true, describe: "post-warmup settle time in ms for V8 background compilation (0 to skip)" },
48
53
  url: { type: "string", requiresArg: true, describe: "page URL for browser profiling (enables browser mode)" },
49
- headless: { type: "boolean", default: true, describe: "run browser in headless mode" },
54
+ "page-load": { type: "boolean", default: false, describe: "passive page-load profiling (no __bench needed)" },
55
+ "wait-for": { type: "string", requiresArg: true, describe: "page-load completion: CSS selector, JS expression, 'load', or 'domcontentloaded'" },
56
+ headless: { type: "boolean", default: false, describe: "run browser in headless mode (default: headed)" },
50
57
  timeout: { type: "number", default: 60, describe: "browser page timeout in seconds" },
58
+ chrome: { type: "string", requiresArg: true, describe: "Chrome binary path (default: auto-detect or CHROME_PATH)" },
59
+ "chrome-profile": { type: "string", requiresArg: true, describe: "Chrome user profile directory (default: temp profile)" },
60
+ "baseline-url": { type: "string", requiresArg: true, describe: "baseline URL for A/B comparison (fresh tab per batch)" },
51
61
  "chrome-args": { type: "string", array: true, requiresArg: true, describe: "extra Chromium flags" },
52
62
  } as const;
53
63
 
54
- /** @return yargs with standard benchmark options */
55
- export function defaultCliArgs(yargsInstance: Argv): Argv<DefaultCliArgs> {
56
- return yargsInstance
57
- .command("$0 [file]", "run benchmarks", y => {
58
- y.positional("file", {
59
- type: "string",
60
- describe: "benchmark file to run",
61
- });
62
- })
63
- .options(cliOptions)
64
- .help()
65
- .strict() as Argv<DefaultCliArgs>;
66
- }
64
+ export const defaultDuration = 0.642;
65
+ export const defaultAdaptiveMaxTime = 20;
66
+
67
+ /** Default values for all CLI options, including alias keys for yargs filtering. */
68
+ export const cliDefaults: Record<string, unknown> = Object.fromEntries(
69
+ Object.entries(cliOptions)
70
+ .filter(([, opt]) => "default" in opt)
71
+ .flatMap(([key, opt]) => {
72
+ const o = opt as Record<string, unknown>;
73
+ const entries: [string, unknown][] = [[key, o.default]];
74
+ if (o.alias) entries.push([o.alias as string, o.default]);
75
+ return entries;
76
+ }),
77
+ );
78
+
79
+ const optionGroups = {
80
+ "Run:": ["duration", "iterations"],
81
+ "Batching:": ["batches", "warmup-batch", "no-batch-trim"],
82
+ "Node:": ["worker", "inspect"],
83
+ "Browser:": [
84
+ "url",
85
+ "baseline-url",
86
+ "page-load",
87
+ "wait-for",
88
+ "headless",
89
+ "timeout",
90
+ "chrome",
91
+ "chrome-profile",
92
+ "chrome-args",
93
+ ],
94
+ "GC:": ["gc-stats", "gc-force"],
95
+ "Allocation Profiling:": [
96
+ "alloc",
97
+ "alloc-interval",
98
+ "alloc-depth",
99
+ "alloc-rows",
100
+ "alloc-stack",
101
+ "alloc-verbose",
102
+ "alloc-raw",
103
+ "alloc-user-only",
104
+ ],
105
+ "CPU Profiling:": ["profile", "profile-interval", "call-counts"],
106
+ "Output:": [
107
+ "stats",
108
+ "view",
109
+ "view-serve",
110
+ "equiv-margin",
111
+ "archive",
112
+ "export-perfetto",
113
+ "export-profile",
114
+ "editor",
115
+ ],
116
+ "Selecting Benchmarks:": ["filter", "all", "list"],
117
+ "V8 Tuning:": [
118
+ "warmup",
119
+ "trace-opt",
120
+ "pause-first",
121
+ "pause-interval",
122
+ "pause-duration",
123
+ "pause-warmup",
124
+ ],
125
+ "Adaptive:": ["adaptive", "min-time", "convergence"],
126
+ } as const;
67
127
 
68
- /** @return parsed command line arguments */
128
+ const { url: _url, ...browserOnlyOptions } = cliOptions;
129
+
130
+ /** Parse command line arguments with optional custom yargs configuration. */
69
131
  export function parseCliArgs<T = DefaultCliArgs>(
70
132
  args: string[],
71
133
  configure: Configure<T> = defaultCliArgs as Configure<T>,
72
134
  ): T {
73
- const yargsInstance = configure(yargs(args));
74
- return yargsInstance.parseSync() as T;
135
+ return configure(yargs(args)).parseSync() as T;
136
+ }
137
+
138
+ /** Configure yargs for browser benchmarking with url as a required positional. */
139
+ export function browserCliArgs(yargsInstance: Argv): Argv<DefaultCliArgs> {
140
+ return applyGroups(
141
+ yargsInstance
142
+ .command("$0 <url>", "run browser benchmarks", y => {
143
+ y.positional("url", {
144
+ type: "string",
145
+ describe: "page URL for browser profiling",
146
+ });
147
+ })
148
+ .options(browserOnlyOptions)
149
+ .help()
150
+ .strict(),
151
+ ) as Argv<DefaultCliArgs>;
152
+ }
153
+
154
+ /** Configure yargs with standard benchmark options and file positional. */
155
+ export function defaultCliArgs(yargsInstance: Argv): Argv<DefaultCliArgs> {
156
+ return applyGroups(
157
+ yargsInstance
158
+ .command("$0 [file]", "run benchmarks", y => {
159
+ y.positional("file", {
160
+ type: "string",
161
+ describe: "benchmark file to run",
162
+ });
163
+ })
164
+ .options(cliOptions)
165
+ .help()
166
+ .strict(),
167
+ ) as Argv<DefaultCliArgs>;
168
+ }
169
+
170
+ /** Strip yargs internals (`_`, `$0`) and undefined values, converting kebab-case to camelCase. */
171
+ export function cleanCliArgs(args: DefaultCliArgs): Record<string, unknown> {
172
+ const skip = new Set(["_", "$0"]);
173
+ const camel = (k: string) =>
174
+ k.replace(/-([a-z])/g, (_, l: string) => l.toUpperCase());
175
+ return Object.fromEntries(
176
+ Object.entries(args)
177
+ .filter(([k, v]) => v !== undefined && v !== null && !skip.has(k))
178
+ .map(([k, v]) => [camel(k), v]),
179
+ );
180
+ }
181
+
182
+ /** Assign options to their labeled groups in yargs help output. */
183
+ function applyGroups(y: Argv): Argv {
184
+ return Object.entries(optionGroups).reduce(
185
+ (acc, [label, keys]) => acc.group(keys as unknown as string[], label),
186
+ y,
187
+ );
75
188
  }
@@ -0,0 +1,179 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { buildSpeedscopeFile } from "../export/AllocExport.ts";
4
+ import { archiveBenchmark, collectSources } from "../export/ArchiveExport.ts";
5
+ import {
6
+ annotateFramesWithCounts,
7
+ buildCoverageMap,
8
+ } from "../export/CoverageExport.ts";
9
+ import { resolveEditorUri } from "../export/EditorUri.ts";
10
+ import { exportPerfettoTrace } from "../export/PerfettoExport.ts";
11
+ import { buildTimeSpeedscopeFile } from "../export/TimeExport.ts";
12
+ import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
13
+ import type { TimeProfile } from "../profiling/node/TimeSampler.ts";
14
+ import type { ReportGroup, ReportSection } from "../report/BenchmarkReport.ts";
15
+ import { groupReports } from "../report/BenchmarkReport.ts";
16
+ import type { GitVersion } from "../report/GitUtils.ts";
17
+ import { prepareHtmlData } from "../report/HtmlReport.ts";
18
+ import type { ReportData } from "../viewer/ReportData.ts";
19
+ import type { DefaultCliArgs } from "./CliArgs.ts";
20
+ import {
21
+ cliComparisonOptions,
22
+ cliHeapReportOptions,
23
+ needsAlloc,
24
+ } from "./CliOptions.ts";
25
+ import { printHeapReports, withStatus } from "./CliReport.ts";
26
+ import {
27
+ optionalJson,
28
+ startViewerServer,
29
+ waitForCtrlC,
30
+ } from "./ViewerServer.ts";
31
+
32
+ /** Options for exporting benchmark results to various formats */
33
+ export interface ExportOptions {
34
+ results: ReportGroup[];
35
+ args: DefaultCliArgs;
36
+ sections?: ReportSection[];
37
+ currentVersion?: GitVersion;
38
+ baselineVersion?: GitVersion;
39
+ }
40
+
41
+ /** Export options for matrix benchmarks (results/args supplied by the matrix pipeline). */
42
+ export interface MatrixExportOptions {
43
+ sections?: ReportSection[];
44
+ currentVersion?: GitVersion;
45
+ baselineVersion?: GitVersion;
46
+ }
47
+
48
+ type FrameContainer = {
49
+ shared: { frames: { name: string; file?: string; line?: number }[] };
50
+ };
51
+
52
+ /** Export reports (JSON, Perfetto, archive, viewer) based on CLI args. */
53
+ export async function exportReports(options: ExportOptions): Promise<void> {
54
+ const { results, args, sections, currentVersion, baselineVersion } = options;
55
+
56
+ const wantViewer = args.view || args["view-serve"] || args.archive != null;
57
+ const comparison = cliComparisonOptions(args);
58
+ const htmlOpts = {
59
+ cliArgs: args,
60
+ sections,
61
+ currentVersion,
62
+ baselineVersion,
63
+ ...comparison,
64
+ };
65
+ const reportData = wantViewer
66
+ ? withStatus("computing viewer data", () =>
67
+ prepareHtmlData(results, htmlOpts),
68
+ )
69
+ : undefined;
70
+
71
+ exportFileFormats(results, args);
72
+
73
+ const profileFile = buildSpeedscopeFile(results);
74
+ const timeFile = buildAllTimeProfiles(results);
75
+ const coverageData = await annotateCoverage(results, profileFile, timeFile);
76
+ const timeData = timeFile ? JSON.stringify(timeFile) : undefined;
77
+
78
+ if (args.archive != null) {
79
+ const outputPath = args.archive || undefined;
80
+ await archiveBenchmark({
81
+ groups: results,
82
+ reportData,
83
+ timeProfileData: timeData,
84
+ coverageData,
85
+ outputPath,
86
+ });
87
+ }
88
+ if (args.view || args["view-serve"]) {
89
+ await openViewer(profileFile, timeData, coverageData, reportData, args);
90
+ }
91
+ }
92
+
93
+ /** Print heap reports (if enabled) and export results. */
94
+ export async function finishReports(
95
+ results: ReportGroup[],
96
+ args: DefaultCliArgs,
97
+ exportOptions?: MatrixExportOptions,
98
+ ): Promise<void> {
99
+ if (needsAlloc(args)) {
100
+ printHeapReports(results, cliHeapReportOptions(args));
101
+ }
102
+ await exportReports({ results, args, ...exportOptions });
103
+ }
104
+
105
+ /** Write Perfetto and time profile files if requested by CLI args. */
106
+ function exportFileFormats(results: ReportGroup[], args: DefaultCliArgs): void {
107
+ if (args["export-perfetto"])
108
+ exportPerfettoTrace(results, args["export-perfetto"], args);
109
+ if (args["export-profile"])
110
+ exportTimeProfile(results, args["export-profile"]);
111
+ }
112
+
113
+ /** Build combined Speedscope file from all time profiles in results. */
114
+ function buildAllTimeProfiles(results: ReportGroup[]) {
115
+ const entries = results.flatMap(group =>
116
+ groupReports(group)
117
+ .filter(r => r.measuredResults.timeProfile)
118
+ .map(r => ({
119
+ name: r.name,
120
+ profile: r.measuredResults.timeProfile as TimeProfile,
121
+ })),
122
+ );
123
+ return buildTimeSpeedscopeFile(entries);
124
+ }
125
+
126
+ /** Annotate speedscope frame names with coverage counts. Returns serialized coverage map. */
127
+ async function annotateCoverage(
128
+ results: ReportGroup[],
129
+ profileFile?: FrameContainer,
130
+ timeFile?: FrameContainer,
131
+ ): Promise<string | undefined> {
132
+ const coverage = mergeCoverage(results);
133
+ if (!coverage) return undefined;
134
+
135
+ const frames = coverage.scripts.map(s => ({ file: s.url }));
136
+ const sources = await collectSources(frames);
137
+ const covMap = buildCoverageMap(coverage, sources);
138
+ if (profileFile) annotateFramesWithCounts(profileFile.shared.frames, covMap);
139
+ if (timeFile) annotateFramesWithCounts(timeFile.shared.frames, covMap);
140
+ return JSON.stringify(Object.fromEntries(covMap.map));
141
+ }
142
+
143
+ /** Start viewer server with profile data and block until Ctrl+C. */
144
+ async function openViewer(
145
+ profileFile: ReturnType<typeof buildSpeedscopeFile>,
146
+ timeData: string | undefined,
147
+ coverageData: string | undefined,
148
+ reportData: ReportData | undefined,
149
+ args: DefaultCliArgs,
150
+ ): Promise<void> {
151
+ const viewer = await startViewerServer({
152
+ profileData: optionalJson(profileFile),
153
+ timeProfileData: timeData,
154
+ coverageData,
155
+ reportData: optionalJson(reportData),
156
+ editorUri: resolveEditorUri(args.editor),
157
+ open: !args["view-serve"],
158
+ });
159
+ await waitForCtrlC();
160
+ viewer.close();
161
+ }
162
+
163
+ /** Export the first raw V8 TimeProfile to a JSON file. */
164
+ function exportTimeProfile(results: ReportGroup[], path: string): void {
165
+ const profile = results
166
+ .flatMap(g => groupReports(g))
167
+ .find(r => r.measuredResults.timeProfile)?.measuredResults.timeProfile;
168
+ if (!profile) return void console.log("No time profiles to export.");
169
+ writeFileSync(resolve(path), JSON.stringify(profile));
170
+ console.log(`Time profile exported to: ${path}`);
171
+ }
172
+
173
+ /** Merge coverage data from all results into a single CoverageData. */
174
+ function mergeCoverage(results: ReportGroup[]): CoverageData | undefined {
175
+ const scripts = results.flatMap(group =>
176
+ groupReports(group).flatMap(r => r.measuredResults.coverage?.scripts ?? []),
177
+ );
178
+ return scripts.length > 0 ? { scripts } : undefined;
179
+ }
@@ -0,0 +1,147 @@
1
+ import type { RunMatrixOptions } from "../matrix/BenchMatrix.ts";
2
+ import type { HeapReportOptions } from "../profiling/node/HeapSampleReport.ts";
3
+ import type { ComparisonOptions } from "../report/BenchmarkReport.ts";
4
+ import { buildTimeSection } from "../report/StandardSections.ts";
5
+ import type { RunnerOptions } from "../runners/BenchRunner.ts";
6
+ import {
7
+ type DefaultCliArgs,
8
+ defaultAdaptiveMaxTime,
9
+ defaultDuration,
10
+ } from "./CliArgs.ts";
11
+
12
+ /**
13
+ * Resolve duration/iterations flags into runner limits.
14
+ *
15
+ * | Flags set | maxTime | maxIterations |
16
+ * |-------------------|--------------------|---------------|
17
+ * | neither | defaultDuration*1000 | undefined |
18
+ * | --iterations only | undefined | N |
19
+ * | --duration only | duration*1000 | undefined |
20
+ * | both | duration*1000 | N |
21
+ */
22
+ type Limits = {
23
+ maxTime: number | undefined;
24
+ maxIterations: number | undefined;
25
+ };
26
+
27
+ /** Convert CLI args to matrix runner options. */
28
+ export function cliToMatrixOptions(args: DefaultCliArgs): RunMatrixOptions {
29
+ const { iterations, worker, batches } = args;
30
+ const { maxTime } = resolveLimits(args);
31
+ return {
32
+ iterations,
33
+ maxTime,
34
+ useWorker: worker,
35
+ batches,
36
+ warmupBatch: args["warmup-batch"],
37
+ ...cliCommonOptions(args),
38
+ };
39
+ }
40
+
41
+ /** Validate CLI argument combinations. */
42
+ export function validateArgs(args: DefaultCliArgs): void {
43
+ if (args["gc-stats"] && !args.worker && !args.url) {
44
+ throw new Error(
45
+ "--gc-stats requires worker mode (the default). Remove --no-worker flag.",
46
+ );
47
+ }
48
+ // Eagerly validate --stats tokens so users see the error before running benchmarks.
49
+ if (args.stats) buildTimeSection(args.stats);
50
+ }
51
+
52
+ /** Convert CLI args to benchmark runner options. */
53
+ export function cliToRunnerOptions(args: DefaultCliArgs): RunnerOptions {
54
+ const { inspect, iterations, adaptive } = args;
55
+ const gcForce = args["gc-force"];
56
+ if (inspect)
57
+ return { maxIterations: iterations ?? 1, warmupTime: 0, gcForce };
58
+ if (adaptive) return createAdaptiveOptions(args);
59
+ return { ...resolveLimits(args), ...cliCommonOptions(args) };
60
+ }
61
+
62
+ /** Convert CLI args to heap report display options. */
63
+ export function cliHeapReportOptions(args: DefaultCliArgs): HeapReportOptions {
64
+ return {
65
+ topN: args["alloc-rows"],
66
+ stackDepth: args["alloc-stack"],
67
+ verbose: args["alloc-verbose"],
68
+ raw: args["alloc-raw"],
69
+ userOnly: args["alloc-user-only"],
70
+ };
71
+ }
72
+
73
+ /** True if any alloc-related flag implies allocation sampling. */
74
+ export function needsAlloc(args: DefaultCliArgs): boolean {
75
+ return (
76
+ args.alloc ||
77
+ args.archive != null ||
78
+ args["alloc-raw"] ||
79
+ args["alloc-verbose"] ||
80
+ args["alloc-user-only"]
81
+ );
82
+ }
83
+
84
+ /** True if any profiling flag implies CPU time sampling. */
85
+ export function needsProfile(args: DefaultCliArgs): boolean {
86
+ return args.profile || !!args["export-profile"];
87
+ }
88
+
89
+ /** Extract baseline comparison options from CLI args. */
90
+ export function cliComparisonOptions(args: DefaultCliArgs): ComparisonOptions {
91
+ return {
92
+ equivMargin: args["equiv-margin"],
93
+ noBatchTrim: args["no-batch-trim"],
94
+ };
95
+ }
96
+
97
+ export function resolveLimits(args: {
98
+ duration?: number;
99
+ iterations?: number;
100
+ }): Limits {
101
+ const { duration, iterations } = args;
102
+ if (duration == null && iterations == null)
103
+ return { maxTime: defaultDuration * 1000, maxIterations: undefined };
104
+ return {
105
+ maxTime: duration != null ? duration * 1000 : undefined,
106
+ maxIterations: iterations,
107
+ };
108
+ }
109
+
110
+ /** Runner/matrix options shared across all CLI modes. */
111
+ function cliCommonOptions(args: DefaultCliArgs) {
112
+ const { warmup } = args;
113
+ const { "gc-force": gcForce, "gc-stats": gcStats } = args;
114
+ const { "trace-opt": traceOpt, "call-counts": callCounts } = args;
115
+ const { "pause-warmup": pauseWarmup, "pause-first": pauseFirst } = args;
116
+ const { "pause-interval": pauseInterval, "pause-duration": pauseDuration } =
117
+ args;
118
+ const { "alloc-interval": allocInterval, "alloc-depth": allocDepth } = args;
119
+ const { "profile-interval": profileInterval } = args;
120
+ return {
121
+ gcForce,
122
+ warmup,
123
+ traceOpt,
124
+ gcStats,
125
+ callCounts,
126
+ pauseWarmup,
127
+ pauseFirst,
128
+ pauseInterval,
129
+ pauseDuration,
130
+ alloc: needsAlloc(args),
131
+ allocInterval,
132
+ allocDepth,
133
+ profile: needsProfile(args),
134
+ profileInterval,
135
+ };
136
+ }
137
+
138
+ /** Build runner options for adaptive sampling mode. */
139
+ function createAdaptiveOptions(args: DefaultCliArgs): RunnerOptions {
140
+ return {
141
+ minTime: (args["min-time"] ?? 1) * 1000,
142
+ maxTime: defaultAdaptiveMaxTime * 1000,
143
+ targetConfidence: args.convergence,
144
+ adaptive: true,
145
+ ...cliCommonOptions(args),
146
+ } as any;
147
+ }