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,4 +1,4 @@
1
- import type { BenchMatrix } from "../BenchMatrix.ts";
1
+ import type { BenchMatrix } from "./BenchMatrix.ts";
2
2
  import { loadCasesModule } from "./CaseLoader.ts";
3
3
  import { discoverVariants } from "./VariantLoader.ts";
4
4
 
@@ -17,11 +17,8 @@ export interface FilteredMatrix<T = unknown> extends BenchMatrix<T> {
17
17
  /** Parse filter string: "case/variant", "case/", "/variant", or "case" */
18
18
  export function parseMatrixFilter(filter: string): MatrixFilter {
19
19
  if (filter.includes("/")) {
20
- const [casePart, variantPart] = filter.split("/", 2);
21
- return {
22
- case: casePart || undefined,
23
- variant: variantPart || undefined,
24
- };
20
+ const [casePart, varPart] = filter.split("/", 2);
21
+ return { case: casePart || undefined, variant: varPart || undefined };
25
22
  }
26
23
  return { case: filter };
27
24
  }
@@ -36,68 +33,73 @@ export async function filterMatrix<T>(
36
33
  const caseList = await getFilteredCases(matrix, filter.case);
37
34
  const variantList = await getFilteredVariants(matrix, filter.variant);
38
35
 
39
- const filteredCases =
40
- caseList && matrix.filteredCases
41
- ? caseList.filter(c => matrix.filteredCases!.includes(c))
42
- : (caseList ?? matrix.filteredCases);
43
-
44
- const filteredVariants =
45
- variantList && matrix.filteredVariants
46
- ? variantList.filter(v => matrix.filteredVariants!.includes(v))
47
- : (variantList ?? matrix.filteredVariants);
36
+ const filteredCases = intersectFilters(caseList, matrix.filteredCases);
37
+ const filteredVariants = intersectFilters(
38
+ variantList,
39
+ matrix.filteredVariants,
40
+ );
48
41
 
49
42
  return { ...matrix, filteredCases, filteredVariants };
50
43
  }
51
44
 
52
- /** Get case IDs matching filter pattern */
45
+ /** Collect all case IDs from either casesModule or inline cases */
46
+ export async function resolveCaseIds<T>(
47
+ matrix: BenchMatrix<T>,
48
+ ): Promise<string[] | undefined> {
49
+ if (matrix.casesModule)
50
+ return (await loadCasesModule(matrix.casesModule)).cases;
51
+ return matrix.cases;
52
+ }
53
+
54
+ /** Collect all variant IDs from either inline variants or variantDir */
55
+ export async function resolveVariantIds<T>(
56
+ matrix: BenchMatrix<T>,
57
+ ): Promise<string[]> {
58
+ if (matrix.variants) return Object.keys(matrix.variants);
59
+ if (matrix.variantDir) return discoverVariants(matrix.variantDir);
60
+ throw new Error("BenchMatrix requires 'variants' or 'variantDir'");
61
+ }
62
+
63
+ /** Return case IDs matching a substring pattern, or all if no pattern */
53
64
  async function getFilteredCases<T>(
54
65
  matrix: BenchMatrix<T>,
55
66
  casePattern?: string,
56
67
  ): Promise<string[] | undefined> {
57
68
  if (!casePattern) return undefined;
58
-
59
- const caseIds = matrix.casesModule
60
- ? (await loadCasesModule(matrix.casesModule)).cases
61
- : matrix.cases;
69
+ const caseIds = await resolveCaseIds(matrix);
62
70
  if (!caseIds) return ["default"]; // implicit single case
63
-
64
- const filtered = caseIds.filter(id => matchPattern(id, casePattern));
65
- if (filtered.length === 0) {
66
- throw new Error(`No cases match filter: "${casePattern}"`);
67
- }
68
- return filtered;
71
+ return filterByPattern(caseIds, casePattern, "cases");
69
72
  }
70
73
 
71
- /** Get variant IDs matching filter pattern */
74
+ /** Return variant IDs matching a substring pattern, or all if no pattern */
72
75
  async function getFilteredVariants<T>(
73
76
  matrix: BenchMatrix<T>,
74
77
  variantPattern?: string,
75
78
  ): Promise<string[] | undefined> {
76
79
  if (!variantPattern) return undefined;
80
+ const allIds = await resolveVariantIds(matrix);
81
+ return filterByPattern(allIds, variantPattern, "variants");
82
+ }
77
83
 
78
- if (matrix.variants) {
79
- const ids = Object.keys(matrix.variants).filter(id =>
80
- matchPattern(id, variantPattern),
81
- );
82
- if (ids.length === 0) {
83
- throw new Error(`No variants match filter: "${variantPattern}"`);
84
- }
85
- return ids;
86
- }
87
-
88
- if (matrix.variantDir) {
89
- const allIds = await discoverVariants(matrix.variantDir);
90
- const filtered = allIds.filter(id => matchPattern(id, variantPattern));
91
- if (filtered.length === 0) {
92
- throw new Error(`No variants match filter: "${variantPattern}"`);
93
- }
94
- return filtered;
95
- }
84
+ /** Intersect two optional filter lists: both present ==> intersection, otherwise the one that exists */
85
+ function intersectFilters(a?: string[], b?: string[]): string[] | undefined {
86
+ if (a && b) return a.filter(v => b.includes(v));
87
+ return a ?? b;
88
+ }
96
89
 
97
- throw new Error("BenchMatrix requires 'variants' or 'variantDir'");
90
+ /** Filter IDs by substring pattern, throwing if no matches */
91
+ function filterByPattern(
92
+ ids: string[],
93
+ pattern: string,
94
+ label: string,
95
+ ): string[] {
96
+ const filtered = ids.filter(id => matchPattern(id, pattern));
97
+ if (filtered.length === 0)
98
+ throw new Error(`No ${label} match filter: "${pattern}"`);
99
+ return filtered;
98
100
  }
99
101
 
100
- /** Match id against pattern (case-insensitive substring) */
102
+ /** Case-insensitive substring match */
101
103
  function matchPattern(id: string, pattern: string): boolean {
102
104
  return id.toLowerCase().includes(pattern.toLowerCase());
103
105
  }
@@ -0,0 +1,50 @@
1
+ import { TimingRunner } from "../runners/TimingRunner.ts";
2
+ import type {
3
+ BenchMatrix,
4
+ CaseResult,
5
+ RunMatrixOptions,
6
+ VariantResult,
7
+ } from "./BenchMatrix.ts";
8
+ import {
9
+ buildRunnerOptions,
10
+ prepareBenchFn,
11
+ resolveCases,
12
+ } from "./BenchMatrix.ts";
13
+ import { loadCaseData } from "./CaseLoader.ts";
14
+
15
+ /** Run matrix with in-memory variant functions (no worker isolation) */
16
+ export async function runMatrixInline<T>(
17
+ matrix: BenchMatrix<T>,
18
+ options: RunMatrixOptions,
19
+ ): Promise<{ name: string; variants: VariantResult[] }> {
20
+ if (matrix.baselineDir)
21
+ throw new Error(
22
+ "BenchMatrix with inline 'variants' cannot use 'baselineDir'. Use 'variantDir' instead.",
23
+ );
24
+
25
+ const { casesModule, caseIds } = await resolveCases(matrix, options);
26
+ const runner = new TimingRunner();
27
+ const runnerOpts = buildRunnerOptions(options);
28
+
29
+ const allEntries = Object.entries(matrix.variants!);
30
+ const { filteredVariants } = options;
31
+ const variantEntries = filteredVariants
32
+ ? allEntries.filter(([id]) => filteredVariants.includes(id))
33
+ : allEntries;
34
+
35
+ const variants: VariantResult[] = [];
36
+ for (const [variantId, variant] of variantEntries) {
37
+ const cases: CaseResult[] = [];
38
+ for (const caseId of caseIds) {
39
+ const loaded = await loadCaseData(casesModule, caseId);
40
+ const data = casesModule || matrix.cases ? loaded.data : (undefined as T);
41
+ const fn = await prepareBenchFn(variant, data);
42
+ const spec = { name: variantId, fn };
43
+ const [measured] = await runner.runBench(spec, runnerOpts);
44
+ cases.push({ caseId, measured, metadata: loaded.metadata });
45
+ }
46
+ variants.push({ id: variantId, cases });
47
+ }
48
+
49
+ return { name: matrix.name, variants };
50
+ }
@@ -1,106 +1,53 @@
1
- import type { CaseResult, MatrixResults } from "../BenchMatrix.ts";
2
- import { injectDiffColumns, type ResultsMapper } from "../BenchmarkReport.ts";
3
- import { totalProfileBytes } from "../heap-sample/HeapSampleReport.ts";
4
- import { type GcStatsInfo, gcStatsSection } from "../StandardSections.ts";
5
1
  import {
6
- average,
7
- bootstrapDifferenceCI,
8
- type DifferenceCI,
9
- } from "../StatisticalUtils.ts";
10
- import {
11
- duration,
12
- formatBytes,
13
- formatDiffWithCI,
14
- truncate,
15
- } from "../table-util/Formatters.ts";
16
- import {
17
- buildTable,
18
- type ColumnGroup,
19
- type ResultGroup,
20
- } from "../table-util/TableReport.ts";
21
-
22
- /** Custom column definition for extra computed metrics */
23
- export interface ExtraColumn {
24
- key: string;
25
- title: string;
26
- groupTitle?: string; // optional column group header
27
- extract: (caseResult: CaseResult) => unknown;
28
- formatter?: (value: unknown) => string;
29
- }
30
-
31
- /** Options for matrix report generation */
2
+ type ComparisonOptions,
3
+ computeDiffCI,
4
+ extractSectionValues,
5
+ findPrimaryColumn,
6
+ type ReportSection,
7
+ } from "../report/BenchmarkReport.ts";
8
+ import { truncate } from "../report/Formatters.ts";
9
+ import { runsSection, timeSection } from "../report/StandardSections.ts";
10
+ import { buildTable } from "../report/text/TableReport.ts";
11
+ import { sectionColumnGroups } from "../report/text/TextReport.ts";
12
+ import type { MeasuredResults } from "../runners/MeasuredResults.ts";
13
+ import type {
14
+ CaseResult,
15
+ MatrixResults,
16
+ VariantResult,
17
+ } from "./BenchMatrix.ts";
18
+
19
+ /** Options for {@link reportMatrixResults} */
32
20
  export interface MatrixReportOptions {
33
- extraColumns?: ExtraColumn[];
34
- sections?: ResultsMapper[]; // ResultsMapper sections (like BenchSuite)
35
- variantTitle?: string; // custom title for the variant column (default: "variant")
21
+ /** ReportSection sections (default: [timeSection, runsSection]) */
22
+ sections?: ReportSection[];
23
+ /** Custom title for the variant column (default: "variant") */
24
+ variantTitle?: string;
25
+ /** Comparison options (equivalence margin, batch trimming) */
26
+ comparison?: ComparisonOptions;
36
27
  }
37
28
 
38
- /** Row data for matrix report table */
39
- interface MatrixReportRow extends Record<string, unknown> {
40
- name: string;
41
- time: number;
42
- samples: number;
43
- diffCI?: DifferenceCI;
29
+ interface VariantCase {
30
+ variant: VariantResult;
31
+ cr: CaseResult;
44
32
  }
45
33
 
46
- /** GC statistics columns - derived from gcStatsSection for consistency */
47
- export const gcStatsColumns: ExtraColumn[] = gcStatsSection
48
- .columns()[0]
49
- .columns.map(col => ({
50
- key: col.key as string,
51
- title: col.title,
52
- groupTitle: "GC",
53
- extract: (r: CaseResult) =>
54
- gcStatsSection.extract(r.measured)[col.key as keyof GcStatsInfo],
55
- formatter: (v: unknown) => col.formatter?.(v) ?? "-",
56
- }));
57
-
58
- /** GC pause time column */
59
- export const gcPauseColumn: ExtraColumn = {
60
- key: "gcPause",
61
- title: "pause",
62
- groupTitle: "GC",
63
- extract: r => r.measured.gcStats?.gcPauseTime,
64
- formatter: v => (v != null ? `${(v as number).toFixed(1)}ms` : "-"),
65
- };
34
+ type Row = Record<string, unknown> & { name: string };
66
35
 
67
- /** Heap sampling total bytes column */
68
- export const heapTotalColumn: ExtraColumn = {
69
- key: "heapTotal",
70
- title: "heap",
71
- extract: r => {
72
- const profile = r.measured.heapProfile;
73
- if (!profile?.head) return undefined;
74
- return totalProfileBytes(profile);
75
- },
76
- formatter: formatBytesOrDash,
77
- };
36
+ const defaultSections: ReportSection[] = [timeSection, runsSection];
78
37
 
79
- /** Format matrix results as one table per case */
38
+ /** Format matrix results as text, with one table per case */
80
39
  export function reportMatrixResults(
81
40
  results: MatrixResults,
82
41
  options?: MatrixReportOptions,
83
42
  ): string {
84
- const tables = buildCaseTables(results, options);
85
- const header = `Matrix: ${results.name}`;
86
- return [header, ...tables].join("\n\n");
87
- }
43
+ if (results.variants.length === 0) return `Matrix: ${results.name}`;
88
44
 
89
- /** Format bytes with fallback to "-" for missing values */
90
- function formatBytesOrDash(value: unknown): string {
91
- return formatBytes(value) ?? "-";
92
- }
93
-
94
- /** Build one table for each case showing all variants */
95
- function buildCaseTables(
96
- results: MatrixResults,
97
- options?: MatrixReportOptions,
98
- ): string[] {
99
- if (results.variants.length === 0) return [];
100
-
101
- // Get all case IDs from first variant (all variants have same cases)
45
+ // all variants have the same cases
102
46
  const caseIds = results.variants[0].cases.map(c => c.caseId);
103
- return caseIds.map(caseId => buildCaseTable(results, caseId, options));
47
+ const tables = caseIds.map(caseId =>
48
+ buildCaseTable(results, caseId, options),
49
+ );
50
+ return [`Matrix: ${results.name}`, ...tables].join("\n\n");
104
51
  }
105
52
 
106
53
  /** Build table for a single case showing all variants */
@@ -109,19 +56,45 @@ function buildCaseTable(
109
56
  caseId: string,
110
57
  options?: MatrixReportOptions,
111
58
  ): string {
112
- const caseTitle = formatCaseTitle(results, caseId);
113
-
114
- if (options?.sections?.length) {
115
- return buildSectionTable(results, caseId, options, caseTitle);
116
- }
59
+ const title = formatCaseTitle(results, caseId);
60
+ const sections = options?.sections ?? defaultSections;
61
+ const variantTitle = options?.variantTitle ?? "variant";
62
+ const primaryCol = findPrimaryColumn(sections);
63
+
64
+ const caseResults = collectCaseResults(results, caseId);
65
+ const shared = sharedBaseline(caseResults);
66
+
67
+ const rows: Row[] = caseResults.flatMap(({ variant, cr }) => {
68
+ const vals = extractSectionValues(cr.measured, sections, cr.metadata);
69
+ const row: Row = { name: truncate(variant.id, 25), ...vals };
70
+ if (cr.baseline && primaryCol?.statKind) {
71
+ const { statKind, higherIsBetter } = primaryCol;
72
+ row.diffCI = computeDiffCI(
73
+ cr.baseline,
74
+ cr.measured,
75
+ statKind,
76
+ options?.comparison,
77
+ higherIsBetter,
78
+ );
79
+ }
80
+ const out: Row[] = [row];
81
+ if (cr.baseline && !shared)
82
+ out.push({
83
+ name: " \u21B3 baseline",
84
+ ...extractSectionValues(cr.baseline, sections, cr.metadata),
85
+ });
86
+ return out;
87
+ });
117
88
 
118
- const rows = buildCaseRows(results, caseId, options?.extraColumns);
119
- const hasBaseline = rows.some(r => r.diffCI);
120
- const columns = buildColumns(hasBaseline, options);
89
+ if (shared)
90
+ rows.push({
91
+ name: "=> baseline",
92
+ ...extractSectionValues(shared, sections),
93
+ });
121
94
 
122
- const resultGroup: ResultGroup<MatrixReportRow> = { results: rows };
123
- const table = buildTable(columns, [resultGroup]);
124
- return `${caseTitle}\n${table}`;
95
+ const hasDiff = rows.some(r => r.diffCI);
96
+ const cols = sectionColumnGroups(sections, hasDiff, variantTitle);
97
+ return `${title}\n${buildTable(cols, [{ results: rows }])}`;
125
98
  }
126
99
 
127
100
  /** Format case title with metadata if available */
@@ -129,162 +102,29 @@ function formatCaseTitle(results: MatrixResults, caseId: string): string {
129
102
  const caseResult = results.variants[0]?.cases.find(c => c.caseId === caseId);
130
103
  const metadata = caseResult?.metadata;
131
104
 
132
- if (metadata && Object.keys(metadata).length > 0) {
133
- const metaParts = Object.entries(metadata)
134
- .map(([k, v]) => `${v} ${k}`)
135
- .join(", ");
136
- return `${caseId} (${metaParts})`;
137
- }
138
- return caseId;
105
+ if (!metadata || Object.keys(metadata).length === 0) return caseId;
106
+ const meta = Object.entries(metadata)
107
+ .map(([k, v]) => `${v} ${k}`)
108
+ .join(", ");
109
+ return `${caseId} (${meta})`;
139
110
  }
140
111
 
141
- /** Build table using ResultsMapper sections */
142
- function buildSectionTable(
112
+ /** Collect (variant, caseResult) pairs for a given caseId */
113
+ function collectCaseResults(
143
114
  results: MatrixResults,
144
115
  caseId: string,
145
- options: MatrixReportOptions,
146
- caseTitle: string,
147
- ): string {
148
- const sections = options.sections!;
149
- const variantTitle = options.variantTitle ?? "name";
150
-
151
- const rows: Record<string, unknown>[] = [];
152
- let hasBaseline = false;
153
-
154
- for (const variant of results.variants) {
155
- const caseResult = variant.cases.find(c => c.caseId === caseId);
156
- if (!caseResult) continue;
157
-
158
- const row: Record<string, unknown> = { name: truncate(variant.id, 25) };
159
-
160
- for (const section of sections) {
161
- Object.assign(
162
- row,
163
- section.extract(caseResult.measured, caseResult.metadata),
164
- );
165
- }
166
-
167
- if (caseResult.baseline) {
168
- hasBaseline = true;
169
- const { samples: base } = caseResult.baseline;
170
- row.diffCI = bootstrapDifferenceCI(base, caseResult.measured.samples);
171
- }
172
-
173
- rows.push(row);
174
- }
175
-
176
- const columnGroups = buildSectionColumns(sections, variantTitle, hasBaseline);
177
- const resultGroup: ResultGroup<Record<string, unknown>> = { results: rows };
178
- const table = buildTable(columnGroups, [resultGroup]);
179
- return `${caseTitle}\n${table}`;
180
- }
181
-
182
- /** Build rows for all variants for a given case */
183
- function buildCaseRows(
184
- results: MatrixResults,
185
- caseId: string,
186
- extraColumns?: ExtraColumn[],
187
- ): MatrixReportRow[] {
116
+ ): VariantCase[] {
188
117
  return results.variants.flatMap(variant => {
189
- const caseResult = variant.cases.find(c => c.caseId === caseId);
190
- return caseResult ? [buildRow(variant.id, caseResult, extraColumns)] : [];
118
+ const cr = variant.cases.find(c => c.caseId === caseId);
119
+ return cr ? [{ variant, cr }] : [];
191
120
  });
192
121
  }
193
122
 
194
- /** Build column configuration */
195
- function buildColumns(
196
- hasBaseline: boolean,
197
- options?: MatrixReportOptions,
198
- ): ColumnGroup<MatrixReportRow>[] {
199
- const variantTitle = options?.variantTitle ?? "variant";
200
- const nameCol: ColumnGroup<MatrixReportRow> = {
201
- columns: [{ key: "name", title: variantTitle }],
202
- };
203
-
204
- const ciKey = "diffCI" as keyof MatrixReportRow;
205
- const diffCol = { key: ciKey, title: "Δ% CI", formatter: formatDiff };
206
- const timeCol: ColumnGroup<MatrixReportRow> = {
207
- columns: [
208
- { key: "time", title: "time", formatter: duration },
209
- ...(hasBaseline ? [diffCol] : []),
210
- ],
211
- };
212
-
213
- const groups: ColumnGroup<MatrixReportRow>[] = [nameCol, timeCol];
214
-
215
- // Add extra columns, grouped by groupTitle
216
- const extraColumns = options?.extraColumns;
217
- if (extraColumns?.length) {
218
- const byGroup = new Map<string | undefined, ExtraColumn[]>();
219
- for (const col of extraColumns) {
220
- const group = byGroup.get(col.groupTitle) ?? [];
221
- group.push(col);
222
- byGroup.set(col.groupTitle, group);
223
- }
224
- for (const [groupTitle, cols] of byGroup) {
225
- groups.push({
226
- groupTitle,
227
- columns: cols.map(col => ({
228
- key: col.key as keyof MatrixReportRow,
229
- title: col.title,
230
- formatter: col.formatter ?? String,
231
- })),
232
- });
233
- }
234
- }
235
-
236
- return groups;
237
- }
238
-
239
- /** Build column groups from ResultsMapper sections */
240
- function buildSectionColumns(
241
- sections: ResultsMapper[],
242
- variantTitle: string,
243
- hasBaseline: boolean,
244
- ): ColumnGroup<Record<string, unknown>>[] {
245
- const nameCol: ColumnGroup<Record<string, unknown>> = {
246
- columns: [{ key: "name", title: variantTitle }],
247
- };
248
-
249
- const sectionColumns = sections.flatMap(s => s.columns());
250
- const columnGroups = hasBaseline
251
- ? injectDiffColumns(sectionColumns)
252
- : (sectionColumns as ColumnGroup<Record<string, unknown>>[]);
253
-
254
- return [nameCol, ...columnGroups];
255
- }
256
-
257
- /** Build a single row from case result */
258
- function buildRow(
259
- variantId: string,
260
- caseResult: CaseResult,
261
- extraColumns?: ExtraColumn[],
262
- ): MatrixReportRow {
263
- const { measured, baseline } = caseResult;
264
- const samples = measured.samples;
265
- const time = measured.time?.avg ?? average(samples);
266
-
267
- const row: MatrixReportRow = {
268
- name: truncate(variantId, 25),
269
- time,
270
- samples: samples.length,
271
- };
272
-
273
- if (baseline) {
274
- row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
275
- }
276
-
277
- if (extraColumns) {
278
- for (const col of extraColumns) {
279
- row[col.key] = col.extract(caseResult);
280
- }
281
- }
282
-
283
- return row;
284
- }
285
-
286
- /** Format diff with CI, or "baseline" marker */
287
- function formatDiff(value: unknown): string | null {
288
- if (!value) return null;
289
- return formatDiffWithCI(value as DifferenceCI);
123
+ /** @return shared baseline if all variants reference the same one (baselineVariant mode) */
124
+ function sharedBaseline(
125
+ caseResults: VariantCase[],
126
+ ): MeasuredResults | undefined {
127
+ const baselines = caseResults.map(({ cr }) => cr.baseline).filter(Boolean);
128
+ if (baselines.length < 2) return undefined;
129
+ return baselines.every(b => b === baselines[0]) ? baselines[0] : undefined;
290
130
  }
@@ -1,8 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { fileURLToPath } from "node:url";
3
- import type { Variant } from "../BenchMatrix.ts";
3
+ import type { Variant } from "./BenchMatrix.ts";
4
4
 
5
- /** Discover variant ids from a directory of .ts files */
5
+ /** List variant IDs by scanning .ts files in a directory */
6
6
  export async function discoverVariants(dirUrl: string): Promise<string[]> {
7
7
  const dirPath = fileURLToPath(dirUrl);
8
8
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
@@ -12,7 +12,7 @@ export async function discoverVariants(dirUrl: string): Promise<string[]> {
12
12
  .sort();
13
13
  }
14
14
 
15
- /** Load a variant module and extract run/setup exports */
15
+ /** Import a variant module and return its run/setup exports as a Variant */
16
16
  export async function loadVariant<T = unknown>(
17
17
  dirUrl: string,
18
18
  variantId: string,
@@ -22,12 +22,12 @@ export async function loadVariant<T = unknown>(
22
22
  return extractVariant(module, variantId, moduleUrl);
23
23
  }
24
24
 
25
- /** Get module URL for a variant in a directory */
25
+ /** Resolve the import URL for a variant file */
26
26
  export function variantModuleUrl(dirUrl: string, variantId: string): string {
27
27
  return new URL(`${variantId}.ts`, dirUrl).href;
28
28
  }
29
29
 
30
- /** Extract variant from module exports */
30
+ /** Validate and extract a Variant from a module's exports */
31
31
  function extractVariant<T>(
32
32
  module: Record<string, unknown>,
33
33
  variantId: string,
@@ -0,0 +1,51 @@
1
+ import {
2
+ instrumentOpts,
3
+ startInstruments,
4
+ stopInstruments,
5
+ } from "./BrowserCDP.ts";
6
+ import type { BrowserProfileResult, ProfileCtx } from "./BrowserProfiler.ts";
7
+
8
+ /**
9
+ * Bench function mode: run window.__bench in a timed iteration loop.
10
+ *
11
+ * Simplified vs TimingRunner because it runs inside page.evaluate()
12
+ * where shared code, Node APIs, and V8 intrinsics are unavailable.
13
+ *
14
+ * Not feasible in browser page context:
15
+ * - heap tracking (no getHeapStatistics)
16
+ * - V8 opt status tracing (no %GetOptimizationStatus)
17
+ * - explicit GC or pause-for-compilation
18
+ */
19
+ export async function runBenchLoop(
20
+ ctx: ProfileCtx,
21
+ ): Promise<BrowserProfileResult> {
22
+ const { page, cdp, params, samplingInterval } = ctx;
23
+ const maxTime = params.maxTime ?? Number.MAX_SAFE_INTEGER;
24
+ const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;
25
+ const opts = instrumentOpts(params, samplingInterval);
26
+
27
+ await startInstruments(cdp, opts);
28
+
29
+ const { samples, totalMs } = await page.evaluate(
30
+ async ({ maxTime, maxIter }) => {
31
+ const bench = (globalThis as any).__bench;
32
+ const estimated = Math.min(maxIter, Math.ceil(maxTime / 0.1));
33
+ const samples = new Array<number>(estimated);
34
+ let count = 0;
35
+ const startAll = performance.now();
36
+ const deadline = startAll + maxTime;
37
+ for (let i = 0; i < maxIter && performance.now() < deadline; i++) {
38
+ const t0 = performance.now();
39
+ await bench();
40
+ samples[count++] = performance.now() - t0;
41
+ }
42
+ samples.length = count;
43
+ return { samples, totalMs: performance.now() - startAll };
44
+ },
45
+ { maxTime, maxIter },
46
+ );
47
+
48
+ const collected = await stopInstruments(cdp, opts);
49
+
50
+ return { samples, wallTimeMs: totalMs, ...collected };
51
+ }