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,134 @@
1
+ import * as Plot from "@observablehq/plot";
2
+
3
+ /** Plot data bounds used to position the legend overlay */
4
+ export interface LegendBounds {
5
+ xMin: number;
6
+ xMax: number;
7
+ yMin?: number;
8
+ yMax: number;
9
+ }
10
+
11
+ /** A single entry in the plot legend with color, label, and symbol style */
12
+ export interface LegendItem {
13
+ color: string;
14
+ label: string;
15
+ style:
16
+ | "filled-dot"
17
+ | "hollow-dot"
18
+ | "vertical-bar"
19
+ | "vertical-line"
20
+ | "rect";
21
+ strokeDash?: string;
22
+ }
23
+
24
+ interface LegendPos {
25
+ legendX: number;
26
+ y: number;
27
+ textX: number;
28
+ xRange: number;
29
+ yRange: number;
30
+ }
31
+
32
+ const rectFields = { x1: "x1", x2: "x2", y1: "y1", y2: "y2" } as const;
33
+
34
+ /** Build complete legend marks array, positioned in the right margin */
35
+ export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
36
+ const xRange = Math.max(bounds.xMax - bounds.xMin, bounds.xMax * 0.1 || 1);
37
+ const yRange = bounds.yMax - (bounds.yMin ?? 0);
38
+ const legendX = bounds.xMax + xRange * 0.04;
39
+ const textX = legendX + xRange * 0.03;
40
+ const itemHeight = yRange * 0.07;
41
+ const topY = bounds.yMax - yRange * 0.02;
42
+
43
+ const pos = (i: number): LegendPos => ({
44
+ legendX,
45
+ y: topY - i * itemHeight,
46
+ textX,
47
+ xRange,
48
+ yRange,
49
+ });
50
+ return items.flatMap((item, i) => [
51
+ symbolMark(pos(i), item),
52
+ textMark(pos(i), item.label),
53
+ ]);
54
+ }
55
+
56
+ function symbolMark(pos: LegendPos, item: LegendItem): any {
57
+ switch (item.style) {
58
+ case "filled-dot":
59
+ return dotMark(pos.legendX, pos.y, item.color, true);
60
+ case "hollow-dot":
61
+ return dotMark(pos.legendX, pos.y, item.color, false);
62
+ case "vertical-bar":
63
+ return verticalBarMark(pos, item.color);
64
+ case "vertical-line":
65
+ return verticalLineMark(pos, item.color, item.strokeDash);
66
+ case "rect":
67
+ return rectMark(pos, item.color);
68
+ }
69
+ }
70
+
71
+ function textMark(pos: LegendPos, label: string): any {
72
+ return Plot.text([{ x: pos.textX, y: pos.y, text: label }], {
73
+ x: "x",
74
+ y: "y",
75
+ text: "text",
76
+ fontSize: 11,
77
+ textAnchor: "start",
78
+ fill: "#333",
79
+ clip: false,
80
+ });
81
+ }
82
+
83
+ function dotMark(x: number, y: number, color: string, filled: boolean): any {
84
+ const base = { x: "x", y: "y", r: 4, clip: false };
85
+ const style = filled
86
+ ? { ...base, fill: color }
87
+ : { ...base, stroke: color, fill: "none", strokeWidth: 1.5 };
88
+ return Plot.dot([{ x, y }], style);
89
+ }
90
+
91
+ function verticalBarMark(pos: LegendPos, color: string): any {
92
+ const { legendX, y, xRange, yRange } = pos;
93
+ const hw = xRange * 0.006;
94
+ const hh = yRange * 0.025;
95
+ const data = [{ x1: legendX - hw, x2: legendX + hw, y1: y - hh, y2: y + hh }];
96
+ return Plot.rect(data, {
97
+ ...rectFields,
98
+ fill: color,
99
+ fillOpacity: 0.6,
100
+ clip: false,
101
+ });
102
+ }
103
+
104
+ function verticalLineMark(
105
+ pos: LegendPos,
106
+ color: string,
107
+ strokeDash?: string,
108
+ ): any {
109
+ const { legendX, y, yRange } = pos;
110
+ const half = yRange * 0.025;
111
+ return Plot.ruleX([legendX], {
112
+ y1: y - half,
113
+ y2: y + half,
114
+ stroke: color,
115
+ strokeWidth: 2,
116
+ strokeDasharray: strokeDash,
117
+ clip: false,
118
+ });
119
+ }
120
+
121
+ function rectMark(pos: LegendPos, color: string): any {
122
+ const { legendX, y, xRange, yRange } = pos;
123
+ const hw = xRange * 0.015;
124
+ const hh = yRange * 0.02;
125
+ const data = [{ x1: legendX - hw, x2: legendX + hw, y1: y - hh, y2: y + hh }];
126
+ return Plot.rect(data, {
127
+ ...rectFields,
128
+ fill: color,
129
+ fillOpacity: 0.3,
130
+ stroke: color,
131
+ strokeWidth: 1,
132
+ clip: false,
133
+ });
134
+ }
@@ -0,0 +1,85 @@
1
+ /** A single timing sample from a benchmark run */
2
+ export interface Sample {
3
+ benchmark: string;
4
+ value: number;
5
+ iteration: number;
6
+ }
7
+
8
+ /** A sample with warmup/optimization metadata for time series plots */
9
+ export interface TimeSeriesPoint {
10
+ benchmark: string;
11
+ iteration: number;
12
+ value: number;
13
+ isWarmup: boolean;
14
+ isBaseline?: boolean;
15
+ isRejected?: boolean;
16
+ /** V8 optimization status code (e.g. 17=turbofan, 33=maglev) */
17
+ optStatus?: number;
18
+ }
19
+
20
+ /** Heap usage sample (in bytes) at a given iteration */
21
+ export interface HeapPoint {
22
+ benchmark: string;
23
+ iteration: number;
24
+ value: number;
25
+ }
26
+
27
+ /** GcEvent flattened with benchmark name for multi-series plots */
28
+ export interface FlatGcEvent {
29
+ benchmark: string;
30
+ sampleIndex: number;
31
+ duration: number;
32
+ }
33
+
34
+ /** PausePoint flattened with benchmark name for multi-series plots */
35
+ export interface FlatPausePoint {
36
+ benchmark: string;
37
+ sampleIndex: number;
38
+ durationMs: number;
39
+ }
40
+
41
+ /** Display unit (ns/us/ms) with conversion and formatting functions */
42
+ export interface TimeUnit {
43
+ unitSuffix: string;
44
+ convertValue: (ms: number) => number;
45
+ formatValue: (d: number) => string;
46
+ }
47
+
48
+ /** Shared Observable Plot layout: margins, dimensions, font size */
49
+ export const plotLayout = {
50
+ marginTop: 24,
51
+ marginLeft: 70,
52
+ marginRight: 110,
53
+ marginBottom: 60,
54
+ width: 550,
55
+ height: 300,
56
+ style: { fontSize: "14px" },
57
+ } as const;
58
+
59
+ /** Format a number as a signed percentage string (e.g. "+1.2%", "-3.4%") */
60
+ export function formatPct(v: number, precision = 1): string {
61
+ const sign = v >= 0 ? "+" : "";
62
+ return `${sign}${v.toFixed(precision)}%`;
63
+ }
64
+
65
+ /** Pick display unit (ns/us/ms) based on average value magnitude (in ms) */
66
+ export function getTimeUnit(values: number[]): TimeUnit {
67
+ const avg = values.reduce((s, v) => s + v, 0) / values.length;
68
+ const locale = (digits: number) => (d: number) =>
69
+ d.toLocaleString("en-US", { maximumFractionDigits: digits });
70
+ const fmt0 = locale(0);
71
+ const fmt1 = locale(1);
72
+ if (avg < 0.001)
73
+ return {
74
+ unitSuffix: "ns",
75
+ convertValue: ms => ms * 1e6,
76
+ formatValue: fmt0,
77
+ };
78
+ if (avg < 1)
79
+ return {
80
+ unitSuffix: "\u00b5s",
81
+ convertValue: ms => ms * 1e3,
82
+ formatValue: fmt1,
83
+ };
84
+ return { unitSuffix: "ms", convertValue: ms => ms, formatValue: fmt1 };
85
+ }
@@ -0,0 +1,230 @@
1
+ import {
2
+ average,
3
+ splitByOffsets,
4
+ tukeyKeep,
5
+ } from "../../stats/StatisticalUtils.ts";
6
+ import type { BenchmarkEntry, ReportData } from "../ReportData.ts";
7
+ import type {
8
+ FlatGcEvent,
9
+ FlatPausePoint,
10
+ HeapPoint,
11
+ Sample,
12
+ TimeSeriesPoint,
13
+ } from "./PlotTypes.ts";
14
+
15
+ /** Benchmark entry tagged with whether it's the baseline for comparison */
16
+ export interface PreparedBenchmark extends BenchmarkEntry {
17
+ isBaseline: boolean;
18
+ }
19
+
20
+ /** All sample data flattened across benchmarks into arrays for plotting */
21
+ export interface FlattenedData {
22
+ allSamples: Sample[];
23
+ timeSeries: TimeSeriesPoint[];
24
+ heapSeries: HeapPoint[];
25
+ baselineHeapSeries: HeapPoint[];
26
+ allGcEvents: FlatGcEvent[];
27
+ allPausePoints: FlatPausePoint[];
28
+ }
29
+
30
+ /** Combine baseline and benchmarks into a single list with display names */
31
+ export function prepareBenchmarks(
32
+ group: ReportData["groups"][0],
33
+ ): PreparedBenchmark[] {
34
+ const base = group.baseline;
35
+ const current = group.benchmarks.map(b => ({ ...b, isBaseline: false }));
36
+ if (!base) return current;
37
+
38
+ const baseName = base.name.endsWith("(baseline)")
39
+ ? base.name
40
+ : base.name + " (baseline)";
41
+ return [{ ...base, name: baseName, isBaseline: true }, ...current];
42
+ }
43
+
44
+ /** Collect all sample data across benchmarks into flat arrays for plotting */
45
+ export function flattenSamples(benchmarks: PreparedBenchmark[]): FlattenedData {
46
+ const out: FlattenedData = {
47
+ allSamples: [],
48
+ timeSeries: [],
49
+ heapSeries: [],
50
+ baselineHeapSeries: [],
51
+ allGcEvents: [],
52
+ allPausePoints: [],
53
+ };
54
+ for (const b of benchmarks) {
55
+ if (b.samples?.length) flattenBenchmark(b, out);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /** @return batch count from the first benchmark with batchOffsets, or 0 */
61
+ export function batchCount(benchmarks: PreparedBenchmark[]): number {
62
+ return (
63
+ benchmarks.find(b => b.batchOffsets?.length)?.batchOffsets?.length ?? 0
64
+ );
65
+ }
66
+
67
+ /** Filter flattened data to a single batch, re-indexing iterations from 0 */
68
+ export function filterToBatch(
69
+ flat: FlattenedData,
70
+ benchmarks: PreparedBenchmark[],
71
+ batchIndex: number,
72
+ ): FlattenedData {
73
+ const ranges = new Map<string, [number, number]>();
74
+ for (const b of benchmarks) {
75
+ const offsets = b.batchOffsets;
76
+ if (!offsets?.length) continue;
77
+ const start = offsets[batchIndex];
78
+ const end =
79
+ batchIndex + 1 < offsets.length
80
+ ? offsets[batchIndex + 1]
81
+ : b.samples.length;
82
+ ranges.set(b.name, [start, end]);
83
+ }
84
+
85
+ const inBatch = (name: string, iter: number) => {
86
+ const r = ranges.get(name);
87
+ return r ? iter >= r[0] && iter < r[1] : true;
88
+ };
89
+ const reindex = (name: string, iter: number) => {
90
+ const r = ranges.get(name);
91
+ return r ? iter - r[0] : iter;
92
+ };
93
+ const sliceIter = <T extends { benchmark: string; iteration: number }>(
94
+ arr: T[],
95
+ ) =>
96
+ arr
97
+ .filter(d => inBatch(d.benchmark, d.iteration))
98
+ .map(d => ({ ...d, iteration: reindex(d.benchmark, d.iteration) }));
99
+
100
+ return {
101
+ allSamples: sliceIter(flat.allSamples),
102
+ timeSeries: sliceIter(flat.timeSeries.filter(d => !d.isWarmup)),
103
+ heapSeries: sliceIter(flat.heapSeries),
104
+ baselineHeapSeries: sliceIter(flat.baselineHeapSeries),
105
+ allGcEvents: flat.allGcEvents.filter(d =>
106
+ inBatch(d.benchmark, d.sampleIndex),
107
+ ),
108
+ allPausePoints: flat.allPausePoints.filter(d =>
109
+ inBatch(d.benchmark, d.sampleIndex),
110
+ ),
111
+ };
112
+ }
113
+
114
+ /** Extract time series, heap, GC, and pause data from one benchmark */
115
+ function flattenBenchmark(b: PreparedBenchmark, out: FlattenedData): void {
116
+ flattenWarmup(b, b.name, out);
117
+ flattenSamplesAndHeap(b, b.name, out);
118
+ flattenGcEvents(b, b.name, out);
119
+ flattenPausePoints(b, b.name, out);
120
+ }
121
+
122
+ /** Warmup samples get negative iteration indices so they appear left of zero */
123
+ function flattenWarmup(
124
+ b: PreparedBenchmark,
125
+ name: string,
126
+ out: FlattenedData,
127
+ ): void {
128
+ const warmupCount = b.warmupSamples?.length || 0;
129
+ b.warmupSamples?.forEach((value, i) => {
130
+ out.timeSeries.push({
131
+ benchmark: name,
132
+ iteration: i - warmupCount,
133
+ value,
134
+ isWarmup: true,
135
+ });
136
+ });
137
+ }
138
+
139
+ /** Populate timeSeries, allSamples (excluding rejected), and heap data */
140
+ function flattenSamplesAndHeap(
141
+ b: PreparedBenchmark,
142
+ name: string,
143
+ out: FlattenedData,
144
+ ): void {
145
+ const rejected = rejectedIndices(b);
146
+ const isBase = b.isBaseline || undefined;
147
+ b.samples.forEach((value, i) => {
148
+ const isRejected = rejected?.has(i) || undefined;
149
+ if (!isRejected)
150
+ out.allSamples.push({ benchmark: name, value, iteration: i });
151
+ const optStatus = b.optSamples?.[i];
152
+ out.timeSeries.push({
153
+ benchmark: name,
154
+ iteration: i,
155
+ value,
156
+ isWarmup: false,
157
+ isBaseline: isBase,
158
+ isRejected,
159
+ optStatus,
160
+ });
161
+ if (b.heapSamples?.[i] !== undefined) {
162
+ const target = b.isBaseline ? out.baselineHeapSeries : out.heapSeries;
163
+ target.push({ benchmark: name, iteration: i, value: b.heapSamples[i] });
164
+ }
165
+ });
166
+ }
167
+
168
+ /** Map GC events to sample indices using cumulative sample durations */
169
+ function flattenGcEvents(
170
+ b: PreparedBenchmark,
171
+ name: string,
172
+ out: FlattenedData,
173
+ ): void {
174
+ if (!b.gcEvents?.length) return;
175
+ const endTimes = cumulativeSum(b.samples);
176
+ for (const gc of b.gcEvents) {
177
+ const idx = endTimes.findIndex(t => t >= gc.offset);
178
+ const sampleIndex = idx >= 0 ? idx : b.samples.length - 1;
179
+ out.allGcEvents.push({
180
+ benchmark: name,
181
+ sampleIndex,
182
+ duration: gc.duration,
183
+ });
184
+ }
185
+ }
186
+
187
+ /** Flatten benchmark pause points into the shared output arrays */
188
+ function flattenPausePoints(
189
+ b: PreparedBenchmark,
190
+ name: string,
191
+ out: FlattenedData,
192
+ ): void {
193
+ if (!b.pausePoints) return;
194
+ for (const p of b.pausePoints)
195
+ out.allPausePoints.push({
196
+ benchmark: name,
197
+ sampleIndex: p.sampleIndex,
198
+ durationMs: p.durationMs,
199
+ });
200
+ }
201
+
202
+ /** @return sample indices in Tukey-rejected batches, or undefined if none */
203
+ function rejectedIndices(b: PreparedBenchmark): Set<number> | undefined {
204
+ const offsets = b.batchOffsets;
205
+ if (!offsets || offsets.length < 4) return undefined;
206
+
207
+ const means = splitByOffsets(b.samples, offsets).map(s => average(s));
208
+ const kept = new Set(tukeyKeep(means));
209
+
210
+ const rejected = new Set<number>();
211
+ for (let bi = 0; bi < means.length; bi++) {
212
+ if (!kept.has(bi)) {
213
+ const start = offsets[bi];
214
+ const end = bi + 1 < offsets.length ? offsets[bi + 1] : b.samples.length;
215
+ for (let j = start; j < end; j++) rejected.add(j);
216
+ }
217
+ }
218
+ return rejected.size > 0 ? rejected : undefined;
219
+ }
220
+
221
+ /** Running total of sample durations, used to map GC offsets to sample indices */
222
+ function cumulativeSum(arr: number[]): number[] {
223
+ const result: number[] = [];
224
+ let sum = 0;
225
+ for (const v of arr) {
226
+ sum += v;
227
+ result.push(sum);
228
+ }
229
+ return result;
230
+ }