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,319 @@
1
+ import * as Plot from "@observablehq/plot";
2
+ import * as d3 from "d3";
3
+ import type { LegendItem } from "./LegendUtils.ts";
4
+ import type { FlatGcEvent, FlatPausePoint } from "./PlotTypes.ts";
5
+
6
+ /** Internal sample representation with display values and metadata */
7
+ export interface SampleData {
8
+ benchmark: string;
9
+ sample: number;
10
+ value: number;
11
+ displayValue: number;
12
+ isBaseline: boolean;
13
+ isWarmup: boolean;
14
+ isRejected: boolean;
15
+ optTier: string | null;
16
+ }
17
+
18
+ /** Computed scales, ranges, and metadata for rendering the time series plot */
19
+ export interface PlotContext {
20
+ convertedData: SampleData[];
21
+ xMin: number;
22
+ xMax: number;
23
+ yMin: number;
24
+ yMax: number;
25
+ unitSuffix: string;
26
+ formatValue: (d: number) => string;
27
+ convertValue: (ms: number) => number;
28
+ hasWarmup: boolean;
29
+ hasRejected: boolean;
30
+ baselineNames: Set<string>;
31
+ optTiers: string[];
32
+ benchmarks: string[];
33
+ }
34
+
35
+ /** Parameters for mapping heap byte values to the time series Y axis */
36
+ export interface HeapScale {
37
+ heapMinBytes: number;
38
+ heapRangeBytes: number;
39
+ scale: number;
40
+ yMin: number;
41
+ }
42
+
43
+ interface LegendParams {
44
+ hasWarmup: boolean;
45
+ gcCount: number;
46
+ pauseCount: number;
47
+ hasHeap: boolean;
48
+ hasBaselineHeap: boolean;
49
+ hasRejected: boolean;
50
+ optTiers: string[];
51
+ benchmarks: string[];
52
+ baselineNames: Set<string>;
53
+ }
54
+
55
+ type Downsample = <T>(
56
+ data: T[],
57
+ n: number,
58
+ getX: (d: T) => number,
59
+ getY: (d: T) => number,
60
+ ) => T[];
61
+
62
+ const optTierColors: Record<string, string> = {
63
+ turbofan: "#22c55e",
64
+ optimized: "#22c55e",
65
+ "turbofan+maglev": "#22c55e",
66
+ maglev: "#eab308",
67
+ sparkplug: "#f97316",
68
+ interpreted: "#dc3545",
69
+ };
70
+
71
+ const maxDots = 1000;
72
+
73
+ /** Build legend items based on which data series are present in the plot */
74
+ export function buildLegendItems(p: LegendParams): LegendItem[] {
75
+ const { hasWarmup, gcCount, pauseCount, hasHeap, hasBaselineHeap } = p;
76
+ const { hasRejected, optTiers, benchmarks, baselineNames } = p;
77
+ const items: LegendItem[] = [];
78
+ if (hasWarmup)
79
+ items.push({ color: "#dc3545", label: "warmup", style: "hollow-dot" });
80
+ if (gcCount > 0)
81
+ items.push({
82
+ color: "#22c55e",
83
+ label: `gc (${gcCount})`,
84
+ style: "vertical-line",
85
+ });
86
+ if (pauseCount > 0)
87
+ items.push({
88
+ color: "#888",
89
+ label: `pause (${pauseCount})`,
90
+ style: "vertical-line",
91
+ strokeDash: "4,4",
92
+ });
93
+ if (hasHeap) items.push({ color: "#93c5fd", label: "heap", style: "rect" });
94
+ if (hasBaselineHeap)
95
+ items.push({ color: "#fcd34d", label: "heap (baseline)", style: "rect" });
96
+ items.push(...seriesLegendItems(optTiers, benchmarks, baselineNames));
97
+ if (hasRejected)
98
+ items.push({ color: "#999", label: "rejected", style: "hollow-dot" });
99
+ return items;
100
+ }
101
+
102
+ /** Area fill marks for heap usage overlay on the time series chart */
103
+ export function heapMarks(
104
+ heapData: { sample: number; y: number }[],
105
+ yMin: number,
106
+ color: string,
107
+ ): any[] {
108
+ if (heapData.length === 0) return [];
109
+ return [
110
+ Plot.areaY(heapData, {
111
+ x: "sample",
112
+ y: "y",
113
+ y1: yMin,
114
+ fill: color,
115
+ fillOpacity: 0.15,
116
+ stroke: color,
117
+ strokeWidth: 1,
118
+ strokeOpacity: 0.4,
119
+ }),
120
+ ];
121
+ }
122
+
123
+ /** Right-side Y axis for heap MB (overlaid on the time Y axis) */
124
+ export function heapAxisMarks(
125
+ hs: HeapScale | undefined,
126
+ xMax: number,
127
+ xMin: number,
128
+ ): any[] {
129
+ if (!hs) return [];
130
+ const xRange = xMax - xMin;
131
+ const tickX = xMax + xRange * 0.01;
132
+ const labelX = xMax + xRange * 0.06;
133
+ const minMB = hs.heapMinBytes / 1024 / 1024;
134
+ const maxMB = (hs.heapMinBytes + hs.heapRangeBytes) / 1024 / 1024;
135
+ const ticks = d3.ticks(minMB, maxMB, 3);
136
+ const fmtMB = (mb: number) => {
137
+ if (mb >= 100) return mb.toFixed(0);
138
+ if (mb >= 10) return mb.toFixed(1);
139
+ return mb.toFixed(2);
140
+ };
141
+ const tickData = ticks.map(mb => ({
142
+ x: tickX,
143
+ y: hs.yMin + (mb * 1024 * 1024 - hs.heapMinBytes) * hs.scale,
144
+ label: fmtMB(mb),
145
+ }));
146
+ const textOpts = {
147
+ x: "x",
148
+ y: "y",
149
+ fontSize: 10,
150
+ textAnchor: "start" as const,
151
+ fill: "#333",
152
+ clip: false,
153
+ };
154
+ const mbY = hs.yMin + hs.heapRangeBytes * hs.scale * 0.5;
155
+ const mbData = [{ x: labelX, y: mbY, text: "MB" }];
156
+ return [
157
+ Plot.text(tickData, { ...textOpts, text: "label" }),
158
+ Plot.text(mbData, { ...textOpts, text: "text" }),
159
+ ];
160
+ }
161
+
162
+ /** Vertical line marks for GC events rising from the X axis */
163
+ export function gcMark(
164
+ gcEvents: FlatGcEvent[],
165
+ yMin: number,
166
+ convertValue: (ms: number) => number,
167
+ ): any {
168
+ const data = gcEvents.map(gc => ({
169
+ x1: gc.sampleIndex,
170
+ y1: yMin,
171
+ x2: gc.sampleIndex,
172
+ y2: yMin + convertValue(gc.duration),
173
+ duration: gc.duration,
174
+ }));
175
+ return Plot.link(data, {
176
+ x1: "x1",
177
+ y1: "y1",
178
+ x2: "x2",
179
+ y2: "y2",
180
+ stroke: "#22c55e",
181
+ strokeWidth: 2,
182
+ strokeOpacity: 0.8,
183
+ title: (d: { duration: number }) => `GC: ${d.duration.toFixed(2)}ms`,
184
+ });
185
+ }
186
+
187
+ /** Dashed vertical rules marking pause points across the full Y range */
188
+ export function pauseMarks(
189
+ pausePoints: FlatPausePoint[],
190
+ yMin: number,
191
+ yMax: number,
192
+ ): any[] {
193
+ return pausePoints.map(p =>
194
+ Plot.ruleX([p.sampleIndex], {
195
+ y1: yMin,
196
+ y2: yMax,
197
+ stroke: "#888",
198
+ strokeWidth: 1,
199
+ strokeDasharray: "4,4",
200
+ strokeOpacity: 0.7,
201
+ title: `Pause: ${p.durationMs}ms`,
202
+ }),
203
+ );
204
+ }
205
+
206
+ /** Dot marks for all sample categories: warmup, baseline, measured, rejected */
207
+ export function sampleDotMarks(
208
+ ctx: PlotContext,
209
+ showRejected: boolean,
210
+ lttb: Downsample,
211
+ ): any[] {
212
+ const { unitSuffix, formatValue } = ctx;
213
+ const fmtVal = (d: SampleData) =>
214
+ `${formatValue(d.displayValue)}${unitSuffix}`;
215
+ const tipTitle = (d: SampleData) =>
216
+ d.optTier
217
+ ? `Iteration ${d.sample}: ${fmtVal(d)} [${d.optTier}]`
218
+ : `Iteration ${d.sample}: ${fmtVal(d)}`;
219
+ const xy = { x: "sample" as const, y: "displayValue" as const, r: 3 };
220
+ const { warmup, baseline, measured, rejected } = partitionSamples(
221
+ ctx.convertedData,
222
+ showRejected,
223
+ lttb,
224
+ );
225
+ return [
226
+ Plot.dot(warmup, {
227
+ ...xy,
228
+ stroke: "#dc3545",
229
+ fill: "none",
230
+ strokeWidth: 1.5,
231
+ opacity: 0.7,
232
+ title: (d: SampleData) => `Warmup ${d.sample}: ${fmtVal(d)}`,
233
+ }),
234
+ Plot.dot(baseline, {
235
+ ...xy,
236
+ stroke: "#ffa500",
237
+ fill: "none",
238
+ strokeWidth: 2,
239
+ opacity: 0.8,
240
+ title: tipTitle,
241
+ }),
242
+ Plot.dot(measured, {
243
+ ...xy,
244
+ opacity: 0.8,
245
+ title: tipTitle,
246
+ fill: (d: SampleData) =>
247
+ (d.optTier && optTierColors[d.optTier]) || "#4682b4",
248
+ }),
249
+ ...rejectedDotMark(rejected, xy, tipTitle),
250
+ ];
251
+ }
252
+
253
+ /** Legend items for opt tiers (when present) or benchmark names */
254
+ function seriesLegendItems(
255
+ optTiers: string[],
256
+ benchmarks: string[],
257
+ baselineNames: Set<string>,
258
+ ): LegendItem[] {
259
+ if (optTiers.length > 0)
260
+ return optTiers.map(tier => ({
261
+ color: optTierColors[tier] || "#4682b4",
262
+ label: tier,
263
+ style: "filled-dot" as const,
264
+ }));
265
+ // sort baselines last so current benchmarks appear first in legend
266
+ const sorted = [...benchmarks].sort(
267
+ (a, b) => Number(baselineNames.has(a)) - Number(baselineNames.has(b)),
268
+ );
269
+ return sorted.map(bm => {
270
+ const isBase = baselineNames.has(bm);
271
+ return {
272
+ color: isBase ? "#ffa500" : "#4682b4",
273
+ label: bm,
274
+ style: (isBase ? "hollow-dot" : "filled-dot") as LegendItem["style"],
275
+ };
276
+ });
277
+ }
278
+
279
+ /** Split samples into warmup/baseline/measured/rejected and downsample each */
280
+ function partitionSamples(
281
+ data: SampleData[],
282
+ showRejected: boolean,
283
+ lttb: Downsample,
284
+ ) {
285
+ const downsample = (arr: SampleData[]) =>
286
+ lttb(
287
+ arr,
288
+ maxDots,
289
+ d => d.sample,
290
+ d => d.displayValue,
291
+ );
292
+ const active = data.filter(d => !d.isWarmup && !d.isRejected);
293
+ const warmup = downsample(data.filter(d => d.isWarmup));
294
+ const baseline = downsample(active.filter(d => d.isBaseline));
295
+ const measured = downsample(active.filter(d => !d.isBaseline));
296
+ const rejected = showRejected
297
+ ? data.filter(d => d.isRejected && !d.isWarmup)
298
+ : [];
299
+ return { warmup, baseline, measured, rejected };
300
+ }
301
+
302
+ /** Semi-transparent hollow dots for Tukey-rejected outlier samples */
303
+ function rejectedDotMark(
304
+ rejected: SampleData[],
305
+ xy: { x: "sample"; y: "displayValue"; r: number },
306
+ tipTitle: (d: SampleData) => string,
307
+ ): any[] {
308
+ if (!rejected.length) return [];
309
+ return [
310
+ Plot.dot(rejected, {
311
+ ...xy,
312
+ stroke: "#999",
313
+ fill: "none",
314
+ strokeWidth: 1,
315
+ opacity: 0.3,
316
+ title: (d: SampleData) => `Rejected ${tipTitle(d)}`,
317
+ }),
318
+ ];
319
+ }