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
@@ -0,0 +1,332 @@
1
+ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
2
+ import { useLazyPlot } from "./LazyPlot.ts";
3
+ import type { GitVersion } from "../../report/GitUtils.ts";
4
+ import type { DifferenceCI } from "../../stats/StatisticalUtils.ts";
5
+ import { formatRelativeTime } from "../DateFormat.ts";
6
+ import { formatCount, formatDecimalBytes } from "../LineData.ts";
7
+ import type {
8
+ BenchmarkEntry,
9
+ BenchmarkGroup,
10
+ BootstrapCIData,
11
+ ReportData,
12
+ ViewerEntry,
13
+ ViewerRow,
14
+ ViewerSection,
15
+ } from "../ReportData.ts";
16
+ import { formatPct } from "../plots/PlotTypes.ts";
17
+ import { activeTabId, provider, reportData } from "../State.ts";
18
+
19
+ const skipArgs = new Set(["_", "$0", "view", "file"]);
20
+
21
+ /** Main summary view: fetches report data, shows CLI args header and collapsible benchmark groups. */
22
+ export function SummaryPanel() {
23
+ const dataProvider = provider.value!;
24
+ const data = reportData.value;
25
+ const [error, setError] = useState<string | null>(null);
26
+
27
+ useEffect(() => {
28
+ dataProvider.fetchReportData()
29
+ .then(result => (reportData.value = result as ReportData))
30
+ .catch(err => {
31
+ console.error("Report load failed:", err);
32
+ setError(String(err));
33
+ });
34
+ }, [dataProvider]);
35
+
36
+ if (error)
37
+ return <div class="empty-state"><p>Failed to load report data: {error}</p></div>;
38
+ if (!data)
39
+ return <div class="empty-state"><p>Loading report&hellip;</p></div>;
40
+
41
+ return (
42
+ <>
43
+ <ReportHeader metadata={data.metadata} />
44
+ {data.groups.map((group, i) => (
45
+ <CollapsibleGroup key={i} group={group} />
46
+ ))}
47
+ </>
48
+ );
49
+ }
50
+
51
+ declare const __BENCHFORGE_GIT_HASH__: string;
52
+ declare const __BENCHFORGE_GIT_DIRTY__: boolean;
53
+ declare const __BENCHFORGE_BUILD_DATE__: string;
54
+
55
+ /** Fallback for dev/unbundled builds where compile-time globals are absent. */
56
+ function safeGlobal<T>(v: T, fallback: T): T {
57
+ return typeof v !== "undefined" ? v : fallback;
58
+ }
59
+
60
+ /** Assemble "benchforge <hash> <relative-date>" from compile-time globals. */
61
+ function benchforgeLabel(): string {
62
+ const hash = safeGlobal(__BENCHFORGE_GIT_HASH__, "dev");
63
+ const dirty = safeGlobal(__BENCHFORGE_GIT_DIRTY__, false);
64
+ const date = safeGlobal(__BENCHFORGE_BUILD_DATE__, "");
65
+ const label = `benchforge ${hash}${dirty ? "*" : ""}`;
66
+ return date ? `${label} ${formatRelativeTime(date)}` : label;
67
+ }
68
+
69
+ function ReportHeader({ metadata }: { metadata: ReportData["metadata"] }) {
70
+ const { cliArgs, cliDefaults, currentVersion, baselineVersion } = metadata;
71
+ const versions = [
72
+ currentVersion && `Current: ${formatVersion(currentVersion)}`,
73
+ baselineVersion && `Baseline: ${formatVersion(baselineVersion)}`,
74
+ ].filter(Boolean);
75
+
76
+ return (
77
+ <div class="report-header">
78
+ <div class="cli-args">{formatCliArgs(cliArgs, cliDefaults)}</div>
79
+ <div class="header-right">
80
+ <div class="metadata">{new Date().toLocaleString()}</div>
81
+ <div class="metadata benchforge-version">{benchforgeLabel()}</div>
82
+ {versions.length > 0 && (
83
+ <div class="version-info">{versions.join(" | ")}</div>
84
+ )}
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ /** Expandable benchmark group with comparison badge and section panels. */
91
+ function CollapsibleGroup({ group }: { group: BenchmarkGroup }) {
92
+ const [open, setOpen] = useState(true);
93
+ const current = group.benchmarks?.[0];
94
+ if (!current) return <div class="error">No benchmark data for this group</div>;
95
+
96
+ const ci = current.comparisonCI;
97
+ return (
98
+ <div class="benchmark-group">
99
+ <div class="group-header" onClick={() => setOpen(o => !o)}>
100
+ <span class="group-toggle">{open ? "\u25be" : "\u25b8"}</span>
101
+ <h2>{group.name}</h2>
102
+ {ci && <ComparisonBadge ci={ci} />}
103
+ {group.warnings && (
104
+ <span class="batch-warnings">
105
+ {group.warnings.map(w => <span class="batch-warning">{w}</span>)}
106
+ </span>
107
+ )}
108
+ </div>
109
+ {open && <GroupContent current={current} />}
110
+ </div>
111
+ );
112
+ }
113
+
114
+ function GroupContent({ current }: { current: BenchmarkEntry }) {
115
+ const ref = useRef<HTMLDivElement>(null);
116
+ useEffect(() => {
117
+ if (ref.current) alignRunColumns(ref.current);
118
+ });
119
+ return (
120
+ <div class="panel-grid" ref={ref}>
121
+ {current.sections?.map((s, i) => <SectionPanel key={i} section={s} />)}
122
+ <HeapPanel entry={current} />
123
+ <CoveragePanel entry={current} />
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function SectionPanel({ section }: { section: ViewerSection }) {
129
+ if (!section.rows.length) return null;
130
+ const range = useMemo(() => sectionEstimateRange(section), [section]);
131
+ const titleEl = section.tabLink
132
+ ? <a class="panel-title-link" onClick={() => (activeTabId.value = section.tabLink!)}>{section.title}</a>
133
+ : <span>{section.title}</span>;
134
+
135
+ return (
136
+ <div class="section-panel">
137
+ <div class="panel-header">{titleEl}</div>
138
+ <div class="panel-body">
139
+ {section.rows.map((row, i) => <StatRow key={i} row={row} estimateRange={range} />)}
140
+ </div>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ /** Set CSS vars so run-name and run-value columns align across all sections. */
146
+ function alignRunColumns(panel: HTMLElement): void {
147
+ const maxW = (sel: string) =>
148
+ Math.max(0, ...[...panel.querySelectorAll<HTMLElement>(sel)].map(el => el.scrollWidth));
149
+ const maxName = maxW(".run-name");
150
+ const maxValue = maxW(".run-value");
151
+ if (maxName) panel.style.setProperty("--run-name-width", `${maxName}px`);
152
+ if (maxValue) panel.style.setProperty("--run-value-width", `${maxValue}px`);
153
+ }
154
+
155
+ function StatRow({ row, estimateRange }: { row: ViewerRow; estimateRange?: [number, number] }) {
156
+ if (row.shared) {
157
+ return (
158
+ <div class="stat-row">
159
+ <div class="row-header">
160
+ <span class="row-label">{row.label}</span>
161
+ </div>
162
+ <div class="run-entry">
163
+ <span class="run-name" />
164
+ <span class="run-value">{row.entries[0]?.value}</span>
165
+ </div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ return (
171
+ <div class={`stat-row${row.primary ? " primary-row" : ""}`}>
172
+ <div class="row-header">
173
+ <span class="row-label">{row.label}</span>
174
+ {row.comparisonCI && <ComparisonBadge ci={row.comparisonCI} compact />}
175
+ </div>
176
+ {row.entries.map((entry, i) => (
177
+ <RunEntry key={i} entry={entry} estimateRange={estimateRange} />
178
+ ))}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ /** Proportional horizontal offset range for aligning bootstrap CI plots. */
184
+ const maxCIShift = 80;
185
+
186
+ function RunEntry({ entry, estimateRange }: { entry: ViewerEntry; estimateRange?: [number, number] }) {
187
+ const ci = entry.bootstrapCI;
188
+ const [lo, hi] = estimateRange ?? [0, 0];
189
+ const shift = ci && hi > lo ? ((ci.estimate - lo) / (hi - lo)) * maxCIShift : undefined;
190
+ return (
191
+ <div class="run-entry">
192
+ <span class="run-name">{entry.runName}</span>
193
+ {ci
194
+ ? <BootstrapCIMount ci={ci} label={entry.value} shift={shift} />
195
+ : <span class="run-value">{entry.value}</span>}
196
+ </div>
197
+ );
198
+ }
199
+
200
+ function SharedStat({ label, value }: { label: string; value: string }) {
201
+ return (
202
+ <div class="stat-row shared-row">
203
+ <span class="row-label">{label}</span>
204
+ <span class="row-value">{value}</span>
205
+ </div>
206
+ );
207
+ }
208
+
209
+ function HeapPanel({ entry }: { entry: BenchmarkEntry }) {
210
+ const { heapSummary: heap, allocationSamples: allocSamples } = entry;
211
+ if (!heap && !allocSamples?.length) return null;
212
+
213
+ return (
214
+ <div class="section-panel">
215
+ <div class="panel-header">
216
+ <a class="panel-title-link" onClick={() => (activeTabId.value = "flamechart")}>
217
+ heap allocation
218
+ </a>
219
+ </div>
220
+ <div class="panel-body">
221
+ {heap && (
222
+ <>
223
+ <SharedStat label="total bytes" value={formatDecimalBytes(heap.totalBytes)} />
224
+ <SharedStat label="user bytes" value={formatDecimalBytes(heap.userBytes)} />
225
+ </>
226
+ )}
227
+ {allocSamples && allocSamples.length > 0 && (
228
+ <SharedStat label="alloc samples" value={allocSamples.length.toLocaleString()} />
229
+ )}
230
+ </div>
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function CoveragePanel({ entry }: { entry: BenchmarkEntry }) {
236
+ const cov = entry.coverageSummary;
237
+ if (!cov) return null;
238
+
239
+ return (
240
+ <div class="section-panel">
241
+ <div class="panel-header">
242
+ <span>calls</span>
243
+ </div>
244
+ <div class="panel-body">
245
+ <SharedStat label="functions tracked" value={cov.functionCount.toLocaleString()} />
246
+ <SharedStat label="total calls" value={formatCount(cov.totalCalls)} />
247
+ </div>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ const directionLabels: Record<string, string> = {
253
+ faster: "Faster", slower: "Slower", uncertain: "Inconclusive", equivalent: "Equivalent",
254
+ };
255
+
256
+ function ComparisonBadge({ ci, compact }: { ci: DifferenceCI; compact?: boolean }) {
257
+ return (
258
+ <span class="comparison-badge">
259
+ <span class={`badge badge-${ci.direction}`}>
260
+ {compact ? formatPct(ci.percent) : directionLabels[ci.direction]}
261
+ </span>
262
+ {ci.histogram && <CIPlotMount ci={ci} compact={compact} />}
263
+ </span>
264
+ );
265
+ }
266
+
267
+ /** Lazy-imports CIPlot and renders a confidence interval chart inline. */
268
+ function CIPlotMount({ ci, compact }: { ci: DifferenceCI; compact?: boolean }) {
269
+ const ref = useLazyPlot(async () => {
270
+ const { createCIPlot } = await import("../plots/CIPlot.ts");
271
+ const equivMargin = (reportData.value?.metadata.cliArgs?.["equiv-margin"] as number) || undefined;
272
+ const opts = compact ? { width: 200, height: 70, title: "", equivMargin } : { equivMargin };
273
+ return createCIPlot(ci, opts);
274
+ }, [ci, compact], "CI plot");
275
+ return <div class="ci-plot-container" ref={ref} />;
276
+ }
277
+
278
+ /** Lazy-imports CIPlot and renders a bootstrap distribution sparkline inline. */
279
+ function BootstrapCIMount({ ci, label, shift }: {
280
+ ci: BootstrapCIData; label?: string; shift?: number;
281
+ }) {
282
+ const ref = useLazyPlot(async () => {
283
+ const { createDistributionPlot } = await import("../plots/CIPlot.ts");
284
+ const opts = {
285
+ width: 240, height: 80, title: "", direction: "uncertain" as const,
286
+ ciLabels: ci.ciLabels, includeZero: false, smooth: true, pointLabel: label,
287
+ ciLevel: ci.ciLevel, ciReliable: ci.ciReliable,
288
+ };
289
+ return createDistributionPlot(ci.histogram, ci.ci, ci.estimate, opts);
290
+ }, [ci, label], "Bootstrap CI plot");
291
+ const style = shift != null ? { marginLeft: `${Math.round(shift)}px` } : undefined;
292
+ return <div class="ci-plot-inline" style={style} ref={ref} />;
293
+ }
294
+
295
+ /** Compute min/max bootstrap estimates across a section for proportional positioning */
296
+ function sectionEstimateRange(section: ViewerSection): [number, number] | undefined {
297
+ const estimates = section.rows
298
+ .flatMap(row => row.entries)
299
+ .map(e => e.bootstrapCI?.estimate)
300
+ .filter((v): v is number => v != null);
301
+ if (estimates.length < 2) return undefined;
302
+ const min = Math.min(...estimates), max = Math.max(...estimates);
303
+ return max > min ? [min, max] : undefined;
304
+ }
305
+
306
+ /** Format CLI args for display, filtering out defaults, internal keys, and camelCase aliases. */
307
+ function formatCliArgs(
308
+ args?: Record<string, unknown>,
309
+ defaults?: Record<string, unknown>,
310
+ ): string {
311
+ if (!args) return "benchforge";
312
+ const isDisplayable = (key: string, value: unknown): boolean => {
313
+ if (skipArgs.has(key) || value === undefined || value === false) return false;
314
+ if (defaults?.[key] === value) return false;
315
+ // skip camelCase aliases (yargs generates both kebab-case and camelCase)
316
+ if (!key.includes("-") && key !== key.toLowerCase()) return false;
317
+ if (key === "convergence" && !args.adaptive) return false;
318
+ return true;
319
+ };
320
+ const flags = Object.entries(args)
321
+ .filter(([key, value]) => isDisplayable(key, value))
322
+ .map(([key, value]) => (value === true ? `--${key}` : `--${key} ${value}`));
323
+ return ["benchforge", ...flags].join(" ");
324
+ }
325
+
326
+ /** Format a git version as "hash (relative-date)", with dirty marker. */
327
+ function formatVersion(v: GitVersion): string {
328
+ if (!v || v.hash === "unknown") return "unknown";
329
+ const hash = v.dirty ? v.hash + "*" : v.hash;
330
+ if (!v.date) return hash;
331
+ return `${hash} (${formatRelativeTime(v.date)})`;
332
+ }
@@ -0,0 +1,131 @@
1
+ import { useState } from "preact/hooks";
2
+ import type { DataProvider } from "../Providers.ts";
3
+ import {
4
+ activeTabId,
5
+ defaultTabId,
6
+ provider,
7
+ reportData,
8
+ samplesLoaded,
9
+ sourceTabs,
10
+ } from "../State.ts";
11
+ import { hasSufficientSamples } from "./SamplesPanel.tsx";
12
+ import { ThemeToggle } from "./ThemeToggle.tsx";
13
+
14
+ /** Top navigation bar with fixed tabs, dynamic source tabs, theme toggle, and archive download. */
15
+ export function TabBar() {
16
+ const dataProvider = provider.value!;
17
+ const { config } = dataProvider;
18
+ const data = reportData.value;
19
+ const samplesEnabled = !!data && hasSufficientSamples(data);
20
+
21
+ return (
22
+ <div class="tab-bar">
23
+ <TabButton tabId="summary" disabled={!config.hasReport}>
24
+ Summary
25
+ </TabButton>
26
+ <TabButton tabId="samples" disabled={!samplesEnabled} onActivate={() => (samplesLoaded.value = true)}>
27
+ Iterations
28
+ </TabButton>
29
+ <TabButton tabId="flamechart" disabled={!config.hasProfile}>
30
+ Allocation
31
+ </TabButton>
32
+ <TabButton tabId="time-flamechart" disabled={!config.hasTimeProfile}>
33
+ Timing
34
+ </TabButton>
35
+
36
+ {sourceTabs.value.map(st => (
37
+ <SourceTabBtn key={st.id} tabId={st.id} file={st.file} line={st.line} />
38
+ ))}
39
+
40
+ <div class="tab-spacer" />
41
+ <ThemeToggle />
42
+ <ArchiveButton provider={dataProvider} />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ /** Fixed tab button that sets the active tab on click. */
48
+ function TabButton({ tabId, disabled, onActivate, children }: {
49
+ tabId: string; disabled: boolean; onActivate?: () => void; children: preact.ComponentChildren;
50
+ }) {
51
+ const active = activeTabId.value === tabId;
52
+ return (
53
+ <button
54
+ class={`tab${active ? " active" : ""}`}
55
+ data-tab={tabId}
56
+ id={`tab-${tabId}`}
57
+ disabled={disabled}
58
+ onClick={() => {
59
+ activeTabId.value = tabId;
60
+ onActivate?.();
61
+ }}
62
+ >
63
+ {children}
64
+ </button>
65
+ );
66
+ }
67
+
68
+ /** Source-file tab with close button; clicking the close span removes the tab instead of activating it. */
69
+ function SourceTabBtn({ tabId, file, line }: { tabId: string; file: string; line: number }) {
70
+ const active = activeTabId.value === tabId;
71
+ const shortName = file.split("/").pop() || file;
72
+ const label = line ? `${shortName}:${line}` : shortName;
73
+
74
+ return (
75
+ <button
76
+ class={`tab${active ? " active" : ""}`}
77
+ data-tab={tabId}
78
+ onClick={(e: MouseEvent) => {
79
+ if ((e.target as HTMLElement).closest(".tab-close")) {
80
+ closeSourceTab(tabId);
81
+ return;
82
+ }
83
+ activeTabId.value = tabId;
84
+ }}
85
+ >
86
+ {label}{" "}
87
+ <span class="tab-close" title="Close">
88
+ &times;
89
+ </span>
90
+ </button>
91
+ );
92
+ }
93
+
94
+ /** Remove a source tab and fall back to the best available fixed tab. */
95
+ function closeSourceTab(tabId: string): void {
96
+ sourceTabs.value = sourceTabs.value.filter(t => t.id !== tabId);
97
+ if (activeTabId.value === tabId) activeTabId.value = defaultTabId();
98
+ }
99
+
100
+ /** Download button that bundles all report data into a `.benchforge` archive. */
101
+ function ArchiveButton({ provider: dataProvider }: { provider: DataProvider }) {
102
+ const [archiving, setArchiving] = useState(false);
103
+
104
+ async function downloadArchive(): Promise<void> {
105
+ setArchiving(true);
106
+ try {
107
+ const { blob, filename } = await dataProvider.createArchive();
108
+ const url = URL.createObjectURL(blob);
109
+ const link = Object.assign(document.createElement("a"), { href: url, download: filename });
110
+ document.body.appendChild(link);
111
+ link.click();
112
+ link.remove();
113
+ URL.revokeObjectURL(url);
114
+ } catch (err) {
115
+ console.error("Archive failed:", err);
116
+ } finally {
117
+ setArchiving(false);
118
+ }
119
+ }
120
+
121
+ return (
122
+ <button
123
+ class="tab archive-btn"
124
+ data-action="archive"
125
+ disabled={archiving}
126
+ onClick={downloadArchive}
127
+ >
128
+ {archiving ? "Archiving\u2026" : "Archive \u2193"}
129
+ </button>
130
+ );
131
+ }
@@ -0,0 +1,46 @@
1
+ import type { ViewerConfig } from "../Providers.ts";
2
+ import { activeTabId, provider, sourceTabs } from "../State.ts";
3
+ import { SamplesPanel } from "./SamplesPanel.tsx";
4
+ import { SourcePanel } from "./SourcePanel.tsx";
5
+ import { SummaryPanel } from "./SummaryPanel.tsx";
6
+
7
+ /** Renders all tab panels, showing only the active one via CSS class toggling. */
8
+ export function TabContent() {
9
+ const dataProvider = provider.value!;
10
+ const { config } = dataProvider;
11
+ const tabId = activeTabId.value;
12
+ const panelClass = (id: string) => `report-panel${tabId === id ? " active" : ""}`;
13
+
14
+ return (
15
+ <div class="tab-content">
16
+ <div id="summary-panel" class={panelClass("summary")}>
17
+ {config.hasReport && <SummaryPanel />}
18
+ </div>
19
+ <div id="samples-panel" class={panelClass("samples")}>
20
+ <SamplesPanel />
21
+ </div>
22
+ <iframe
23
+ id="speedscope-iframe"
24
+ src={iframeSrc(dataProvider.profileUrl("alloc"), config)}
25
+ style={{ display: tabId === "flamechart" ? "block" : "none" }}
26
+ />
27
+ <iframe
28
+ id="time-speedscope-iframe"
29
+ src={iframeSrc(dataProvider.profileUrl("time"), config)}
30
+ style={{ display: tabId === "time-flamechart" ? "block" : "none" }}
31
+ />
32
+ {sourceTabs.value.map(st => (
33
+ <SourcePanel key={st.id} tab={st} />
34
+ ))}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ /** Build a Speedscope iframe hash-URL with optional editor URI. */
40
+ function iframeSrc(url: string | null, config: ViewerConfig): string {
41
+ if (!url) return "";
42
+ const parts = ["profileURL=" + encodeURIComponent(url)];
43
+ if (config.editorUri)
44
+ parts.push("editorUri=" + encodeURIComponent(config.editorUri));
45
+ return "speedscope/#" + parts.join("&");
46
+ }
@@ -0,0 +1,50 @@
1
+ import { themePreference } from "../State.ts";
2
+ import { setTheme } from "../Theme.ts";
3
+
4
+ /** Light/dark mode buttons. Clicking the active mode reverts to system default. */
5
+ export function ThemeToggle(): preact.JSX.Element {
6
+ const pref = themePreference.value;
7
+
8
+ return (
9
+ <div class="theme-toggle">
10
+ <button
11
+ class={`theme-btn${pref === "light" ? " active" : ""}`}
12
+ title="Light mode"
13
+ onClick={() => setTheme(pref === "light" ? "system" : "light")}
14
+ >
15
+ <Sun />
16
+ </button>
17
+ <button
18
+ class={`theme-btn${pref === "dark" ? " active" : ""}`}
19
+ title="Dark mode"
20
+ onClick={() => setTheme(pref === "dark" ? "system" : "dark")}
21
+ >
22
+ <Moon />
23
+ </button>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ function Sun() {
29
+ return (
30
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <circle cx="12" cy="12" r="5" />
32
+ <line x1="12" y1="1" x2="12" y2="3" />
33
+ <line x1="12" y1="21" x2="12" y2="23" />
34
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
35
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
36
+ <line x1="1" y1="12" x2="3" y2="12" />
37
+ <line x1="21" y1="12" x2="23" y2="12" />
38
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
39
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
40
+ </svg>
41
+ );
42
+ }
43
+
44
+ function Moon() {
45
+ return (
46
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
48
+ </svg>
49
+ );
50
+ }
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Benchforge Viewer</title>
7
+ <script>
8
+ var m = document.cookie.match(/(?:^|; )theme=(light|dark)/);
9
+ if (m) document.documentElement.dataset.theme = m[1];
10
+ </script>
11
+ <link rel="stylesheet" href="./shell.css">
12
+ <link rel="stylesheet" href="./report.css">
13
+ </head>
14
+
15
+ <body>
16
+ <div id="app"></div>
17
+
18
+ <script type="module" src="./main.tsx"></script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,4 @@
1
+ import { render } from "preact";
2
+ import { App } from "./components/App.tsx";
3
+
4
+ render(<App />, document.getElementById("app")!);