benchforge 0.1.9 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -260
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/BenchRunner-DglX1NOn.d.mts +302 -0
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +731 -522
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +92 -120
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -26
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -48
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +138 -844
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +91 -126
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +87 -62
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +55 -53
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +94 -254
  87. package/src/matrix/VariantLoader.ts +9 -9
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +55 -13
  101. package/src/profiling/node/ResolvedProfile.ts +98 -0
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +167 -287
  115. package/src/runners/BenchRunner.ts +27 -22
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +58 -61
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +180 -296
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +162 -178
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +31 -40
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +9 -41
  135. package/src/{tests → test}/BenchMatrix.test.ts +31 -28
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +51 -0
  144. package/src/{tests → test}/MatrixFilter.test.ts +16 -16
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +57 -56
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +35 -30
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +42 -47
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BenchRunner-CSKN9zPy.d.mts +0 -225
  205. package/dist/BrowserHeapSampler-DCeL42RE.mjs +0 -202
  206. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  207. package/dist/GcStats-ByEovUi1.mjs +0 -77
  208. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  209. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  210. package/dist/TimingUtils-ClclVQ7E.mjs +0 -597
  211. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  212. package/dist/browser/index.js +0 -914
  213. package/dist/src-Cf_LXwlp.mjs +0 -2873
  214. package/dist/src-Cf_LXwlp.mjs.map +0 -1
  215. package/src/BenchMatrix.ts +0 -380
  216. package/src/BenchmarkReport.ts +0 -156
  217. package/src/HtmlDataPrep.ts +0 -148
  218. package/src/StandardSections.ts +0 -261
  219. package/src/StatisticalUtils.ts +0 -176
  220. package/src/TypeUtil.ts +0 -8
  221. package/src/browser/BrowserGcStats.ts +0 -44
  222. package/src/browser/BrowserHeapSampler.ts +0 -271
  223. package/src/export/JsonExport.ts +0 -103
  224. package/src/export/JsonFormat.ts +0 -91
  225. package/src/heap-sample/HeapSampleReport.ts +0 -196
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -152
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. package/src/{table-util/test → test}/TableValueExtractor.ts +9 -9
@@ -1,68 +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
- /** Format matrix results as one table per case */
34
+ type Row = Record<string, unknown> & { name: string };
35
+
36
+ const defaultSections: ReportSection[] = [timeSection, runsSection];
37
+
38
+ /** Format matrix results as text, with one table per case */
47
39
  export function reportMatrixResults(
48
40
  results: MatrixResults,
49
41
  options?: MatrixReportOptions,
50
42
  ): string {
51
- const tables = buildCaseTables(results, options);
52
- const header = `Matrix: ${results.name}`;
53
- return [header, ...tables].join("\n\n");
54
- }
43
+ if (results.variants.length === 0) return `Matrix: ${results.name}`;
55
44
 
56
- /** Build one table for each case showing all variants */
57
- function buildCaseTables(
58
- results: MatrixResults,
59
- options?: MatrixReportOptions,
60
- ): string[] {
61
- if (results.variants.length === 0) return [];
62
-
63
- // Get all case IDs from first variant (all variants have same cases)
45
+ // all variants have the same cases
64
46
  const caseIds = results.variants[0].cases.map(c => c.caseId);
65
- 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");
66
51
  }
67
52
 
68
53
  /** Build table for a single case showing all variants */
@@ -71,170 +56,45 @@ function buildCaseTable(
71
56
  caseId: string,
72
57
  options?: MatrixReportOptions,
73
58
  ): string {
74
- const caseTitle = formatCaseTitle(results, caseId);
75
-
76
- if (options?.sections?.length) {
77
- return buildSectionTable(results, caseId, options, caseTitle);
78
- }
79
-
80
- const rows = buildCaseRows(results, caseId, options?.extraColumns);
81
- const hasBaseline = rows.some(r => r.diffCI);
82
- const columns = buildColumns(hasBaseline, options);
83
-
84
- const resultGroup: ResultGroup<MatrixReportRow> = { results: rows };
85
- const table = buildTable(columns, [resultGroup]);
86
- return `${caseTitle}\n${table}`;
87
- }
88
-
89
- /** Build table using ResultsMapper sections */
90
- function buildSectionTable(
91
- results: MatrixResults,
92
- caseId: string,
93
- options: MatrixReportOptions,
94
- caseTitle: string,
95
- ): string {
96
- const sections = options.sections!;
97
- const variantTitle = options.variantTitle ?? "name";
98
-
99
- const rows: Record<string, unknown>[] = [];
100
- let hasBaseline = false;
101
-
102
- for (const variant of results.variants) {
103
- const caseResult = variant.cases.find(c => c.caseId === caseId);
104
- if (!caseResult) continue;
105
-
106
- const row: Record<string, unknown> = { name: truncate(variant.id, 25) };
107
-
108
- for (const section of sections) {
109
- Object.assign(
110
- row,
111
- section.extract(caseResult.measured, caseResult.metadata),
112
- );
113
- }
114
-
115
- if (caseResult.baseline) {
116
- hasBaseline = true;
117
- const { samples: base } = caseResult.baseline;
118
- row.diffCI = bootstrapDifferenceCI(base, caseResult.measured.samples);
119
- }
120
-
121
- rows.push(row);
122
- }
123
-
124
- const columnGroups = buildSectionColumns(sections, variantTitle, hasBaseline);
125
- const resultGroup: ResultGroup<Record<string, unknown>> = { results: rows };
126
- const table = buildTable(columnGroups, [resultGroup]);
127
- return `${caseTitle}\n${table}`;
128
- }
129
-
130
- /** Build column groups from ResultsMapper sections */
131
- function buildSectionColumns(
132
- sections: ResultsMapper[],
133
- variantTitle: string,
134
- hasBaseline: boolean,
135
- ): ColumnGroup<Record<string, unknown>>[] {
136
- const nameCol: ColumnGroup<Record<string, unknown>> = {
137
- columns: [{ key: "name", title: variantTitle }],
138
- };
139
-
140
- const sectionColumns = sections.flatMap(s => s.columns());
141
- const columnGroups = hasBaseline
142
- ? injectDiffColumns(sectionColumns)
143
- : (sectionColumns as ColumnGroup<Record<string, unknown>>[]);
144
-
145
- return [nameCol, ...columnGroups];
146
- }
147
-
148
- /** Build rows for all variants for a given case */
149
- function buildCaseRows(
150
- results: MatrixResults,
151
- caseId: string,
152
- extraColumns?: ExtraColumn[],
153
- ): MatrixReportRow[] {
154
- return results.variants.flatMap(variant => {
155
- const caseResult = variant.cases.find(c => c.caseId === caseId);
156
- return caseResult ? [buildRow(variant.id, caseResult, extraColumns)] : [];
157
- });
158
- }
159
-
160
- /** Build a single row from case result */
161
- function buildRow(
162
- variantId: string,
163
- caseResult: CaseResult,
164
- extraColumns?: ExtraColumn[],
165
- ): MatrixReportRow {
166
- const { measured, baseline } = caseResult;
167
- const samples = measured.samples;
168
- const time = measured.time?.avg ?? average(samples);
169
-
170
- const row: MatrixReportRow = {
171
- name: truncate(variantId, 25),
172
- time,
173
- samples: samples.length,
174
- };
175
-
176
- if (baseline) {
177
- row.diffCI = bootstrapDifferenceCI(baseline.samples, samples);
178
- }
179
-
180
- if (extraColumns) {
181
- for (const col of extraColumns) {
182
- row[col.key] = col.extract(caseResult);
183
- }
184
- }
185
-
186
- return row;
187
- }
188
-
189
- /** Build column configuration */
190
- function buildColumns(
191
- hasBaseline: boolean,
192
- options?: MatrixReportOptions,
193
- ): ColumnGroup<MatrixReportRow>[] {
59
+ const title = formatCaseTitle(results, caseId);
60
+ const sections = options?.sections ?? defaultSections;
194
61
  const variantTitle = options?.variantTitle ?? "variant";
195
- const nameCol: ColumnGroup<MatrixReportRow> = {
196
- columns: [{ key: "name", title: variantTitle }],
197
- };
198
-
199
- const ciKey = "diffCI" as keyof MatrixReportRow;
200
- const diffCol = { key: ciKey, title: "Δ% CI", formatter: formatDiff };
201
- const timeCol: ColumnGroup<MatrixReportRow> = {
202
- columns: [
203
- { key: "time", title: "time", formatter: duration },
204
- ...(hasBaseline ? [diffCol] : []),
205
- ],
206
- };
207
-
208
- const groups: ColumnGroup<MatrixReportRow>[] = [nameCol, timeCol];
209
-
210
- // Add extra columns, grouped by groupTitle
211
- const extraColumns = options?.extraColumns;
212
- if (extraColumns?.length) {
213
- const byGroup = new Map<string | undefined, ExtraColumn[]>();
214
- for (const col of extraColumns) {
215
- const group = byGroup.get(col.groupTitle) ?? [];
216
- group.push(col);
217
- byGroup.set(col.groupTitle, group);
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
+ );
218
79
  }
219
- for (const [groupTitle, cols] of byGroup) {
220
- groups.push({
221
- groupTitle,
222
- columns: cols.map(col => ({
223
- key: col.key as keyof MatrixReportRow,
224
- title: col.title,
225
- formatter: col.formatter ?? String,
226
- })),
80
+ const out: Row[] = [row];
81
+ if (cr.baseline && !shared)
82
+ out.push({
83
+ name: " \u21B3 baseline",
84
+ ...extractSectionValues(cr.baseline, sections, cr.metadata),
227
85
  });
228
- }
229
- }
86
+ return out;
87
+ });
230
88
 
231
- return groups;
232
- }
89
+ if (shared)
90
+ rows.push({
91
+ name: "=> baseline",
92
+ ...extractSectionValues(shared, sections),
93
+ });
233
94
 
234
- /** Format diff with CI, or "baseline" marker */
235
- function formatDiff(value: unknown): string | null {
236
- if (!value) return null;
237
- return formatDiffWithCI(value as DifferenceCI);
95
+ const hasDiff = rows.some(r => r.diffCI);
96
+ const cols = sectionColumnGroups(sections, hasDiff, variantTitle);
97
+ return `${title}\n${buildTable(cols, [{ results: rows }])}`;
238
98
  }
239
99
 
240
100
  /** Format case title with metadata if available */
@@ -242,49 +102,29 @@ function formatCaseTitle(results: MatrixResults, caseId: string): string {
242
102
  const caseResult = results.variants[0]?.cases.find(c => c.caseId === caseId);
243
103
  const metadata = caseResult?.metadata;
244
104
 
245
- if (metadata && Object.keys(metadata).length > 0) {
246
- const metaParts = Object.entries(metadata)
247
- .map(([k, v]) => `${v} ${k}`)
248
- .join(", ");
249
- return `${caseId} (${metaParts})`;
250
- }
251
- 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})`;
252
110
  }
253
111
 
254
- /** GC statistics columns - derived from gcStatsSection for consistency */
255
- export const gcStatsColumns: ExtraColumn[] = gcStatsSection
256
- .columns()[0]
257
- .columns.map(col => ({
258
- key: col.key as string,
259
- title: col.title,
260
- groupTitle: "GC",
261
- extract: (r: CaseResult) =>
262
- gcStatsSection.extract(r.measured)[col.key as keyof GcStatsInfo],
263
- formatter: (v: unknown) => col.formatter?.(v) ?? "-",
264
- }));
265
-
266
- /** Format bytes with fallback to "-" for missing values */
267
- function formatBytesOrDash(value: unknown): string {
268
- return formatBytes(value) ?? "-";
112
+ /** Collect (variant, caseResult) pairs for a given caseId */
113
+ function collectCaseResults(
114
+ results: MatrixResults,
115
+ caseId: string,
116
+ ): VariantCase[] {
117
+ return results.variants.flatMap(variant => {
118
+ const cr = variant.cases.find(c => c.caseId === caseId);
119
+ return cr ? [{ variant, cr }] : [];
120
+ });
269
121
  }
270
122
 
271
- /** GC pause time column */
272
- export const gcPauseColumn: ExtraColumn = {
273
- key: "gcPause",
274
- title: "pause",
275
- groupTitle: "GC",
276
- extract: r => r.measured.gcStats?.gcPauseTime,
277
- formatter: v => (v != null ? `${(v as number).toFixed(1)}ms` : "-"),
278
- };
279
-
280
- /** Heap sampling total bytes column */
281
- export const heapTotalColumn: ExtraColumn = {
282
- key: "heapTotal",
283
- title: "heap",
284
- extract: r => {
285
- const profile = r.measured.heapProfile;
286
- if (!profile?.head) return undefined;
287
- return totalProfileBytes(profile);
288
- },
289
- formatter: formatBytesOrDash,
290
- };
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;
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,7 +22,12 @@ export async function loadVariant<T = unknown>(
22
22
  return extractVariant(module, variantId, moduleUrl);
23
23
  }
24
24
 
25
- /** Extract variant from module exports */
25
+ /** Resolve the import URL for a variant file */
26
+ export function variantModuleUrl(dirUrl: string, variantId: string): string {
27
+ return new URL(`${variantId}.ts`, dirUrl).href;
28
+ }
29
+
30
+ /** Validate and extract a Variant from a module's exports */
26
31
  function extractVariant<T>(
27
32
  module: Record<string, unknown>,
28
33
  variantId: string,
@@ -39,8 +44,3 @@ function extractVariant<T>(
39
44
  }
40
45
  return { setup: setup as (data: T) => unknown, run: run as () => void };
41
46
  }
42
-
43
- /** Get module URL for a variant in a directory */
44
- export function variantModuleUrl(dirUrl: string, variantId: string): string {
45
- return new URL(`${variantId}.ts`, dirUrl).href;
46
- }
@@ -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
+ }
@@ -0,0 +1,133 @@
1
+ import type { GcStats } from "../../runners/GcStats.ts";
2
+ import type { CoverageData, ScriptCoverage } from "../node/CoverageTypes.ts";
3
+ import type { HeapProfile } from "../node/HeapSampler.ts";
4
+ import type { TimeProfile } from "../node/TimeSampler.ts";
5
+ import { browserGcStats } from "./BrowserGcStats.ts";
6
+ import type { CdpClient } from "./CdpClient.ts";
7
+ import type { TraceEvent } from "./ChromeTraceEvent.ts";
8
+
9
+ /** Options controlling which CDP instruments (heap, CPU, coverage) to enable. */
10
+ export interface InstrumentOpts {
11
+ alloc: boolean;
12
+ profile: boolean;
13
+ callCounts: boolean;
14
+ samplingInterval: number;
15
+ profileInterval?: number;
16
+ }
17
+
18
+ /** Build InstrumentOpts from profile params and heap sampling interval. */
19
+ export function instrumentOpts(
20
+ params: {
21
+ alloc?: boolean;
22
+ profile?: boolean;
23
+ callCounts?: boolean;
24
+ profileInterval?: number;
25
+ },
26
+ samplingInterval: number,
27
+ ): InstrumentOpts {
28
+ const {
29
+ alloc = false,
30
+ profile = false,
31
+ callCounts = false,
32
+ profileInterval,
33
+ } = params;
34
+ return { alloc, profile, callCounts, samplingInterval, profileInterval };
35
+ }
36
+
37
+ /** Start CDP GC tracing; returns the mutable array that collects trace events. */
38
+ export async function startGcTracing(cdp: CdpClient): Promise<TraceEvent[]> {
39
+ const events: TraceEvent[] = [];
40
+ cdp.on("Tracing.dataCollected", ({ value }) => {
41
+ events.push(...(value as unknown as TraceEvent[]));
42
+ });
43
+ await cdp.send("Tracing.start", {
44
+ traceConfig: { includedCategories: ["v8", "v8.gc"] },
45
+ });
46
+ return events;
47
+ }
48
+
49
+ /** End CDP tracing and aggregate collected events into GcStats. */
50
+ export async function collectTracing(
51
+ cdp: CdpClient,
52
+ traceEvents: TraceEvent[],
53
+ ): Promise<GcStats> {
54
+ const done = new Promise<void>(r =>
55
+ cdp.once("Tracing.tracingComplete", () => r()),
56
+ );
57
+ await cdp.send("Tracing.end");
58
+ await done;
59
+ return browserGcStats(traceEvents);
60
+ }
61
+
62
+ /** Start CDP Profiler for CPU time sampling (caller manages Profiler.enable/disable) */
63
+ export async function startTimeProfiling(
64
+ cdp: CdpClient,
65
+ interval?: number,
66
+ ): Promise<void> {
67
+ if (interval) await cdp.send("Profiler.setSamplingInterval", { interval });
68
+ await cdp.send("Profiler.start");
69
+ }
70
+
71
+ /** Stop CDP CPU sampling and return the profile. */
72
+ export async function stopTimeProfiling(cdp: CdpClient): Promise<TimeProfile> {
73
+ const { profile } = await cdp.send("Profiler.stop");
74
+ return profile as unknown as TimeProfile;
75
+ }
76
+
77
+ /** Start precise coverage (caller manages Profiler.enable/disable). */
78
+ export async function startCoverageCollection(cdp: CdpClient): Promise<void> {
79
+ await cdp.send("Profiler.startPreciseCoverage", {
80
+ callCount: true,
81
+ detailed: true,
82
+ });
83
+ }
84
+
85
+ /** Collect precise coverage, filtering out browser-internal scripts. */
86
+ export async function collectCoverage(cdp: CdpClient): Promise<CoverageData> {
87
+ const { result } = await cdp.send("Profiler.takePreciseCoverage");
88
+ await cdp.send("Profiler.stopPreciseCoverage");
89
+ const scripts = (result as unknown as ScriptCoverage[]).filter(isPageScript);
90
+ return { scripts };
91
+ }
92
+
93
+ /** Stop active instruments and return collected profiles/coverage. */
94
+ export async function stopInstruments(
95
+ cdp: CdpClient,
96
+ opts: InstrumentOpts,
97
+ ): Promise<{
98
+ heapProfile?: HeapProfile;
99
+ timeProfile?: TimeProfile;
100
+ coverage?: CoverageData;
101
+ }> {
102
+ const heapProfile = opts.alloc
103
+ ? ((await cdp.send("HeapProfiler.stopSampling")).profile as HeapProfile)
104
+ : undefined;
105
+ const timeProfile = opts.profile ? await stopTimeProfiling(cdp) : undefined;
106
+ const coverage = opts.callCounts ? await collectCoverage(cdp) : undefined;
107
+ if (opts.profile || opts.callCounts) await cdp.send("Profiler.disable");
108
+ return { heapProfile, timeProfile, coverage };
109
+ }
110
+
111
+ /** Start requested CDP instruments (heap, CPU, coverage). */
112
+ export async function startInstruments(
113
+ cdp: CdpClient,
114
+ opts: InstrumentOpts,
115
+ ): Promise<void> {
116
+ if (opts.alloc) {
117
+ await cdp.send("HeapProfiler.startSampling", {
118
+ samplingInterval: opts.samplingInterval,
119
+ includeObjectsCollectedByMajorGC: true,
120
+ includeObjectsCollectedByMinorGC: true,
121
+ });
122
+ }
123
+ if (opts.profile || opts.callCounts) await cdp.send("Profiler.enable");
124
+ if (opts.profile) await startTimeProfiling(cdp, opts.profileInterval);
125
+ if (opts.callCounts) await startCoverageCollection(cdp);
126
+ }
127
+
128
+ /** Exclude chrome:// and devtools:// internal scripts. */
129
+ function isPageScript(s: ScriptCoverage): boolean {
130
+ return (
131
+ !!s.url && !s.url.startsWith("chrome") && !s.url.startsWith("devtools")
132
+ );
133
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ aggregateGcStats,
3
+ type GcEvent,
4
+ type GcStats,
5
+ } from "../../runners/GcStats.ts";
6
+ import type { TraceEvent } from "./ChromeTraceEvent.ts";
7
+
8
+ /** Convert MinorGC/MajorGC trace events into GcEvent[]. */
9
+ export function parseGcTraceEvents(traceEvents: TraceEvent[]): GcEvent[] {
10
+ return traceEvents
11
+ .filter(e => e.ph === "X" && gcType(e.name))
12
+ .map(e => ({
13
+ type: gcType(e.name)!,
14
+ pauseMs: (e.dur ?? 0) / 1000,
15
+ collected: Math.max(
16
+ 0,
17
+ Number(e.args?.usedHeapSizeBefore ?? 0) -
18
+ Number(e.args?.usedHeapSizeAfter ?? 0),
19
+ ),
20
+ }));
21
+ }
22
+
23
+ /** Parse and aggregate CDP trace events into GcStats. */
24
+ export function browserGcStats(traceEvents: TraceEvent[]): GcStats {
25
+ return aggregateGcStats(parseGcTraceEvents(traceEvents));
26
+ }
27
+
28
+ /** Map CDP event names (MinorGC/MajorGC) to GcEvent type. */
29
+ function gcType(name: string): GcEvent["type"] | undefined {
30
+ if (name === "MinorGC") return "scavenge";
31
+ if (name === "MajorGC") return "mark-compact";
32
+ return undefined;
33
+ }