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,159 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import type { Browser } from "playwright";
5
+ import { chromium } from "playwright";
6
+ import { afterAll, beforeAll, expect, test } from "vitest";
7
+
8
+ const binPath = path.resolve(import.meta.dirname!, "../../bin/benchforge");
9
+ const examplePath = path.resolve(
10
+ import.meta.dirname!,
11
+ "../../examples/simple-cli.ts",
12
+ );
13
+
14
+ let proc: ChildProcess;
15
+ let port: number;
16
+ let browser: Browser;
17
+
18
+ test("live viewer: summary tab shows stats", {
19
+ timeout: 30_000,
20
+ }, async () => {
21
+ const consoleErrors: string[] = [];
22
+ const page = await browser.newPage();
23
+ try {
24
+ page.on("console", msg => {
25
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
26
+ consoleErrors.push(msg.text());
27
+ });
28
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
29
+
30
+ const summaryPanel = page.locator("#summary-panel");
31
+ const panel = summaryPanel.locator(".section-panel").first();
32
+ await panel.waitFor({ state: "visible", timeout: 15_000 });
33
+ const statRows = await summaryPanel.locator(".stat-row").count();
34
+ expect(statRows).toBeGreaterThan(0);
35
+ } finally {
36
+ await page.close();
37
+ }
38
+ expect(consoleErrors).toEqual([]);
39
+ });
40
+
41
+ test("live viewer: samples tab shows chart SVG", {
42
+ timeout: 30_000,
43
+ }, async () => {
44
+ const consoleErrors: string[] = [];
45
+ const page = await browser.newPage();
46
+ try {
47
+ page.on("console", msg => {
48
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
49
+ consoleErrors.push(msg.text());
50
+ });
51
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
52
+
53
+ // Wait for summary to load (samples tab becomes enabled)
54
+ await page
55
+ .locator("#summary-panel .section-panel")
56
+ .first()
57
+ .waitFor({ state: "visible", timeout: 15_000 });
58
+
59
+ await page.locator("#tab-samples").click();
60
+
61
+ const samplesPanel = page.locator("#samples-panel");
62
+ const svg = samplesPanel.locator("svg").first();
63
+ await svg.waitFor({ state: "visible", timeout: 15_000 });
64
+ const childCount = await svg
65
+ .locator("path, rect, circle, line, text")
66
+ .count();
67
+ expect(childCount).toBeGreaterThan(0);
68
+ } finally {
69
+ await page.close();
70
+ }
71
+ expect(consoleErrors).toEqual([]);
72
+ });
73
+
74
+ test("live viewer: allocation tab has speedscope content", {
75
+ timeout: 30_000,
76
+ }, async () => {
77
+ const page = await browser.newPage();
78
+ try {
79
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
80
+
81
+ await page.locator("#tab-flamechart").click();
82
+ const frame = page.frameLocator("#speedscope-iframe");
83
+ await frame
84
+ .locator("body *")
85
+ .first()
86
+ .waitFor({ state: "visible", timeout: 15_000 });
87
+ } finally {
88
+ await page.close();
89
+ }
90
+ });
91
+
92
+ test("live viewer: timing tab has speedscope content", {
93
+ timeout: 30_000,
94
+ }, async () => {
95
+ const page = await browser.newPage();
96
+ try {
97
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
98
+
99
+ await page.locator("#tab-time-flamechart").click();
100
+ const frame = page.frameLocator("#time-speedscope-iframe");
101
+ await frame
102
+ .locator("body *")
103
+ .first()
104
+ .waitFor({ state: "visible", timeout: 15_000 });
105
+ } finally {
106
+ await page.close();
107
+ }
108
+ });
109
+
110
+ beforeAll(async () => {
111
+ const args = [
112
+ examplePath,
113
+ "--view-serve",
114
+ "--alloc",
115
+ "--profile",
116
+ "--iterations",
117
+ "3",
118
+ "--warmup",
119
+ "0",
120
+ ];
121
+
122
+ proc = spawn(binPath, args, {
123
+ stdio: ["ignore", "pipe", "pipe"],
124
+ });
125
+
126
+ // Parse port from stdout line like "Viewer: http://localhost:3939"
127
+ const portP = new Promise<number>((resolve, reject) => {
128
+ let stdout = "";
129
+ proc.stdout!.on("data", (chunk: Buffer) => {
130
+ stdout += chunk.toString();
131
+ const match = stdout.match(/Viewer: http:\/\/localhost:(\d+)/);
132
+ if (match) resolve(Number(match[1]));
133
+ });
134
+ proc.on("error", reject);
135
+ proc.on("exit", code => {
136
+ if (!port)
137
+ reject(
138
+ new Error(
139
+ `Process exited (${code}) before viewer started.\nstdout: ${stdout}`,
140
+ ),
141
+ );
142
+ });
143
+ setTimeout(
144
+ () =>
145
+ reject(new Error(`Timed out waiting for viewer.\nstdout: ${stdout}`)),
146
+ 60_000,
147
+ );
148
+ });
149
+
150
+ [port, browser] = await Promise.all([
151
+ portP,
152
+ chromium.launch({ headless: true }),
153
+ ]);
154
+ }, 90_000);
155
+
156
+ afterAll(async () => {
157
+ await browser?.close();
158
+ proc?.kill();
159
+ });
@@ -0,0 +1,137 @@
1
+ import { createServer, type Server } from "node:http";
2
+ import path from "node:path";
3
+ import type { Browser } from "playwright";
4
+ import { chromium } from "playwright";
5
+ import sirv from "sirv";
6
+ import { afterAll, beforeAll, expect, test } from "vitest";
7
+
8
+ const viewerDir = path.resolve(import.meta.dirname!, "../../dist/viewer");
9
+ const archivePath = path.resolve(
10
+ import.meta.dirname!,
11
+ "../../examples/simple-cli.benchforge",
12
+ );
13
+
14
+ let server: Server;
15
+ let port: number;
16
+ let browser: Browser;
17
+
18
+ test("static viewer: drop zone appears on load", {
19
+ timeout: 30_000,
20
+ }, async () => {
21
+ const page = await browser.newPage();
22
+ try {
23
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
24
+ await page.locator(".drop-zone").waitFor({ state: "visible" });
25
+ } finally {
26
+ await page.close();
27
+ }
28
+ });
29
+
30
+ test("static viewer: archive upload shows summary with stats", {
31
+ timeout: 30_000,
32
+ }, async () => {
33
+ const consoleErrors: string[] = [];
34
+ const page = await browser.newPage();
35
+ try {
36
+ page.on("console", msg => {
37
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
38
+ consoleErrors.push(msg.text());
39
+ });
40
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
41
+
42
+ const fileInput = page.locator('.drop-zone input[type="file"]');
43
+ await fileInput.setInputFiles(archivePath);
44
+
45
+ await page
46
+ .locator(".drop-zone")
47
+ .waitFor({ state: "detached", timeout: 15_000 });
48
+
49
+ const summaryPanel = page.locator("#summary-panel");
50
+ const stats = summaryPanel.locator(".section-panel").first();
51
+ await stats.waitFor({ state: "visible", timeout: 15_000 });
52
+ const statRows = await summaryPanel.locator(".stat-row").count();
53
+ expect(statRows).toBeGreaterThan(0);
54
+ } finally {
55
+ await page.close();
56
+ }
57
+ expect(consoleErrors).toEqual([]);
58
+ });
59
+
60
+ test("static viewer: tab navigation after archive upload", {
61
+ timeout: 30_000,
62
+ }, async () => {
63
+ const consoleErrors: string[] = [];
64
+ const page = await browser.newPage();
65
+ try {
66
+ page.on("console", msg => {
67
+ if (msg.type() === "error" && !msg.text().includes("WebGL"))
68
+ consoleErrors.push(msg.text());
69
+ });
70
+ await page.goto(`http://localhost:${port}`, { waitUntil: "networkidle" });
71
+
72
+ const fileInput = page.locator('.drop-zone input[type="file"]');
73
+ await fileInput.setInputFiles(archivePath);
74
+ await page
75
+ .locator(".drop-zone")
76
+ .waitFor({ state: "detached", timeout: 15_000 });
77
+
78
+ // Wait for summary to load
79
+ const summaryPanel = page.locator("#summary-panel");
80
+ await summaryPanel
81
+ .locator(".section-panel")
82
+ .first()
83
+ .waitFor({ state: "visible", timeout: 15_000 });
84
+
85
+ // Samples tab
86
+ await page.locator("#tab-samples").click();
87
+ const samplesPanel = page.locator("#samples-panel");
88
+ await samplesPanel
89
+ .locator("svg")
90
+ .first()
91
+ .waitFor({ state: "visible", timeout: 15_000 });
92
+
93
+ // Allocation tab
94
+ await page.locator("#tab-flamechart").click();
95
+ const allocFrame = page.frameLocator("#speedscope-iframe");
96
+ await allocFrame
97
+ .locator("body *")
98
+ .first()
99
+ .waitFor({ state: "visible", timeout: 15_000 });
100
+
101
+ // Back to Summary
102
+ await page.locator("#tab-summary").click();
103
+ await summaryPanel
104
+ .locator(".section-panel")
105
+ .first()
106
+ .waitFor({ state: "visible" });
107
+ } finally {
108
+ await page.close();
109
+ }
110
+ expect(consoleErrors).toEqual([]);
111
+ });
112
+
113
+ beforeAll(async () => {
114
+ const assets = sirv(viewerDir, { single: true });
115
+ server = createServer((req, res) => {
116
+ assets(req, res, () => {
117
+ res.statusCode = 404;
118
+ res.end("Not found");
119
+ });
120
+ });
121
+ const portP = new Promise<number>((resolve, reject) => {
122
+ server.listen(0, "127.0.0.1", () => {
123
+ const addr = server.address();
124
+ if (typeof addr === "object" && addr) resolve(addr.port);
125
+ else reject(new Error("Failed to get server address"));
126
+ });
127
+ });
128
+ [port, browser] = await Promise.all([
129
+ portP,
130
+ chromium.launch({ headless: true }),
131
+ ]);
132
+ });
133
+
134
+ afterAll(async () => {
135
+ await browser?.close();
136
+ server?.close();
137
+ });
@@ -1,4 +1,4 @@
1
- export const run = () => {
1
+ export const run = (): void => {
2
2
  let _x = 0;
3
3
  for (let i = 0; i < 100; i++) _x++;
4
4
  };
@@ -155,4 +155,6 @@ export const bevy30SamplesMs = [
155
155
  ] as number[];
156
156
 
157
157
  /** Get samples in nanoseconds */
158
- export const bevy30SamplesNs = bevy30SamplesMs.map(s => s * 1_000_000);
158
+ export const bevy30SamplesNs: number[] = bevy30SamplesMs.map(
159
+ s => s * 1_000_000,
160
+ );
@@ -0,0 +1,9 @@
1
+ /** Test cases module with async loadCase */
2
+ export const cases: string[] = ["alpha", "beta"];
3
+
4
+ export async function loadCase(
5
+ id: string,
6
+ ): Promise<{ data: string; metadata: { original: string } }> {
7
+ await Promise.resolve(); // simulate async
8
+ return { data: id.toUpperCase(), metadata: { original: id } };
9
+ }
@@ -1,7 +1,10 @@
1
1
  /** Test cases module for Phase 3 testing */
2
- export const cases = ["small", "large"];
2
+ export const cases: string[] = ["small", "large"];
3
3
 
4
- export function loadCase(id: string) {
4
+ export function loadCase(id: string): {
5
+ data: number[];
6
+ metadata: { size: number };
7
+ } {
5
8
  const data =
6
9
  id === "small" ? [1, 2, 3] : Array.from({ length: 100 }, (_, i) => i);
7
10
  return { data, metadata: { size: data.length } };
@@ -0,0 +1,2 @@
1
+ /** Variant that multiplies array elements */
2
+ export const run = (arr: number[]): number => arr.reduce((a, b) => a * b, 1);
@@ -0,0 +1,2 @@
1
+ /** Variant that sums array elements */
2
+ export const run = (arr: number[]): number => arr.reduce((a, b) => a + b, 0);
@@ -0,0 +1 @@
1
+ export const run = (): void => {};
@@ -1,4 +1,4 @@
1
- export const run = () => {
1
+ export const run = (): void => {
2
2
  let _x = 0;
3
3
  for (let i = 0; i < 100; i++) _x++;
4
4
  };
@@ -0,0 +1 @@
1
+ export const notRun = (): void => {};
@@ -0,0 +1 @@
1
+ export const run = (): void => {};
@@ -1,4 +1,4 @@
1
- export const run = () => {
1
+ export const run = (): void => {
2
2
  let _x = 0;
3
3
  for (let i = 0; i < 100; i++) _x++;
4
4
  };
@@ -0,0 +1,2 @@
1
+ export const setup = (data: unknown): { value: unknown } => ({ value: data });
2
+ export const run = (state: { value: unknown }): unknown => state.value;
@@ -0,0 +1,2 @@
1
+ export const setup = (data: unknown): { value: unknown } => ({ value: data });
2
+ export const run = (state: { value: unknown }): unknown => state.value;
@@ -0,0 +1 @@
1
+ export const run = (): void => {};
@@ -0,0 +1 @@
1
+ export const run = (): void => {};
@@ -0,0 +1 @@
1
+ export const run = (): void => {};
@@ -1,4 +1,4 @@
1
- export const run = () => {
1
+ export const run = (): void => {
2
2
  let _x = 0;
3
3
  for (let i = 0; i < 100; i++) _x++;
4
4
  };
@@ -0,0 +1,30 @@
1
+ /** Format ISO date as local time with UTC: "Jan 9, 2026, 3:45 PM (2026-01-09T23:45:00Z)" */
2
+ export function formatDateWithTimezone(isoDate: string): string {
3
+ const date = new Date(isoDate);
4
+ const local = date.toLocaleString("en-US", {
5
+ month: "short",
6
+ day: "numeric",
7
+ year: "numeric",
8
+ hour: "numeric",
9
+ minute: "2-digit",
10
+ });
11
+ const utc = date.toISOString().replace(".000Z", "Z");
12
+ return `${local} (${utc})`;
13
+ }
14
+
15
+ /** Format relative time: "5m ago", "2h ago", "yesterday", "3 days ago" */
16
+ export function formatRelativeTime(isoDate: string): string {
17
+ const date = new Date(isoDate);
18
+ const now = new Date();
19
+ const diffMs = now.getTime() - date.getTime();
20
+ const diffMins = Math.floor(diffMs / 60000);
21
+ const diffHours = Math.floor(diffMs / 3600000);
22
+ const diffDays = Math.floor(diffMs / 86400000);
23
+
24
+ if (diffMins < 1) return "just now";
25
+ if (diffMins < 60) return `${diffMins}m ago`;
26
+ if (diffHours < 24) return `${diffHours}h ago`;
27
+ if (diffDays === 1) return "yesterday";
28
+ if (diffDays < 30) return `${diffDays} days ago`;
29
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
30
+ }
@@ -0,0 +1,23 @@
1
+ /** Escape a string for safe insertion into HTML. */
2
+ export function escapeHtml(s: string): string {
3
+ const el = document.createElement("div");
4
+ el.textContent = s;
5
+ return el.innerHTML;
6
+ }
7
+
8
+ /** Infer a Shiki language id from a file extension. */
9
+ export function guessLang(file: string): string {
10
+ if (file.endsWith(".ts") || file.endsWith(".tsx")) return "typescript";
11
+ if (file.endsWith(".css")) return "css";
12
+ if (file.endsWith(".html")) return "html";
13
+ return "javascript";
14
+ }
15
+
16
+ /** Extract the pathname from a URL, returning the input unchanged if it isn't a valid URL. */
17
+ export function filePathFromUrl(url: string): string {
18
+ try {
19
+ return new URL(url).pathname;
20
+ } catch {
21
+ return url;
22
+ }
23
+ }
@@ -0,0 +1,120 @@
1
+ import type { ViewerCoverageData, ViewerSpeedscopeFile } from "./Providers.ts";
2
+
3
+ /** Per-line profiling metrics for source gutter display. */
4
+ export interface LineGutterData {
5
+ allocBytes: Map<number, number>;
6
+ selfTimeUs: Map<number, number>;
7
+ callCounts: Map<number, number>;
8
+ }
9
+
10
+ /** Aggregate per-line profiling data for a source file from speedscope profiles. */
11
+ export function computeLineData(
12
+ file: string,
13
+ allocProfile: ViewerSpeedscopeFile | null,
14
+ timeProfile: ViewerSpeedscopeFile | null,
15
+ coverage: ViewerCoverageData | null,
16
+ ): LineGutterData {
17
+ return {
18
+ allocBytes: aggregateSelf(file, allocProfile),
19
+ selfTimeUs: aggregateSelf(file, timeProfile),
20
+ callCounts: extractCallCounts(file, coverage),
21
+ };
22
+ }
23
+
24
+ /** Format byte count for gutter display, scaling to KB/MB as appropriate. */
25
+ export function formatGutterBytes(bytes: number | undefined): string {
26
+ if (!bytes) return "";
27
+ return formatDecimalBytes(bytes);
28
+ }
29
+
30
+ /** Format bytes using decimal (SI) units: KB = 1000, MB = 1e6, GB = 1e9. */
31
+ export function formatDecimalBytes(bytes: number): string {
32
+ if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + " GB";
33
+ if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + " MB";
34
+ if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + " KB";
35
+ return bytes + " B";
36
+ }
37
+
38
+ /** Format microsecond duration for gutter display, scaling to ms/s as appropriate. */
39
+ export function formatGutterTime(us: number | undefined): string {
40
+ if (!us) return "";
41
+ if (us >= 1_000_000) return (us / 1_000_000).toFixed(1) + " s";
42
+ if (us >= 1_000) return (us / 1_000).toFixed(1) + " ms";
43
+ return us.toFixed(0) + " us";
44
+ }
45
+
46
+ /** Format large counts with K/M suffixes (e.g. 1234567 ==> "1.2M"). */
47
+ export function formatCount(n: number): string {
48
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
49
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
50
+ return n.toLocaleString();
51
+ }
52
+
53
+ /** Format a call count for gutter display, scaling to K/M as appropriate. */
54
+ export function formatGutterCount(count: number | undefined): string {
55
+ if (!count) return "";
56
+ return formatCount(count);
57
+ }
58
+
59
+ /** Accumulate weight for the deepest (self) frame matching `file` in each sample. */
60
+ function aggregateSelf(
61
+ file: string,
62
+ profile: ViewerSpeedscopeFile | null,
63
+ ): Map<number, number> {
64
+ const result = new Map<number, number>();
65
+ if (!profile) return result;
66
+
67
+ const { frames } = profile.shared;
68
+
69
+ const fileFrames = new Map<number, number>(); // frameIndex -> line
70
+ frames.forEach((frame, i) => {
71
+ if (frame.line && frame.file && fileMatches(frame.file, file))
72
+ fileFrames.set(i, frame.line);
73
+ });
74
+ if (fileFrames.size === 0) return result;
75
+
76
+ for (const p of profile.profiles) {
77
+ for (let i = 0; i < p.samples.length; i++) {
78
+ const leaf = p.samples[i].at(-1)!;
79
+ const line = fileFrames.get(leaf);
80
+ if (line !== undefined)
81
+ result.set(line, (result.get(line) || 0) + p.weights[i]);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ /** Extract per-function call counts from coverage data for a file. */
88
+ function extractCallCounts(
89
+ file: string,
90
+ coverage: ViewerCoverageData | null,
91
+ ): Map<number, number> {
92
+ const result = new Map<number, number>();
93
+ if (!coverage) return result;
94
+
95
+ const entries = coverage[file] ?? findCoverageEntries(file, coverage);
96
+ if (!entries) return result;
97
+
98
+ for (const entry of entries) {
99
+ if (entry.count <= 0) continue;
100
+ const prev = result.get(entry.startLine) || 0;
101
+ if (entry.count > prev) result.set(entry.startLine, entry.count);
102
+ }
103
+ return result;
104
+ }
105
+
106
+ /** Check if a frame's file URL matches the target file path. */
107
+ function fileMatches(frameFile: string, target: string): boolean {
108
+ if (frameFile === target) return true;
109
+ // Frame files may be full URLs while target is a bare path, or vice versa
110
+ try {
111
+ if (new URL(frameFile).pathname === target) return true;
112
+ } catch {}
113
+ return frameFile.endsWith(target) || target.endsWith(frameFile);
114
+ }
115
+
116
+ /** Find coverage entries by URL matching when exact key lookup fails. */
117
+ function findCoverageEntries(file: string, coverage: ViewerCoverageData) {
118
+ const matchingUrl = Object.keys(coverage).find(url => fileMatches(url, file));
119
+ return matchingUrl ? coverage[matchingUrl] : undefined;
120
+ }