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
@@ -0,0 +1,223 @@
1
+ import { cliDefaults } from "../cli/CliArgs.ts";
2
+ import type { CoverageData } from "../profiling/node/CoverageTypes.ts";
3
+ import {
4
+ filterSites,
5
+ flattenProfile,
6
+ totalBytes,
7
+ } from "../profiling/node/HeapSampleReport.ts";
8
+ import type { HeapProfile } from "../profiling/node/HeapSampler.ts";
9
+ import { resolveProfile } from "../profiling/node/ResolvedProfile.ts";
10
+ import type { MeasuredResults } from "../runners/MeasuredResults.ts";
11
+ import type { DifferenceCI } from "../stats/StatisticalUtils.ts";
12
+ import type {
13
+ BenchmarkEntry,
14
+ BenchmarkGroup,
15
+ CoverageSummary,
16
+ HeapSummary,
17
+ ReportData,
18
+ ViewerSection,
19
+ } from "../viewer/ReportData.ts";
20
+ import {
21
+ type BenchmarkReport,
22
+ type ComparisonOptions,
23
+ hasField,
24
+ type ReportGroup,
25
+ type ReportSection,
26
+ type UnknownRecord,
27
+ } from "./BenchmarkReport.ts";
28
+ import { gcStatsSection } from "./GcSections.ts";
29
+ import type { GitVersion } from "./GitUtils.ts";
30
+ import {
31
+ buildTimeSection,
32
+ optSection,
33
+ runsSection,
34
+ } from "./StandardSections.ts";
35
+ import {
36
+ buildViewerSections,
37
+ hasLowBatchCount,
38
+ isSingleBatch,
39
+ minBatches,
40
+ } from "./ViewerSections.ts";
41
+
42
+ /** Options for prepareHtmlData: report sections, git versions, and CLI args */
43
+ export interface PrepareHtmlOptions extends ComparisonOptions {
44
+ cliArgs?: Record<string, unknown>;
45
+ sections?: ReportSection[];
46
+ currentVersion?: GitVersion;
47
+ baselineVersion?: GitVersion;
48
+ }
49
+
50
+ /** Context shared across reports in a group */
51
+ interface GroupContext {
52
+ baseM?: MeasuredResults;
53
+ baseMeta?: UnknownRecord;
54
+ sections?: ReportSection[];
55
+ comparison?: ComparisonOptions;
56
+ lowBatches: boolean;
57
+ }
58
+
59
+ /** Convert benchmark results into a ReportData payload for the HTML viewer */
60
+ export function prepareHtmlData(
61
+ groups: ReportGroup[],
62
+ options: PrepareHtmlOptions,
63
+ ): ReportData {
64
+ const { cliArgs, currentVersion, baselineVersion, equivMargin, noBatchTrim } =
65
+ options;
66
+ const comparison: ComparisonOptions = { equivMargin, noBatchTrim };
67
+ const sections = options.sections ?? defaultSections(groups, cliArgs);
68
+ return {
69
+ groups: groups.map(g => prepareGroupData(g, sections, comparison)),
70
+ metadata: {
71
+ timestamp: new Date().toISOString(),
72
+ bencherVersion: process.env.npm_package_version || "unknown",
73
+ cliArgs,
74
+ cliDefaults,
75
+ gcTrackingEnabled: cliArgs?.["gc-stats"] === true,
76
+ currentVersion,
77
+ baselineVersion,
78
+ environment: {
79
+ node: process.version,
80
+ platform: process.platform,
81
+ arch: process.arch,
82
+ },
83
+ },
84
+ };
85
+ }
86
+
87
+ /** Build default sections when caller doesn't provide custom ones */
88
+ function defaultSections(
89
+ groups: ReportGroup[],
90
+ cliArgs?: Record<string, unknown>,
91
+ ): ReportSection[] {
92
+ const hasGc = cliArgs?.["gc-stats"] === true;
93
+ const hasOpt = hasField(groups, "optStatus");
94
+ const stats = typeof cliArgs?.stats === "string" ? cliArgs.stats : undefined;
95
+ return [
96
+ buildTimeSection(stats),
97
+ hasGc ? gcStatsSection : undefined,
98
+ hasOpt ? optSection : undefined,
99
+ runsSection,
100
+ ].filter((s): s is ReportSection => s !== undefined);
101
+ }
102
+
103
+ /** @return group data with structured ViewerSections and bootstrap CIs */
104
+ function prepareGroupData(
105
+ group: ReportGroup,
106
+ sections?: ReportSection[],
107
+ comparison?: ComparisonOptions,
108
+ ): BenchmarkGroup {
109
+ const base = group.baseline;
110
+ const baseM = base?.measuredResults;
111
+ const baseline = base
112
+ ? { ...prepareBenchmarkData(base), comparisonCI: undefined }
113
+ : undefined;
114
+ const curM = group.reports[0]?.measuredResults;
115
+ const singleBatch = isSingleBatch(baseM, curM);
116
+ const lowBatches = hasLowBatchCount(baseM, curM);
117
+ const baseMeta = base?.metadata;
118
+ const ctx: GroupContext = {
119
+ baseM,
120
+ baseMeta,
121
+ sections,
122
+ comparison,
123
+ lowBatches,
124
+ };
125
+
126
+ return {
127
+ name: group.name,
128
+ baseline,
129
+ warnings: buildWarnings(singleBatch, lowBatches),
130
+ benchmarks: group.reports.map(r => prepareReportEntry(r, ctx)),
131
+ };
132
+ }
133
+
134
+ /** @return benchmark data with samples, stats, and profiling summaries */
135
+ function prepareBenchmarkData(report: {
136
+ name: string;
137
+ measuredResults: MeasuredResults;
138
+ metadata?: UnknownRecord;
139
+ }): Omit<BenchmarkEntry, "comparisonCI" | "sections"> {
140
+ const { measuredResults: m, name } = report;
141
+ return {
142
+ name,
143
+ samples: m.samples,
144
+ warmupSamples: m.warmupSamples,
145
+ allocationSamples: m.allocationSamples,
146
+ heapSamples: m.heapSamples,
147
+ gcEvents: m.nodeGcTime?.events,
148
+ optSamples: m.optSamples,
149
+ pausePoints: m.pausePoints,
150
+ batchOffsets: m.batchOffsets,
151
+ stats: m.time,
152
+ heapSize: m.heapSize,
153
+ totalTime: m.totalTime,
154
+ heapSummary: m.heapProfile ? summarizeHeap(m.heapProfile) : undefined,
155
+ coverageSummary: m.coverage ? summarizeCoverage(m.coverage) : undefined,
156
+ };
157
+ }
158
+
159
+ function buildWarnings(
160
+ singleBatch: boolean,
161
+ lowBatches: boolean,
162
+ ): string[] | undefined {
163
+ const parts: string[] = [];
164
+ const singleMsg =
165
+ "Confidence intervals may be too narrow (single batch). Use --batches for more accurate intervals.";
166
+ if (singleBatch) parts.push(singleMsg);
167
+ if (lowBatches)
168
+ parts.push(
169
+ `Too few batches for reliable comparison (need ${minBatches}+).`,
170
+ );
171
+ return parts.length ? parts : undefined;
172
+ }
173
+
174
+ /** @return a single benchmark entry with sections and comparison CI */
175
+ function prepareReportEntry(
176
+ report: BenchmarkReport,
177
+ ctx: GroupContext,
178
+ ): BenchmarkEntry {
179
+ const m = report.measuredResults;
180
+ const sectionCtx = {
181
+ current: m,
182
+ baseline: ctx.baseM,
183
+ currentMeta: report.metadata,
184
+ baselineMeta: ctx.baseMeta,
185
+ comparison: ctx.comparison,
186
+ };
187
+ const sections = ctx.sections
188
+ ? buildViewerSections(ctx.sections, sectionCtx)
189
+ : undefined;
190
+ // Primary CI comes from the first primary row's comparisonCI (avoids duplicate bootstrap)
191
+ const comparisonCI = findPrimarySectionCI(sections);
192
+ return { ...prepareBenchmarkData(report), sections, comparisonCI };
193
+ }
194
+
195
+ /** Compute heap allocation summary from profile */
196
+ function summarizeHeap(profile: HeapProfile): HeapSummary {
197
+ const resolved = resolveProfile(profile);
198
+ const userSites = filterSites(flattenProfile(resolved));
199
+ return { totalBytes: resolved.totalBytes, userBytes: totalBytes(userSites) };
200
+ }
201
+
202
+ /** Compute coverage summary from V8 coverage data */
203
+ function summarizeCoverage(coverage: CoverageData): CoverageSummary {
204
+ const fns = coverage.scripts.flatMap(s => s.functions);
205
+ const called = fns.filter(
206
+ fn => fn.ranges.length > 0 && fn.ranges[0].count > 0,
207
+ );
208
+ const totalCalls = called.reduce((sum, fn) => sum + fn.ranges[0].count, 0);
209
+ return { functionCount: called.length, totalCalls };
210
+ }
211
+
212
+ /** Extract the comparison CI from the first primary row across all sections */
213
+ function findPrimarySectionCI(
214
+ sections: ViewerSection[] | undefined,
215
+ ): DifferenceCI | undefined {
216
+ if (!sections) return undefined;
217
+ for (const section of sections) {
218
+ for (const row of section.rows) {
219
+ if (row.primary && row.comparisonCI) return row.comparisonCI;
220
+ }
221
+ }
222
+ return undefined;
223
+ }
@@ -0,0 +1,73 @@
1
+ import type { StatKind } from "../stats/StatisticalUtils.ts";
2
+
3
+ /** Parsed spec for one timing column selected via --stats. */
4
+ export interface StatSpec {
5
+ key: string;
6
+ title: string;
7
+ statKind: StatKind;
8
+ }
9
+
10
+ /** Parse --stats into column specs. Throws on empty/invalid tokens. */
11
+ export function parseStatsArg(stats: string): StatSpec[] {
12
+ const tokens = stats
13
+ .split(",")
14
+ .map(t => t.trim())
15
+ .filter(Boolean);
16
+ if (tokens.length === 0) {
17
+ throw new Error("--stats must list at least one column");
18
+ }
19
+ const seen = new Set<string>();
20
+ const specs: StatSpec[] = [];
21
+ for (const token of tokens) {
22
+ const spec = parseStatToken(token);
23
+ if (seen.has(spec.key)) continue;
24
+ seen.add(spec.key);
25
+ specs.push(spec);
26
+ }
27
+ return specs;
28
+ }
29
+
30
+ /** @return stat spec for a single --stats token. Throws on invalid input. */
31
+ function parseStatToken(token: string): StatSpec {
32
+ const lower = token.toLowerCase();
33
+ if (lower === "mean" || lower === "avg") {
34
+ return { key: "mean", title: "mean", statKind: "mean" };
35
+ }
36
+ if (lower === "median") {
37
+ return { key: "p50", title: "p50", statKind: { percentile: 0.5 } };
38
+ }
39
+ if (lower === "min") {
40
+ return { key: "min", title: "min", statKind: "min" };
41
+ }
42
+ if (lower === "max") {
43
+ return { key: "max", title: "max", statKind: "max" };
44
+ }
45
+ const m = lower.match(/^p(\d+)$/);
46
+ if (m) return parsePercentileToken(token, m[1]);
47
+ throw new Error(
48
+ `invalid --stats token "${token}": expected mean, median, min, max, or p<N> (e.g. p50, p99, p999)`,
49
+ );
50
+ }
51
+
52
+ /** @return spec for a p<N> token, enforcing the 2-digit minimum and 9-prefix rule. */
53
+ function parsePercentileToken(token: string, digits: string): StatSpec {
54
+ if (digits.length < 2) {
55
+ throw new Error(
56
+ `invalid --stats token "${token}": percentile needs at least 2 digits (e.g. p05, p50, p99, p999)`,
57
+ );
58
+ }
59
+ // 3+ digit tokens express sub-percentile precision (p999 = 99.9%,
60
+ // p9999 = 99.99%). Require leading 9 so p100/p500 don't silently
61
+ // map to 10%/50% — use 2-digit p10/p50 for those.
62
+ if (digits.length > 2 && digits[0] !== "9") {
63
+ throw new Error(
64
+ `invalid --stats token "${token}": percentiles with 3+ digits must start with 9 (e.g. p999, p9999); otherwise use 2-digit form (e.g. p50)`,
65
+ );
66
+ }
67
+ const q = Number(digits) / 10 ** digits.length;
68
+ return {
69
+ key: `p${digits}`,
70
+ title: `p${digits}`,
71
+ statKind: { percentile: q },
72
+ };
73
+ }
@@ -0,0 +1,147 @@
1
+ import type {
2
+ MeasuredResults,
3
+ OptStatusInfo,
4
+ } from "../runners/MeasuredResults.ts";
5
+ import { isBootstrappable } from "../stats/StatisticalUtils.ts";
6
+ import type { ReportSection } from "./BenchmarkReport.ts";
7
+ import { formatConvergence, timeMs } from "./Formatters.ts";
8
+ import { gcSections } from "./GcSections.ts";
9
+ import { parseStatsArg } from "./ParseStats.ts";
10
+
11
+ /** Default timing section: mean, p50, p99. */
12
+ export const timeSection: ReportSection = buildTimeSection();
13
+
14
+ /** Report section: number of sample iterations. */
15
+ export const runsSection: ReportSection = {
16
+ title: "",
17
+ columns: [
18
+ {
19
+ key: "runs",
20
+ title: "runs",
21
+ formatter: v => String(v),
22
+ value: (r: MeasuredResults) => r.samples.length,
23
+ },
24
+ ],
25
+ };
26
+
27
+ /** Report section: total sampling duration. */
28
+ export const totalTimeSection: ReportSection = {
29
+ title: "",
30
+ columns: [
31
+ {
32
+ key: "totalTime",
33
+ title: "time",
34
+ formatter: formatTotalTime,
35
+ value: (r: MeasuredResults) => r.totalTime,
36
+ },
37
+ ],
38
+ };
39
+
40
+ /** Report sections: timing stats and convergence for adaptive mode. */
41
+ export const adaptiveSections: ReportSection[] = [
42
+ {
43
+ title: "time",
44
+ columns: [
45
+ {
46
+ key: "median",
47
+ title: "median",
48
+ formatter: timeMs,
49
+ comparable: true,
50
+ statKind: { percentile: 0.5 },
51
+ },
52
+ {
53
+ key: "mean",
54
+ title: "mean",
55
+ formatter: timeMs,
56
+ comparable: true,
57
+ statKind: "mean",
58
+ },
59
+ {
60
+ key: "p99",
61
+ title: "p99",
62
+ formatter: timeMs,
63
+ statKind: { percentile: 0.99 },
64
+ },
65
+ ],
66
+ },
67
+ {
68
+ title: "",
69
+ columns: [
70
+ {
71
+ key: "convergence",
72
+ title: "conv%",
73
+ formatter: formatConvergence,
74
+ value: (r: MeasuredResults) => r.convergence?.confidence,
75
+ },
76
+ ],
77
+ },
78
+ ];
79
+
80
+ /** Report section: V8 optimization tier distribution and deopt count. */
81
+ export const optSection: ReportSection = {
82
+ title: "v8 opt",
83
+ columns: [
84
+ {
85
+ key: "tiers",
86
+ title: "tiers",
87
+ formatter: v => (typeof v === "string" ? v : ""),
88
+ value: (r: MeasuredResults) => {
89
+ const opt = r.optStatus;
90
+ return opt ? formatTierSummary(opt) : undefined;
91
+ },
92
+ },
93
+ {
94
+ key: "deopt",
95
+ title: "deopt",
96
+ formatter: v => (typeof v === "number" ? String(v) : ""),
97
+ value: (r: MeasuredResults) => {
98
+ const opt = r.optStatus;
99
+ return opt && opt.deoptCount > 0 ? opt.deoptCount : undefined;
100
+ },
101
+ },
102
+ ],
103
+ };
104
+
105
+ /** Build a time section with user-chosen percentile/stat columns. */
106
+ export function buildTimeSection(stats = "mean,p50,p99"): ReportSection {
107
+ const specs = parseStatsArg(stats);
108
+ return {
109
+ title: "time",
110
+ columns: specs.map(s => ({
111
+ key: s.key,
112
+ title: s.title,
113
+ formatter: timeMs,
114
+ comparable: isBootstrappable(s.statKind),
115
+ statKind: s.statKind,
116
+ })),
117
+ };
118
+ }
119
+
120
+ /** Format V8 tier distribution sorted by count (e.g. "turbofan:85% sparkplug:15%"). */
121
+ export function formatTierSummary(
122
+ opt: OptStatusInfo,
123
+ sep = ":",
124
+ glue = " ",
125
+ ): string {
126
+ const tiers = Object.entries(opt.byTier);
127
+ const total = tiers.reduce((s, [, t]) => s + t.count, 0);
128
+ const pct = (n: number) => `${((n / total) * 100).toFixed(0)}%`;
129
+ return tiers
130
+ .sort((a, b) => b[1].count - a[1].count)
131
+ .map(([name, t]) => `${name}${sep}${pct(t.count)}`)
132
+ .join(glue);
133
+ }
134
+
135
+ /** @return default report sections from CLI flags (GC stats if enabled, plus run count). */
136
+ export function buildGenericSections(args: {
137
+ "gc-stats"?: boolean;
138
+ alloc?: boolean;
139
+ }): ReportSection[] {
140
+ return [...gcSections(args), runsSection];
141
+ }
142
+
143
+ /** Format total time; brackets indicate >= 30s. */
144
+ function formatTotalTime(v: unknown): string {
145
+ if (typeof v !== "number") return "";
146
+ return v >= 30 ? `[${v.toFixed(1)}s]` : `${v.toFixed(1)}s`;
147
+ }