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,287 +0,0 @@
1
- import type { ComparisonCI, HistogramBin } from "./Types.ts";
2
-
3
- export interface DistributionPlotOptions {
4
- width?: number;
5
- height?: number;
6
- title?: string;
7
- smooth?: boolean;
8
- direction?: "faster" | "slower" | "uncertain";
9
- }
10
-
11
- const defaultOpts: Required<DistributionPlotOptions> = {
12
- width: 260,
13
- height: 85,
14
- title: "p50 Δ%",
15
- smooth: false,
16
- direction: "uncertain",
17
- };
18
-
19
- const colors = {
20
- faster: { fill: "#dcfce7", stroke: "#22c55e" },
21
- slower: { fill: "#fee2e2", stroke: "#ef4444" },
22
- uncertain: { fill: "#dbeafe", stroke: "#3b82f6" },
23
- };
24
-
25
- type Scales = { x: (v: number) => number; y: (v: number) => number };
26
- type Layout = {
27
- width: number;
28
- height: number;
29
- margin: typeof defaultMargin;
30
- plot: { w: number; h: number };
31
- };
32
- const defaultMargin = { top: 22, right: 12, bottom: 22, left: 12 };
33
-
34
- /** Create a small distribution plot showing histogram with CI shading */
35
- export function createDistributionPlot(
36
- histogram: HistogramBin[],
37
- ci: [number, number],
38
- pointEstimate: number,
39
- options: DistributionPlotOptions = {},
40
- ): SVGSVGElement {
41
- const opts = { ...defaultOpts, ...options };
42
- const layout = buildLayout(opts.width, opts.height);
43
- const svg = createSvg(layout.width, layout.height);
44
- if (!histogram?.length) return svg;
45
-
46
- const { fill, stroke } = colors[opts.direction];
47
- const scales = buildScales(histogram, ci, layout);
48
-
49
- drawTitle(svg, opts.title, layout.margin.left);
50
- drawCIRegion(svg, ci, scales, layout, fill);
51
- opts.smooth
52
- ? drawSmoothedDist(svg, histogram, scales, stroke)
53
- : drawHistogramBars(svg, histogram, scales, layout, stroke);
54
- drawZeroLine(svg, scales, layout);
55
- drawPointEstimate(svg, pointEstimate, scales, layout, stroke);
56
- drawCILabels(svg, ci, scales, layout.height);
57
- return svg;
58
- }
59
-
60
- /** Convenience wrapper for ComparisonCI data */
61
- export function createCIPlot(
62
- ci: ComparisonCI,
63
- options: Partial<DistributionPlotOptions> = {},
64
- ): SVGSVGElement {
65
- if (!ci.histogram) return createSvg(0, 0);
66
- return createDistributionPlot(ci.histogram, ci.ci, ci.percent, {
67
- direction: ci.direction,
68
- ...options,
69
- });
70
- }
71
-
72
- function buildLayout(width: number, height: number): Layout {
73
- const margin = defaultMargin;
74
- const w = width - margin.left - margin.right;
75
- const h = height - margin.top - margin.bottom;
76
- return { width, height, margin, plot: { w, h } };
77
- }
78
-
79
- /** Compute x/y scale functions mapping data values to SVG coordinates */
80
- function buildScales(
81
- histogram: HistogramBin[],
82
- ci: [number, number],
83
- layout: Layout,
84
- ): Scales {
85
- const { margin, plot } = layout;
86
- const xMin = Math.min(...histogram.map(b => b.x), ci[0], 0);
87
- const xMax = Math.max(...histogram.map(b => b.x), ci[1], 0);
88
- const yMax = Math.max(...histogram.map(b => b.count));
89
- return {
90
- x: (v: number) => margin.left + ((v - xMin) / (xMax - xMin)) * plot.w,
91
- y: (v: number) => margin.top + plot.h - (v / yMax) * plot.h,
92
- };
93
- }
94
-
95
- function drawTitle(svg: SVGSVGElement, title: string, x: number): void {
96
- svg.appendChild(text(x, 14, title, "start", "13", "#333", "600"));
97
- }
98
-
99
- function drawCIRegion(
100
- svg: SVGSVGElement,
101
- ci: [number, number],
102
- scales: Scales,
103
- layout: Layout,
104
- fill: string,
105
- ) {
106
- const x = scales.x(ci[0]);
107
- const w = scales.x(ci[1]) - x;
108
- svg.appendChild(
109
- rect(x, layout.margin.top, w, layout.plot.h, { fill, opacity: "0.5" }),
110
- );
111
- }
112
-
113
- function drawSmoothedDist(
114
- svg: SVGSVGElement,
115
- histogram: HistogramBin[],
116
- scales: Scales,
117
- stroke: string,
118
- ): void {
119
- const sorted = [...histogram].sort((a, b) => a.x - b.x);
120
- const smoothed = gaussianSmooth(sorted, 2);
121
- const pts = smoothed.map(b => `${scales.x(b.x)},${scales.y(b.count)}`);
122
- const baseline = scales.y(0);
123
- svg.appendChild(
124
- path(
125
- `M${scales.x(smoothed[0].x)},${baseline}L${pts.join("L")}L${scales.x(smoothed.at(-1)!.x)},${baseline}Z`,
126
- { fill: stroke, opacity: "0.3" },
127
- ),
128
- );
129
- svg.appendChild(
130
- path(`M${pts.join("L")}`, { stroke, fill: "none", strokeWidth: "1.5" }),
131
- );
132
- }
133
-
134
- function drawHistogramBars(
135
- svg: SVGSVGElement,
136
- histogram: HistogramBin[],
137
- scales: Scales,
138
- layout: Layout,
139
- stroke: string,
140
- ): void {
141
- const sorted = [...histogram].sort((a, b) => a.x - b.x);
142
- const binW = sorted.length > 1 ? sorted[1].x - sorted[0].x : 1;
143
- const yMax = Math.max(...histogram.map(b => b.count));
144
- const xRange = scales.x(sorted.at(-1)!.x) - scales.x(sorted[0].x) + binW;
145
- for (const bin of sorted) {
146
- const barW = (binW / xRange) * layout.plot.w * 0.9;
147
- const barH = (bin.count / yMax) * layout.plot.h;
148
- svg.appendChild(
149
- rect(
150
- scales.x(bin.x) - barW / 2,
151
- layout.margin.top + layout.plot.h - barH,
152
- barW,
153
- barH,
154
- { fill: stroke, opacity: "0.6" },
155
- ),
156
- );
157
- }
158
- }
159
-
160
- function drawZeroLine(svg: SVGSVGElement, scales: Scales, layout: Layout) {
161
- const zeroX = scales.x(0);
162
- if (zeroX < layout.margin.left || zeroX > layout.width - layout.margin.right)
163
- return;
164
- const top = layout.margin.top;
165
- const attrs = { stroke: "#666", strokeWidth: "1", strokeDasharray: "3,2" };
166
- svg.appendChild(line(zeroX, top, zeroX, top + layout.plot.h, attrs));
167
- }
168
-
169
- function drawPointEstimate(
170
- svg: SVGSVGElement,
171
- pt: number,
172
- scales: Scales,
173
- layout: Layout,
174
- stroke: string,
175
- ) {
176
- const x = scales.x(pt);
177
- const top = layout.margin.top;
178
- svg.appendChild(
179
- line(x, top, x, top + layout.plot.h, { stroke, strokeWidth: "2" }),
180
- );
181
- }
182
-
183
- function drawCILabels(
184
- svg: SVGSVGElement,
185
- ci: [number, number],
186
- scales: Scales,
187
- height: number,
188
- ): void {
189
- svg.appendChild(
190
- text(scales.x(ci[0]), height - 4, formatPct(ci[0]), "middle", "12"),
191
- );
192
- svg.appendChild(
193
- text(scales.x(ci[1]), height - 4, formatPct(ci[1]), "middle", "12"),
194
- );
195
- }
196
-
197
- /** Apply gaussian kernel smoothing to histogram bins */
198
- function gaussianSmooth(bins: HistogramBin[], sigma: number): HistogramBin[] {
199
- return bins.map((bin, i) => {
200
- let sum = 0;
201
- let wt = 0;
202
- for (let j = 0; j < bins.length; j++) {
203
- const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
204
- sum += bins[j].count * w;
205
- wt += w;
206
- }
207
- return { x: bin.x, count: sum / wt };
208
- });
209
- }
210
-
211
- const formatPct = (v: number) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
212
-
213
- function createSvg(w: number, h: number): SVGSVGElement {
214
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
215
- svg.setAttribute("width", String(w));
216
- svg.setAttribute("height", String(h));
217
- if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
218
- return svg;
219
- }
220
-
221
- function rect(
222
- x: number,
223
- y: number,
224
- w: number,
225
- h: number,
226
- attrs: Record<string, string>,
227
- ): SVGRectElement {
228
- const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
229
- el.setAttribute("x", String(x));
230
- el.setAttribute("y", String(y));
231
- el.setAttribute("width", String(w));
232
- el.setAttribute("height", String(h));
233
- setAttrs(el, attrs);
234
- return el;
235
- }
236
-
237
- function path(d: string, attrs: Record<string, string>): SVGPathElement {
238
- const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
239
- el.setAttribute("d", d);
240
- setAttrs(el, attrs);
241
- return el;
242
- }
243
-
244
- function line(
245
- x1: number,
246
- y1: number,
247
- x2: number,
248
- y2: number,
249
- attrs: Record<string, string>,
250
- ): SVGLineElement {
251
- const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
252
- el.setAttribute("x1", String(x1));
253
- el.setAttribute("y1", String(y1));
254
- el.setAttribute("x2", String(x2));
255
- el.setAttribute("y2", String(y2));
256
- setAttrs(el, attrs);
257
- return el;
258
- }
259
-
260
- function text(
261
- x: number,
262
- y: number,
263
- content: string,
264
- anchor = "start",
265
- size = "9",
266
- fill = "#666",
267
- weight = "400",
268
- ): SVGTextElement {
269
- const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
270
- el.setAttribute("x", String(x));
271
- el.setAttribute("y", String(y));
272
- el.setAttribute("text-anchor", anchor);
273
- el.setAttribute("font-size", size);
274
- el.setAttribute("font-weight", weight);
275
- el.setAttribute("fill", fill);
276
- el.textContent = content;
277
- return el;
278
- }
279
-
280
- /** Set SVG attributes, converting camelCase keys to kebab-case */
281
- function setAttrs(el: SVGElement, attrs: Record<string, string>): void {
282
- for (const [k, v] of Object.entries(attrs))
283
- el.setAttribute(
284
- k.replace(/[A-Z]/g, c => "-" + c.toLowerCase()),
285
- v,
286
- );
287
- }
@@ -1,163 +0,0 @@
1
- import * as Plot from "@observablehq/plot";
2
-
3
- export interface LegendBounds {
4
- xMin: number;
5
- xMax: number;
6
- yMax: number;
7
- }
8
-
9
- export interface LegendItem {
10
- color: string;
11
- label: string;
12
- style:
13
- | "filled-dot"
14
- | "hollow-dot"
15
- | "vertical-bar"
16
- | "vertical-line"
17
- | "rect";
18
- strokeDash?: string;
19
- }
20
-
21
- interface LegendPos {
22
- legendX: number;
23
- y: number;
24
- textX: number;
25
- xRange: number;
26
- yMax: number;
27
- }
28
-
29
- /** Draw a semi-transparent white background behind the legend area */
30
- function legendBackground(bounds: LegendBounds): any {
31
- const xRange = bounds.xMax - bounds.xMin;
32
- const data = [
33
- {
34
- x1: bounds.xMin + xRange * 0.65,
35
- x2: bounds.xMin + xRange * 1.05,
36
- y1: bounds.yMax * 0.65,
37
- y2: bounds.yMax * 1.05,
38
- },
39
- ];
40
- const opts = {
41
- x1: "x1",
42
- x2: "x2",
43
- y1: "y1",
44
- y2: "y2",
45
- fill: "white",
46
- fillOpacity: 0.9,
47
- stroke: "#ddd",
48
- strokeWidth: 1,
49
- };
50
- return Plot.rect(data, opts);
51
- }
52
-
53
- function dotMark(x: number, y: number, color: string, filled: boolean): any {
54
- return Plot.dot(
55
- [{ x, y }],
56
- filled
57
- ? { x: "x", y: "y", fill: color, r: 4 }
58
- : { x: "x", y: "y", stroke: color, fill: "none", strokeWidth: 1.5, r: 4 },
59
- );
60
- }
61
-
62
- function verticalBarMark(pos: LegendPos, color: string): any {
63
- const { legendX, y, xRange, yMax } = pos;
64
- const w = xRange * 0.012;
65
- const h = yMax * 0.05;
66
- const data = [
67
- { x1: legendX - w / 2, x2: legendX + w / 2, y1: y - h / 2, y2: y + h / 2 },
68
- ];
69
- return Plot.rect(data, {
70
- x1: "x1",
71
- x2: "x2",
72
- y1: "y1",
73
- y2: "y2",
74
- fill: color,
75
- fillOpacity: 0.6,
76
- });
77
- }
78
-
79
- function verticalLineMark(
80
- pos: LegendPos,
81
- color: string,
82
- strokeDash?: string,
83
- ): any {
84
- const { legendX, y, yMax } = pos;
85
- return Plot.ruleX([legendX], {
86
- y1: y - yMax * 0.025,
87
- y2: y + yMax * 0.025,
88
- stroke: color,
89
- strokeWidth: 2,
90
- strokeDasharray: strokeDash,
91
- });
92
- }
93
-
94
- function rectMark(pos: LegendPos, color: string): any {
95
- const { legendX, y, xRange, yMax } = pos;
96
- const data = [
97
- {
98
- x1: legendX - xRange * 0.01,
99
- x2: legendX + xRange * 0.03,
100
- y1: y - yMax * 0.02,
101
- y2: y + yMax * 0.02,
102
- },
103
- ];
104
- const opts = {
105
- x1: "x1",
106
- x2: "x2",
107
- y1: "y1",
108
- y2: "y2",
109
- fill: color,
110
- fillOpacity: 0.3,
111
- stroke: color,
112
- strokeWidth: 1,
113
- };
114
- return Plot.rect(data, opts);
115
- }
116
-
117
- function symbolMark(pos: LegendPos, item: LegendItem): any {
118
- switch (item.style) {
119
- case "filled-dot":
120
- return dotMark(pos.legendX, pos.y, item.color, true);
121
- case "hollow-dot":
122
- return dotMark(pos.legendX, pos.y, item.color, false);
123
- case "vertical-bar":
124
- return verticalBarMark(pos, item.color);
125
- case "vertical-line":
126
- return verticalLineMark(pos, item.color, item.strokeDash);
127
- case "rect":
128
- return rectMark(pos, item.color);
129
- }
130
- }
131
-
132
- function textMark(pos: LegendPos, label: string): any {
133
- const data = [{ x: pos.textX, y: pos.y, text: label }];
134
- return Plot.text(data, {
135
- x: "x",
136
- y: "y",
137
- text: "text",
138
- fontSize: 11,
139
- textAnchor: "start",
140
- fill: "#333",
141
- });
142
- }
143
-
144
- /** Build complete legend marks array */
145
- export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
146
- const xRange = bounds.xMax - bounds.xMin;
147
- const legendX = bounds.xMin + xRange * 0.68;
148
- const textX = legendX + xRange * 0.04;
149
- const getY = (i: number) => bounds.yMax * 0.98 - i * (bounds.yMax * 0.08);
150
-
151
- const marks: any[] = [legendBackground(bounds)];
152
- for (let i = 0; i < items.length; i++) {
153
- const pos: LegendPos = {
154
- legendX,
155
- y: getY(i),
156
- textX,
157
- xRange,
158
- yMax: bounds.yMax,
159
- };
160
- marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
161
- }
162
- return marks;
163
- }
@@ -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
- /** Render all plots for the benchmark report */
15
- export function renderPlots(data: ReportData): void {
16
- const gcEnabled = data.metadata.gcTrackingEnabled ?? false;
17
- data.groups.forEach((group, groupIndex) => {
18
- try {
19
- renderGroup(group, groupIndex, gcEnabled);
20
- } catch (error) {
21
- console.error("Error rendering plots for group", groupIndex, error);
22
- showError(
23
- groupIndex,
24
- `Error rendering visualizations: ${(error as Error).message}`,
25
- );
26
- }
27
- });
28
- }
29
-
30
- function renderGroup(
31
- group: ReportData["groups"][0],
32
- groupIndex: number,
33
- gcEnabled: boolean,
34
- ): void {
35
- const benchmarks = prepareBenchmarks(group);
36
- if (benchmarks.length === 0 || !benchmarks[0].samples?.length) {
37
- showError(groupIndex, "No sample data available for visualization");
38
- return;
39
- }
40
- const flattened = flattenSamples(benchmarks);
41
- const benchmarkNames = benchmarks.map(b => b.name);
42
-
43
- const currentBenchmark = benchmarks.find(b => !b.isBaseline);
44
- if (currentBenchmark?.comparisonCI?.histogram) {
45
- renderToContainer(`#ci-plot-${groupIndex}`, true, () =>
46
- createCIPlot(currentBenchmark.comparisonCI!),
47
- );
48
- }
49
-
50
- renderToContainer(
51
- `#histogram-${groupIndex}`,
52
- flattened.allSamples.length > 0,
53
- () => createHistogramKde(flattened.allSamples, benchmarkNames),
54
- );
55
- const { timeSeries, allGcEvents, allPausePoints, heapSeries } = flattened;
56
- renderToContainer(
57
- `#sample-timeseries-${groupIndex}`,
58
- timeSeries.length > 0,
59
- () =>
60
- createSampleTimeSeries(
61
- timeSeries,
62
- allGcEvents,
63
- allPausePoints,
64
- heapSeries,
65
- ),
66
- );
67
-
68
- const statsContainer = document.querySelector(`#stats-${groupIndex}`);
69
- if (statsContainer)
70
- statsContainer.innerHTML = benchmarks
71
- .map(b => generateStatsHtml(b, gcEnabled))
72
- .join("");
73
- }
74
-
75
- /** Clear a container element and append a freshly created plot */
76
- function renderToContainer(
77
- selector: string,
78
- condition: boolean,
79
- create: () => SVGSVGElement | HTMLElement,
80
- ): void {
81
- const container = document.querySelector(selector);
82
- if (!container || !condition) return;
83
- container.innerHTML = "";
84
- container.appendChild(create());
85
- }
86
-
87
- interface PreparedBenchmark extends BenchmarkEntry {
88
- name: string;
89
- }
90
-
91
- /** Combine baseline and benchmarks into a single list with display names */
92
- function prepareBenchmarks(
93
- group: ReportData["groups"][0],
94
- ): PreparedBenchmark[] {
95
- const benchmarks: PreparedBenchmark[] = [];
96
- if (group.baseline) {
97
- const name = group.baseline.name + " (baseline)";
98
- benchmarks.push({ ...group.baseline, name, isBaseline: true });
99
- }
100
- for (const b of group.benchmarks)
101
- benchmarks.push({ ...b, isBaseline: false });
102
- return benchmarks;
103
- }
104
-
105
- interface FlattenedData {
106
- allSamples: Sample[];
107
- timeSeries: TimeSeriesPoint[];
108
- heapSeries: HeapPoint[];
109
- allGcEvents: GcEvent[];
110
- allPausePoints: PausePoint[];
111
- }
112
-
113
- function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
114
- const result: FlattenedData = {
115
- allSamples: [],
116
- timeSeries: [],
117
- heapSeries: [],
118
- allGcEvents: [],
119
- allPausePoints: [],
120
- };
121
- for (const b of benchmarks)
122
- if (b.samples?.length) flattenBenchmark(b, result);
123
- return result;
124
- }
125
-
126
- /** Extract time series, heap, GC, and pause data from one benchmark */
127
- function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
128
- const warmupCount = b.warmupSamples?.length || 0;
129
- b.warmupSamples?.forEach((value, i) => {
130
- out.timeSeries.push({
131
- benchmark: b.name,
132
- iteration: i - warmupCount,
133
- value,
134
- isWarmup: true,
135
- });
136
- });
137
-
138
- const sampleEndTimes = cumulativeSum(b.samples);
139
- b.samples.forEach((value, i) => {
140
- out.allSamples.push({ benchmark: b.name, value, iteration: i });
141
- out.timeSeries.push({
142
- benchmark: b.name,
143
- iteration: i,
144
- value,
145
- isWarmup: false,
146
- optStatus: b.optSamples?.[i],
147
- });
148
- if (b.heapSamples?.[i] !== undefined) {
149
- out.heapSeries.push({
150
- benchmark: b.name,
151
- iteration: i,
152
- value: b.heapSamples[i],
153
- });
154
- }
155
- });
156
-
157
- b.gcEvents?.forEach(gc => {
158
- const idx = sampleEndTimes.findIndex(t => t >= gc.offset);
159
- out.allGcEvents.push({
160
- benchmark: b.name,
161
- sampleIndex: idx >= 0 ? idx : b.samples.length - 1,
162
- duration: gc.duration,
163
- });
164
- });
165
- b.pausePoints?.forEach(p => {
166
- out.allPausePoints.push({
167
- benchmark: b.name,
168
- sampleIndex: p.sampleIndex,
169
- durationMs: p.durationMs,
170
- });
171
- });
172
- }
173
-
174
- function cumulativeSum(arr: number[]): number[] {
175
- const result: number[] = [];
176
- let sum = 0;
177
- for (const v of arr) {
178
- sum += v;
179
- result.push(sum);
180
- }
181
- return result;
182
- }
183
-
184
- function showError(groupIndex: number, message: string): void {
185
- const container = document.querySelector(`#group-${groupIndex}`);
186
- if (container) container.innerHTML = `<div class="error">${message}</div>`;
187
- }
188
-
189
- function formatPct(v: number): string {
190
- const sign = v >= 0 ? "+" : "";
191
- return sign + v.toFixed(1) + "%";
192
- }
193
-
194
- function generateCIHtml(ci: BenchmarkEntry["comparisonCI"]): string {
195
- if (!ci) return "";
196
- const text = `${formatPct(ci.percent)} [${formatPct(ci.ci[0])}, ${formatPct(ci.ci[1])}]`;
197
- return `
198
- <div class="stat-item">
199
- <div class="stat-label">vs Baseline</div>
200
- <div class="stat-value ci-${ci.direction}">${text}</div>
201
- </div>
202
- `;
203
- }
204
-
205
- function generateStatsHtml(b: PreparedBenchmark, gcEnabled: boolean): string {
206
- const ciHtml = generateCIHtml(b.comparisonCI);
207
-
208
- if (b.sectionStats?.length) {
209
- const stats = gcEnabled
210
- ? b.sectionStats
211
- : b.sectionStats.filter(s => s.groupTitle !== "gc");
212
- const statsHtml = stats
213
- .map(
214
- stat => `
215
- <div class="stat-item">
216
- <div class="stat-label">${stat.groupTitle ? stat.groupTitle + " " : ""}${stat.label}</div>
217
- <div class="stat-value">${stat.value}</div>
218
- </div>
219
- `,
220
- )
221
- .join("");
222
- return `
223
- <div class="summary-stats">
224
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
225
- <div class="stats-grid">${ciHtml}${statsHtml}</div>
226
- </div>
227
- `;
228
- }
229
-
230
- // Fallback to hardcoded stats
231
- return `
232
- <div class="summary-stats">
233
- <h3 style="margin-bottom: 10px; color: #333;">${b.name}</h3>
234
- <div class="stats-grid">
235
- ${ciHtml}
236
- <div class="stat-item">
237
- <div class="stat-label">Min</div>
238
- <div class="stat-value">${b.stats.min.toFixed(3)}ms</div>
239
- </div>
240
- <div class="stat-item">
241
- <div class="stat-label">Median</div>
242
- <div class="stat-value">${b.stats.p50.toFixed(3)}ms</div>
243
- </div>
244
- <div class="stat-item">
245
- <div class="stat-label">Mean</div>
246
- <div class="stat-value">${b.stats.avg.toFixed(3)}ms</div>
247
- </div>
248
- <div class="stat-item">
249
- <div class="stat-label">Max</div>
250
- <div class="stat-value">${b.stats.max.toFixed(3)}ms</div>
251
- </div>
252
- <div class="stat-item">
253
- <div class="stat-label">P75</div>
254
- <div class="stat-value">${b.stats.p75.toFixed(3)}ms</div>
255
- </div>
256
- <div class="stat-item">
257
- <div class="stat-label">P99</div>
258
- <div class="stat-value">${b.stats.p99.toFixed(3)}ms</div>
259
- </div>
260
- </div>
261
- </div>
262
- `;
263
- }