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,263 +0,0 @@
1
- import { createCIPlot } from "./CIPlot.ts";
2
- import { createHistogramKde } from "./HistogramKde.ts";
3
- import { createSampleTimeSeries } from "./SampleTimeSeries.ts";
4
- import type {
5
- BenchmarkEntry,
6
- GcEvent,
7
- HeapPoint,
8
- PausePoint,
9
- ReportData,
10
- Sample,
11
- TimeSeriesPoint,
12
- } from "./Types.ts";
13
-
14
- interface PreparedBenchmark extends BenchmarkEntry {
15
- name: string;
16
- }
17
-
18
- interface FlattenedData {
19
- allSamples: Sample[];
20
- timeSeries: TimeSeriesPoint[];
21
- heapSeries: HeapPoint[];
22
- allGcEvents: GcEvent[];
23
- allPausePoints: PausePoint[];
24
- }
25
-
26
- /** Render all plots for the benchmark report */
27
- export function renderPlots(data: ReportData): void {
28
- const gcEnabled = data.metadata.gcTrackingEnabled ?? false;
29
- data.groups.forEach((group, groupIndex) => {
30
- try {
31
- renderGroup(group, groupIndex, gcEnabled);
32
- } catch (error) {
33
- console.error("Error rendering plots for group", groupIndex, error);
34
- showError(
35
- groupIndex,
36
- `Error rendering visualizations: ${(error as Error).message}`,
37
- );
38
- }
39
- });
40
- }
41
-
42
- function renderGroup(
43
- group: ReportData["groups"][0],
44
- groupIndex: number,
45
- gcEnabled: boolean,
46
- ): void {
47
- const benchmarks = prepareBenchmarks(group);
48
- if (benchmarks.length === 0 || !benchmarks[0].samples?.length) {
49
- showError(groupIndex, "No sample data available for visualization");
50
- return;
51
- }
52
- const flattened = flattenSamples(benchmarks);
53
- const benchmarkNames = benchmarks.map(b => b.name);
54
-
55
- const currentBenchmark = benchmarks.find(b => !b.isBaseline);
56
- if (currentBenchmark?.comparisonCI?.histogram) {
57
- renderToContainer(`#ci-plot-${groupIndex}`, true, () =>
58
- createCIPlot(currentBenchmark.comparisonCI!),
59
- );
60
- }
61
-
62
- renderToContainer(
63
- `#histogram-${groupIndex}`,
64
- flattened.allSamples.length > 0,
65
- () => createHistogramKde(flattened.allSamples, benchmarkNames),
66
- );
67
- const { timeSeries, allGcEvents, allPausePoints, heapSeries } = flattened;
68
- renderToContainer(
69
- `#sample-timeseries-${groupIndex}`,
70
- timeSeries.length > 0,
71
- () =>
72
- createSampleTimeSeries(
73
- timeSeries,
74
- allGcEvents,
75
- allPausePoints,
76
- heapSeries,
77
- ),
78
- );
79
-
80
- const statsContainer = document.querySelector(`#stats-${groupIndex}`);
81
- if (statsContainer)
82
- statsContainer.innerHTML = benchmarks
83
- .map(b => generateStatsHtml(b, gcEnabled))
84
- .join("");
85
- }
86
-
87
- function showError(groupIndex: number, message: string): void {
88
- const container = document.querySelector(`#group-${groupIndex}`);
89
- if (container) container.innerHTML = `<div class="error">${message}</div>`;
90
- }
91
-
92
- /** Combine baseline and benchmarks into a single list with display names */
93
- function prepareBenchmarks(
94
- group: ReportData["groups"][0],
95
- ): PreparedBenchmark[] {
96
- const benchmarks: PreparedBenchmark[] = [];
97
- if (group.baseline) {
98
- const name = group.baseline.name + " (baseline)";
99
- benchmarks.push({ ...group.baseline, name, isBaseline: true });
100
- }
101
- for (const b of group.benchmarks)
102
- benchmarks.push({ ...b, isBaseline: false });
103
- return benchmarks;
104
- }
105
-
106
- function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
107
- const result: FlattenedData = {
108
- allSamples: [],
109
- timeSeries: [],
110
- heapSeries: [],
111
- allGcEvents: [],
112
- allPausePoints: [],
113
- };
114
- for (const b of benchmarks)
115
- if (b.samples?.length) flattenBenchmark(b, result);
116
- return result;
117
- }
118
-
119
- /** Clear a container element and append a freshly created plot */
120
- function renderToContainer(
121
- selector: string,
122
- condition: boolean,
123
- create: () => SVGSVGElement | HTMLElement,
124
- ): void {
125
- const container = document.querySelector(selector);
126
- if (!container || !condition) return;
127
- container.innerHTML = "";
128
- container.appendChild(create());
129
- }
130
-
131
- function generateStatsHtml(b: PreparedBenchmark, gcEnabled: boolean): string {
132
- const ciHtml = generateCIHtml(b.comparisonCI);
133
-
134
- if (b.sectionStats?.length) {
135
- const stats = gcEnabled
136
- ? b.sectionStats
137
- : b.sectionStats.filter(s => s.groupTitle !== "gc");
138
- const statsHtml = stats
139
- .map(
140
- stat => `
141
- <div class="stat-item">
142
- <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
143
- <div class="stat-value">${stat.value}</div>
144
- </div>
145
- `,
146
- )
147
- .join("");
148
- return `
149
- <div class="summary-stats">
150
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
151
- <div class="stats-grid">${ciHtml}${statsHtml}</div>
152
- </div>
153
- `;
154
- }
155
-
156
- // Fallback to hardcoded stats
157
- return `
158
- <div class="summary-stats">
159
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
160
- <div class="stats-grid">
161
- ${ciHtml}
162
- <div class="stat-item">
163
- <div class="stat-label">Min</div>
164
- <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
165
- </div>
166
- <div class="stat-item">
167
- <div class="stat-label">Median</div>
168
- <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
169
- </div>
170
- <div class="stat-item">
171
- <div class="stat-label">Mean</div>
172
- <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
173
- </div>
174
- <div class="stat-item">
175
- <div class="stat-label">Max</div>
176
- <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
177
- </div>
178
- <div class="stat-item">
179
- <div class="stat-label">P75</div>
180
- <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
181
- </div>
182
- <div class="stat-item">
183
- <div class="stat-label">P99</div>
184
- <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
185
- </div>
186
- </div>
187
- </div>
188
- `;
189
- }
190
-
191
- /** Extract time series, heap, GC, and pause data from one benchmark */
192
- function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
193
- const warmupCount = b.warmupSamples?.length || 0;
194
- b.warmupSamples?.forEach((value, i) => {
195
- out.timeSeries.push({
196
- benchmark: b.name,
197
- iteration: i - warmupCount,
198
- value,
199
- isWarmup: true,
200
- });
201
- });
202
-
203
- const sampleEndTimes = cumulativeSum(b.samples);
204
- b.samples.forEach((value, i) => {
205
- out.allSamples.push({ benchmark: b.name, value, iteration: i });
206
- out.timeSeries.push({
207
- benchmark: b.name,
208
- iteration: i,
209
- value,
210
- isWarmup: false,
211
- optStatus: b.optSamples?.[i],
212
- });
213
- if (b.heapSamples?.[i] !== undefined) {
214
- out.heapSeries.push({
215
- benchmark: b.name,
216
- iteration: i,
217
- value: b.heapSamples[i],
218
- });
219
- }
220
- });
221
-
222
- b.gcEvents?.forEach(gc => {
223
- const idx = sampleEndTimes.findIndex(t => t >= gc.offset);
224
- out.allGcEvents.push({
225
- benchmark: b.name,
226
- sampleIndex: idx >= 0 ? idx : b.samples.length - 1,
227
- duration: gc.duration,
228
- });
229
- });
230
- b.pausePoints?.forEach(p => {
231
- out.allPausePoints.push({
232
- benchmark: b.name,
233
- sampleIndex: p.sampleIndex,
234
- durationMs: p.durationMs,
235
- });
236
- });
237
- }
238
-
239
- function generateCIHtml(ci: BenchmarkEntry["comparisonCI"]): string {
240
- if (!ci) return "";
241
- const text = `${formatPct(ci.percent)} [${formatPct(ci.ci[0])}, ${formatPct(ci.ci[1])}]`;
242
- return `
243
- <div class="stat-item">
244
- <div class="stat-label">vs Baseline</div>
245
- <div class="stat-value ci-${ci.direction}">${text}</div>
246
- </div>
247
- `;
248
- }
249
-
250
- function cumulativeSum(arr: number[]): number[] {
251
- const result: number[] = [];
252
- let sum = 0;
253
- for (const v of arr) {
254
- sum += v;
255
- result.push(sum);
256
- }
257
- return result;
258
- }
259
-
260
- function formatPct(v: number): string {
261
- const sign = v >= 0 ? "+" : "";
262
- return sign + v.toFixed(1) + "%";
263
- }
@@ -1,389 +0,0 @@
1
- import * as Plot from "@observablehq/plot";
2
- import * as d3 from "d3";
3
- import { buildLegend, type LegendItem } from "./LegendUtils.ts";
4
- import type {
5
- GcEvent,
6
- HeapPoint,
7
- PausePoint,
8
- TimeSeriesPoint,
9
- } from "./Types.ts";
10
-
11
- interface SampleData {
12
- benchmark: string;
13
- sample: number;
14
- value: number;
15
- displayValue: number;
16
- isBaseline: boolean;
17
- isWarmup: boolean;
18
- optTier: string | null;
19
- }
20
-
21
- interface PlotContext {
22
- convertedData: SampleData[];
23
- xMin: number;
24
- xMax: number;
25
- yMin: number;
26
- yMax: number;
27
- unitSuffix: string;
28
- formatValue: (d: number) => string;
29
- convertValue: (ms: number) => number;
30
- hasWarmup: boolean;
31
- optTiers: string[];
32
- benchmarks: string[];
33
- }
34
-
35
- const OPT_STATUS_NAMES: Record<number, string> = {
36
- 1: "interpreted",
37
- 129: "sparkplug",
38
- 17: "turbofan",
39
- 33: "maglev",
40
- 49: "turbofan+maglev",
41
- 32769: "optimized",
42
- };
43
- const OPT_TIER_COLORS: Record<string, string> = {
44
- turbofan: "#22c55e",
45
- optimized: "#22c55e",
46
- "turbofan+maglev": "#22c55e",
47
- maglev: "#eab308",
48
- sparkplug: "#f97316",
49
- interpreted: "#dc3545",
50
- };
51
-
52
- /** Create sample time series showing each sample in order */
53
- export function createSampleTimeSeries(
54
- timeSeries: TimeSeriesPoint[],
55
- gcEvents: GcEvent[] = [],
56
- pausePoints: PausePoint[] = [],
57
- heapSeries: HeapPoint[] = [],
58
- ): SVGSVGElement | HTMLElement {
59
- const ctx = buildPlotContext(timeSeries);
60
- const heapData = prepareHeapData(heapSeries, ctx.yMin, ctx.yMax);
61
-
62
- return Plot.plot({
63
- marginTop: 24,
64
- marginLeft: 70,
65
- marginBottom: 60,
66
- marginRight: 110,
67
- width: 550,
68
- height: 300,
69
- style: { fontSize: "14px" },
70
- x: {
71
- label: "Sample",
72
- labelAnchor: "center",
73
- labelOffset: 45,
74
- grid: true,
75
- domain: [ctx.xMin, ctx.xMax],
76
- },
77
- y: {
78
- label: `Time (${ctx.unitSuffix})`,
79
- labelAnchor: "top",
80
- labelArrow: false,
81
- grid: true,
82
- domain: [ctx.yMin, ctx.yMax],
83
- tickFormat: ctx.formatValue,
84
- },
85
- color: { legend: false, scheme: "observable10" },
86
- marks: [
87
- ...heapMarks(heapData, ctx.yMin),
88
- ...(ctx.hasWarmup
89
- ? [
90
- Plot.ruleX([0], {
91
- stroke: "#999",
92
- strokeWidth: 1,
93
- strokeDasharray: "4,4",
94
- }),
95
- ]
96
- : []),
97
- gcMark(gcEvents, ctx.yMin, ctx.convertValue),
98
- ...pauseMarks(pausePoints, ctx.yMin, ctx.yMax),
99
- ...sampleDotMarks(ctx),
100
- Plot.ruleY([ctx.yMin], { stroke: "black", strokeWidth: 1 }),
101
- ...buildLegend(
102
- { xMin: ctx.xMin, xMax: ctx.xMax, yMax: ctx.yMax },
103
- buildLegendItems(
104
- ctx.hasWarmup,
105
- gcEvents.length,
106
- pausePoints.length,
107
- heapData.length > 0,
108
- ctx.optTiers,
109
- ctx.benchmarks,
110
- ),
111
- ),
112
- ],
113
- });
114
- }
115
-
116
- function buildPlotContext(timeSeries: TimeSeriesPoint[]): PlotContext {
117
- const benchmarks = [...new Set(timeSeries.map(d => d.benchmark))];
118
- const sampleData = buildSampleData(timeSeries, benchmarks);
119
- const { unitSuffix, convertValue, formatValue } = getTimeUnit(
120
- sampleData.map(d => d.value),
121
- );
122
- const convertedData = sampleData.map(d => ({
123
- ...d,
124
- displayValue: convertValue(d.value),
125
- }));
126
- const { yMin, yMax } = computeYRange(convertedData.map(d => d.displayValue));
127
- const xMin = d3.min(convertedData, d => d.sample)!;
128
- const xMax = d3.max(convertedData, d => d.sample)!;
129
- const hasWarmup = convertedData.some(d => d.isWarmup);
130
- const tierSet = new Set(
131
- convertedData.filter(d => d.optTier && !d.isWarmup).map(d => d.optTier),
132
- );
133
- const optTiers = [...tierSet].filter((t): t is string => t !== null);
134
- return {
135
- convertedData,
136
- xMin,
137
- xMax,
138
- yMin,
139
- yMax,
140
- unitSuffix,
141
- formatValue,
142
- convertValue,
143
- hasWarmup,
144
- optTiers,
145
- benchmarks,
146
- };
147
- }
148
-
149
- /** Scale heap byte values into the plot's Y coordinate range */
150
- function prepareHeapData(heapSeries: HeapPoint[], yMin: number, yMax: number) {
151
- if (heapSeries.length === 0) return [];
152
- const heapMin = d3.min(heapSeries, d => d.value)!;
153
- const heapRange = d3.max(heapSeries, d => d.value)! - heapMin || 1;
154
- const scale = ((yMax - yMin) * 0.25) / heapRange;
155
- return heapSeries.map(d => ({
156
- sample: d.iteration,
157
- y: yMin + (d.value - heapMin) * scale,
158
- heapMB: d.value / 1024 / 1024,
159
- }));
160
- }
161
-
162
- function heapMarks(
163
- heapData: { sample: number; y: number; heapMB: number }[],
164
- yMin: number,
165
- ): any[] {
166
- if (heapData.length === 0) return [];
167
- return [
168
- Plot.areaY(heapData, {
169
- x: "sample",
170
- y: "y",
171
- y1: yMin,
172
- fill: "#9333ea",
173
- fillOpacity: 0.15,
174
- stroke: "#9333ea",
175
- strokeWidth: 1,
176
- strokeOpacity: 0.4,
177
- }),
178
- Plot.tip(
179
- heapData,
180
- Plot.pointerX({
181
- x: "sample",
182
- y: "y",
183
- title: (d: { heapMB: number }) => `Heap: ${d.heapMB.toFixed(1)} MB`,
184
- }),
185
- ),
186
- ];
187
- }
188
-
189
- function gcMark(
190
- gcEvents: GcEvent[],
191
- yMin: number,
192
- convertValue: (ms: number) => number,
193
- ): any {
194
- const data = gcEvents.map(gc => ({
195
- x1: gc.sampleIndex,
196
- y1: yMin,
197
- x2: gc.sampleIndex,
198
- y2: yMin + convertValue(gc.duration),
199
- duration: gc.duration,
200
- }));
201
- return Plot.link(data, {
202
- x1: "x1",
203
- y1: "y1",
204
- x2: "x2",
205
- y2: "y2",
206
- stroke: "#22c55e",
207
- strokeWidth: 2,
208
- strokeOpacity: 0.8,
209
- title: (d: { duration: number }) => `GC: ${d.duration.toFixed(2)}ms`,
210
- });
211
- }
212
-
213
- function pauseMarks(
214
- pausePoints: PausePoint[],
215
- yMin: number,
216
- yMax: number,
217
- ): any[] {
218
- return pausePoints.map(p =>
219
- Plot.ruleX([p.sampleIndex], {
220
- y1: yMin,
221
- y2: yMax,
222
- stroke: "#888",
223
- strokeWidth: 1,
224
- strokeDasharray: "4,4",
225
- strokeOpacity: 0.7,
226
- title: `Pause: ${p.durationMs}ms`,
227
- }),
228
- );
229
- }
230
-
231
- function sampleDotMarks(ctx: PlotContext): any[] {
232
- const { convertedData, unitSuffix, formatValue } = ctx;
233
- const tipTitle = (d: SampleData) =>
234
- d.optTier
235
- ? `Sample ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix} [${d.optTier}]`
236
- : `Sample ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix}`;
237
- return [
238
- Plot.dot(
239
- convertedData.filter(d => d.isWarmup),
240
- {
241
- x: "sample",
242
- y: "displayValue",
243
- stroke: "#dc3545",
244
- fill: "none",
245
- strokeWidth: 1.5,
246
- r: 3,
247
- opacity: 0.7,
248
- title: (d: SampleData) =>
249
- `Warmup ${d.sample}: ${formatValue(d.displayValue)}${unitSuffix}`,
250
- },
251
- ),
252
- Plot.dot(
253
- convertedData.filter(d => d.isBaseline && !d.isWarmup),
254
- {
255
- x: "sample",
256
- y: "displayValue",
257
- stroke: "#ffa500",
258
- fill: "none",
259
- strokeWidth: 2,
260
- r: 3,
261
- opacity: 0.8,
262
- title: tipTitle,
263
- },
264
- ),
265
- Plot.dot(
266
- convertedData.filter(d => !d.isBaseline && !d.isWarmup),
267
- {
268
- x: "sample",
269
- y: "displayValue",
270
- fill: (d: SampleData) =>
271
- d.optTier ? OPT_TIER_COLORS[d.optTier] || "#4682b4" : "#4682b4",
272
- r: 3,
273
- opacity: 0.8,
274
- title: tipTitle,
275
- },
276
- ),
277
- ];
278
- }
279
-
280
- function buildLegendItems(
281
- hasWarmup: boolean,
282
- gcCount: number,
283
- pauseCount: number,
284
- hasHeap: boolean,
285
- optTiers: string[],
286
- benchmarks: string[],
287
- ): LegendItem[] {
288
- const items: LegendItem[] = [];
289
- if (hasWarmup)
290
- items.push({ color: "#dc3545", label: "warmup", style: "hollow-dot" });
291
- if (gcCount > 0)
292
- items.push({
293
- color: "#22c55e",
294
- label: `gc (${gcCount})`,
295
- style: "vertical-line",
296
- });
297
- if (pauseCount > 0)
298
- items.push({
299
- color: "#888",
300
- label: `pause (${pauseCount})`,
301
- style: "vertical-line",
302
- strokeDash: "4,4",
303
- });
304
- if (hasHeap) items.push({ color: "#9333ea", label: "heap", style: "rect" });
305
- for (const tier of optTiers)
306
- items.push({
307
- color: OPT_TIER_COLORS[tier] || "#4682b4",
308
- label: tier,
309
- style: "filled-dot",
310
- });
311
- if (optTiers.length === 0) {
312
- const sorted = [...benchmarks].sort((a, b) => {
313
- const aBase = a.includes("(baseline)");
314
- const bBase = b.includes("(baseline)");
315
- return aBase === bBase ? 0 : aBase ? 1 : -1;
316
- });
317
- for (const bm of sorted) {
318
- const isBase = bm.includes("(baseline)");
319
- items.push({
320
- color: isBase ? "#ffa500" : "#4682b4",
321
- label: bm,
322
- style: isBase ? "hollow-dot" : "filled-dot",
323
- });
324
- }
325
- }
326
- return items;
327
- }
328
-
329
- function buildSampleData(
330
- timeSeries: TimeSeriesPoint[],
331
- benchmarks: string[],
332
- ): Omit<SampleData, "displayValue">[] {
333
- const result: Omit<SampleData, "displayValue">[] = [];
334
- for (const benchmark of benchmarks) {
335
- const isBaseline = benchmark.includes("(baseline)");
336
- for (const d of timeSeries.filter(t => t.benchmark === benchmark)) {
337
- const optTier =
338
- d.optStatus !== undefined
339
- ? OPT_STATUS_NAMES[d.optStatus] || "unknown"
340
- : null;
341
- result.push({
342
- benchmark,
343
- sample: d.iteration,
344
- value: d.value,
345
- isBaseline,
346
- isWarmup: d.isWarmup || false,
347
- optTier,
348
- });
349
- }
350
- }
351
- return result;
352
- }
353
-
354
- /** Pick display unit (ns/us/ms) based on average value magnitude */
355
- function getTimeUnit(values: number[]) {
356
- const avg = d3.mean(values)!;
357
- const fmt0 = (d: number) => d3.format(",.0f")(d);
358
- const fmt1 = (d: number) => d3.format(",.1f")(d);
359
- if (avg < 0.001)
360
- return {
361
- unitSuffix: "ns",
362
- convertValue: (ms: number) => ms * 1e6,
363
- formatValue: fmt0,
364
- };
365
- if (avg < 1)
366
- return {
367
- unitSuffix: "μs",
368
- convertValue: (ms: number) => ms * 1e3,
369
- formatValue: fmt1,
370
- };
371
- return {
372
- unitSuffix: "ms",
373
- convertValue: (ms: number) => ms,
374
- formatValue: fmt1,
375
- };
376
- }
377
-
378
- /** Compute Y axis range with padding, snapping yMin to a round number */
379
- function computeYRange(values: number[]) {
380
- const dataMin = d3.min(values)!;
381
- const dataMax = d3.max(values)!;
382
- const dataRange = dataMax - dataMin;
383
- const padding = dataRange * 0.15;
384
- let yMin = dataMin - padding;
385
- const magnitude = 10 ** Math.floor(Math.log10(Math.abs(yMin)));
386
- yMin = Math.floor(yMin / magnitude) * magnitude;
387
- if (dataMin > 0 && yMin < 0) yMin = 0;
388
- return { yMin, yMax: dataMax + dataRange * 0.05 };
389
- }