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
@@ -1,271 +0,0 @@
1
- import {
2
- type BrowserServer,
3
- type CDPSession,
4
- chromium,
5
- type Page,
6
- } from "playwright";
7
- import type {
8
- HeapProfile,
9
- HeapSampleOptions,
10
- } from "../heap-sample/HeapSampler.ts";
11
- import type { GcStats } from "../runners/GcStats.ts";
12
- import { browserGcStats, type TraceEvent } from "./BrowserGcStats.ts";
13
-
14
- export interface BrowserProfileParams {
15
- url: string;
16
- heapSample?: boolean;
17
- heapOptions?: HeapSampleOptions;
18
- gcStats?: boolean;
19
- headless?: boolean;
20
- chromeArgs?: string[];
21
- timeout?: number; // seconds
22
- maxTime?: number; // ms, bench function iteration time limit
23
- maxIterations?: number; // exact iteration count (bench function mode)
24
- }
25
-
26
- export interface BrowserProfileResult {
27
- heapProfile?: HeapProfile;
28
- gcStats?: GcStats;
29
- /** Wall-clock ms (lap mode: first start to done, bench function: total loop) */
30
- wallTimeMs?: number;
31
- /** Per-iteration timing samples (ms) from bench function or lap mode */
32
- samples?: number[];
33
- }
34
-
35
- interface LapModeHandle {
36
- promise: Promise<BrowserProfileResult>;
37
- cancel: () => void;
38
- }
39
-
40
- /** Run browser benchmark, auto-detecting page API mode.
41
- * Bench function (window.__bench): CLI controls iteration and timing.
42
- * Lap mode (__start/__lap/__done): page controls the measured region. */
43
- export async function profileBrowser(
44
- params: BrowserProfileParams,
45
- ): Promise<BrowserProfileResult> {
46
- const { url, headless = true, chromeArgs, timeout = 60 } = params;
47
- const { gcStats: collectGc } = params;
48
- const { samplingInterval = 32768 } = params.heapOptions ?? {};
49
-
50
- const server = await chromium.launchServer({ headless, args: chromeArgs });
51
- pipeChromeOutput(server);
52
- const browser = await chromium.connect(server.wsEndpoint());
53
- try {
54
- const page = await browser.newPage();
55
- page.setDefaultTimeout(timeout * 1000);
56
- const cdp = await page.context().newCDPSession(page);
57
-
58
- const pageErrors: string[] = [];
59
- page.on("pageerror", err => pageErrors.push(err.message));
60
-
61
- const traceEvents = collectGc ? await startGcTracing(cdp) : [];
62
- const lapMode = await setupLapMode(
63
- page,
64
- cdp,
65
- params,
66
- samplingInterval,
67
- timeout,
68
- pageErrors,
69
- );
70
-
71
- await page.goto(url, { waitUntil: "load" });
72
- const hasBench = await page.evaluate(
73
- () => typeof (globalThis as any).__bench === "function",
74
- );
75
-
76
- let result: BrowserProfileResult;
77
- if (hasBench) {
78
- lapMode.cancel();
79
- lapMode.promise.catch(() => {}); // suppress unused rejection
80
- result = await runBenchLoop(page, cdp, params, samplingInterval);
81
- } else {
82
- result = await lapMode.promise;
83
- lapMode.cancel();
84
- }
85
-
86
- if (collectGc) {
87
- result = { ...result, gcStats: await collectTracing(cdp, traceEvents) };
88
- }
89
- return result;
90
- } finally {
91
- await browser.close();
92
- await server.close();
93
- }
94
- }
95
-
96
- /** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */
97
- function pipeChromeOutput(server: BrowserServer): void {
98
- const proc = server.process();
99
- const pipe = (stream: NodeJS.ReadableStream | null) =>
100
- stream?.on("data", (chunk: Buffer) => {
101
- for (const line of chunk.toString().split("\n")) {
102
- const text = line.trim();
103
- if (text) process.stderr.write(`[chrome] ${text}\n`);
104
- }
105
- });
106
- pipe(proc.stdout);
107
- pipe(proc.stderr);
108
- }
109
-
110
- /** Start CDP GC tracing, returns the event collector array. */
111
- async function startGcTracing(cdp: CDPSession): Promise<TraceEvent[]> {
112
- const events: TraceEvent[] = [];
113
- cdp.on("Tracing.dataCollected", ({ value }) => {
114
- for (const e of value) events.push(e as unknown as TraceEvent);
115
- });
116
- await cdp.send("Tracing.start", {
117
- traceConfig: { includedCategories: ["v8", "v8.gc"] },
118
- });
119
- return events;
120
- }
121
-
122
- /** Inject __start/__lap as in-page functions, expose __done for results collection.
123
- * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
124
- * instrument start. __done() stops instruments and collects timing data. */
125
- async function setupLapMode(
126
- page: Page,
127
- cdp: CDPSession,
128
- params: BrowserProfileParams,
129
- samplingInterval: number,
130
- timeout: number,
131
- pageErrors: string[],
132
- ): Promise<LapModeHandle> {
133
- const { heapSample } = params;
134
- const { promise, resolve, reject } =
135
- Promise.withResolvers<BrowserProfileResult>();
136
- let instrumentsStarted = false;
137
-
138
- await page.exposeFunction("__benchInstrumentStart", async () => {
139
- if (instrumentsStarted) return;
140
- instrumentsStarted = true;
141
- if (heapSample) {
142
- await cdp.send(
143
- "HeapProfiler.startSampling",
144
- heapSamplingParams(samplingInterval),
145
- );
146
- }
147
- });
148
-
149
- await page.exposeFunction(
150
- "__benchCollect",
151
- async (samples: number[], wallTimeMs: number) => {
152
- let heapProfile: HeapProfile | undefined;
153
- if (heapSample && instrumentsStarted) {
154
- const result = await cdp.send("HeapProfiler.stopSampling");
155
- heapProfile = result.profile as unknown as HeapProfile;
156
- }
157
- resolve({ samples, heapProfile, wallTimeMs });
158
- },
159
- );
160
-
161
- await page.addInitScript(injectLapFunctions);
162
-
163
- const timer = setTimeout(() => {
164
- const lines = [`Timed out after ${timeout}s`];
165
- if (pageErrors.length) {
166
- lines.push("Page JS errors:", ...pageErrors.map(e => ` ${e}`));
167
- } else {
168
- lines.push("Page did not call __done() or define window.__bench");
169
- }
170
- reject(new Error(lines.join("\n")));
171
- }, timeout * 1000);
172
-
173
- return { promise, cancel: () => clearTimeout(timer) };
174
- }
175
-
176
- /** Bench function mode: run window.__bench in a timed iteration loop. */
177
- async function runBenchLoop(
178
- page: Page,
179
- cdp: CDPSession,
180
- params: BrowserProfileParams,
181
- samplingInterval: number,
182
- ): Promise<BrowserProfileResult> {
183
- const { heapSample } = params;
184
- const maxTime = params.maxTime ?? 642;
185
- const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;
186
-
187
- if (heapSample) {
188
- await cdp.send(
189
- "HeapProfiler.startSampling",
190
- heapSamplingParams(samplingInterval),
191
- );
192
- }
193
-
194
- const { samples, totalMs } = await page.evaluate(
195
- async ({ maxTime, maxIter }) => {
196
- const bench = (globalThis as any).__bench;
197
- const samples: number[] = [];
198
- const startAll = performance.now();
199
- const deadline = startAll + maxTime;
200
- for (let i = 0; i < maxIter && performance.now() < deadline; i++) {
201
- const t0 = performance.now();
202
- await bench();
203
- samples.push(performance.now() - t0);
204
- }
205
- return { samples, totalMs: performance.now() - startAll };
206
- },
207
- { maxTime, maxIter },
208
- );
209
-
210
- let heapProfile: HeapProfile | undefined;
211
- if (heapSample) {
212
- const result = await cdp.send("HeapProfiler.stopSampling");
213
- heapProfile = result.profile as unknown as HeapProfile;
214
- }
215
-
216
- return { samples, heapProfile, wallTimeMs: totalMs };
217
- }
218
-
219
- /** Stop CDP tracing and parse GC events into GcStats. */
220
- async function collectTracing(
221
- cdp: CDPSession,
222
- traceEvents: TraceEvent[],
223
- ): Promise<GcStats> {
224
- const complete = new Promise<void>(resolve =>
225
- cdp.once("Tracing.tracingComplete", () => resolve()),
226
- );
227
- await cdp.send("Tracing.end");
228
- await complete;
229
- return browserGcStats(traceEvents);
230
- }
231
-
232
- function heapSamplingParams(samplingInterval: number) {
233
- return {
234
- samplingInterval,
235
- includeObjectsCollectedByMajorGC: true,
236
- includeObjectsCollectedByMinorGC: true,
237
- };
238
- }
239
-
240
- /** In-page timing functions injected via addInitScript (zero CDP overhead).
241
- * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
242
- function injectLapFunctions(): void {
243
- const g = globalThis as any;
244
- g.__benchSamples = [];
245
- g.__benchLastTime = 0;
246
- g.__benchFirstStart = 0;
247
-
248
- g.__start = () => {
249
- const now = performance.now();
250
- g.__benchLastTime = now;
251
- if (!g.__benchFirstStart) {
252
- g.__benchFirstStart = now;
253
- return g.__benchInstrumentStart();
254
- }
255
- };
256
-
257
- g.__lap = () => {
258
- const now = performance.now();
259
- g.__benchSamples.push(now - g.__benchLastTime);
260
- g.__benchLastTime = now;
261
- };
262
-
263
- g.__done = () => {
264
- const wall = g.__benchFirstStart
265
- ? performance.now() - g.__benchFirstStart
266
- : 0;
267
- return g.__benchCollect(g.__benchSamples.slice(), wall);
268
- };
269
- }
270
-
271
- export { profileBrowser as profileBrowserHeap };
@@ -1,103 +0,0 @@
1
- import { writeFile } from "node:fs/promises";
2
- import type { ReportGroup } from "../BenchmarkReport.ts";
3
- import type { DefaultCliArgs } from "../cli/CliArgs.ts";
4
- import type {
5
- BenchmarkGroup,
6
- BenchmarkJsonData,
7
- BenchmarkResult,
8
- } from "./JsonFormat.ts";
9
-
10
- /** Export benchmark results to JSON file */
11
- export async function exportBenchmarkJson(
12
- groups: ReportGroup[],
13
- outputPath: string,
14
- args: DefaultCliArgs,
15
- suiteName = "Benchmark Suite",
16
- ): Promise<void> {
17
- const jsonData = prepareJsonData(groups, args, suiteName);
18
- const jsonString = JSON.stringify(jsonData, null, 2);
19
-
20
- await writeFile(outputPath, jsonString, "utf-8");
21
- console.log(`Benchmark data exported to: ${outputPath}`);
22
- }
23
-
24
- /** Convert ReportGroup data to JSON format */
25
- function prepareJsonData(
26
- groups: ReportGroup[],
27
- args: DefaultCliArgs,
28
- suiteName: string,
29
- ): BenchmarkJsonData {
30
- return {
31
- meta: {
32
- timestamp: new Date().toISOString(),
33
- version: process.env.npm_package_version || "unknown",
34
- args: cleanCliArgs(args),
35
- environment: {
36
- node: process.version,
37
- platform: process.platform,
38
- arch: process.arch,
39
- },
40
- },
41
- suites: [
42
- {
43
- name: suiteName,
44
- groups: groups.map(convertGroup),
45
- },
46
- ],
47
- };
48
- }
49
-
50
- /** Clean CLI args for JSON export (remove undefined values) */
51
- function cleanCliArgs(args: DefaultCliArgs): Record<string, any> {
52
- const toCamel = (k: string) =>
53
- k.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
54
- const entries = Object.entries(args)
55
- .filter(([, v]) => v !== undefined && v !== null)
56
- .map(([k, v]) => [toCamel(k), v]);
57
- return Object.fromEntries(entries);
58
- }
59
-
60
- /** Convert a report group, mapping each report to the JSON result format */
61
- function convertGroup(group: ReportGroup): BenchmarkGroup {
62
- return {
63
- name: "Benchmark Group", // Could be enhanced to include actual group names
64
- baseline: group.baseline ? convertReport(group.baseline) : undefined,
65
- benchmarks: group.reports.map(convertReport),
66
- };
67
- }
68
-
69
- /** Extract measured stats and optional metrics into JSON result shape */
70
- function convertReport(report: any): BenchmarkResult {
71
- const { name, measuredResults: m } = report;
72
- const { time, heapSize, gcTime, cpu } = m;
73
- const minMaxMean = (s: any) =>
74
- s ? { min: s.min, max: s.max, mean: s.avg } : undefined;
75
-
76
- return {
77
- name,
78
- status: "completed",
79
- samples: m.samples || [],
80
- time: {
81
- ...minMaxMean(time)!,
82
- p50: time.p50,
83
- p75: time.p75,
84
- p99: time.p99,
85
- p999: time.p999,
86
- },
87
- heapSize: minMaxMean(heapSize),
88
- gcTime: minMaxMean(gcTime),
89
- cpu: cpu
90
- ? {
91
- instructions: cpu.instructions,
92
- cycles: cpu.cycles,
93
- cacheMisses: m.cpuCacheMiss,
94
- branchMisses: cpu.branchMisses,
95
- }
96
- : undefined,
97
- execution: {
98
- iterations: m.samples?.length || 0,
99
- totalTime: m.totalTime || 0,
100
- warmupRuns: undefined, // Not available in current data structure
101
- },
102
- };
103
- }
@@ -1,91 +0,0 @@
1
- /** Complete benchmark data structure for JSON export */
2
- export interface BenchmarkJsonData {
3
- meta: {
4
- timestamp: string;
5
- version: string;
6
- args: Record<string, any>;
7
- environment: {
8
- node: string;
9
- platform: string;
10
- arch?: string;
11
- };
12
- };
13
- suites: BenchmarkSuite[];
14
- }
15
-
16
- export interface BenchmarkSuite {
17
- name: string;
18
- groups: BenchmarkGroup[];
19
- }
20
-
21
- export interface BenchmarkGroup {
22
- name: string;
23
- baseline?: BenchmarkResult;
24
- benchmarks: BenchmarkResult[];
25
- }
26
-
27
- export interface BenchmarkResult {
28
- name: string;
29
- status: "completed" | "running" | "failed";
30
-
31
- /** Raw execution time samples in milliseconds */
32
- samples: number[];
33
-
34
- /** Statistical summaries */
35
- time: {
36
- min: number;
37
- max: number;
38
- mean: number;
39
- p50: number;
40
- p75: number;
41
- p99: number;
42
- p999: number;
43
- };
44
-
45
- /** Optional performance metrics */
46
- heapSize?: {
47
- min: number;
48
- max: number;
49
- mean: number;
50
- };
51
-
52
- gcTime?: {
53
- min: number;
54
- max: number;
55
- mean: number;
56
- };
57
-
58
- cpu?: {
59
- instructions?: number;
60
- cycles?: number;
61
- cacheMisses?: number;
62
- branchMisses?: number;
63
- };
64
-
65
- /** Execution metadata */
66
- execution: {
67
- iterations: number;
68
- totalTime: number;
69
- warmupRuns?: number;
70
- };
71
-
72
- /** Adaptive mode results */
73
- adaptive?: {
74
- confidenceInterval: {
75
- lower: number;
76
- upper: number;
77
- margin: number;
78
- marginPercent: number;
79
- confidence: number;
80
- };
81
- converged: boolean;
82
- stopReason: "threshold_met" | "max_time" | "max_iterations";
83
- };
84
-
85
- /** Error information */
86
- error?: {
87
- message: string;
88
- type: string;
89
- stackTrace?: string;
90
- };
91
- }
@@ -1,202 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join, resolve } from "node:path";
5
-
6
- import { groupReports, type ReportGroup } from "../BenchmarkReport.ts";
7
- import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
8
- import {
9
- type ResolvedFrame,
10
- type ResolvedProfile,
11
- resolveProfile,
12
- } from "../heap-sample/ResolvedProfile.ts";
13
-
14
- /** speedscope file format (https://www.speedscope.app/file-format-schema.json) */
15
- interface SpeedscopeFile {
16
- $schema: "https://www.speedscope.app/file-format-schema.json";
17
- shared: { frames: SpeedscopeFrame[] };
18
- profiles: SpeedscopeProfile[];
19
- name?: string;
20
- exporter?: string;
21
- }
22
-
23
- interface SpeedscopeFrame {
24
- name: string;
25
- file?: string;
26
- line?: number;
27
- col?: number;
28
- }
29
-
30
- interface SpeedscopeProfile {
31
- type: "sampled";
32
- name: string;
33
- unit: "bytes";
34
- startValue: number;
35
- endValue: number;
36
- samples: number[][]; // each sample is stack of frame indices
37
- weights: number[]; // bytes per sample
38
- }
39
-
40
- /** Export heap profiles from benchmark results to speedscope JSON format.
41
- * Creates one speedscope profile per benchmark that has a heapProfile.
42
- * @returns resolved output path, or undefined if no profiles were found */
43
- export function exportSpeedscope(
44
- groups: ReportGroup[],
45
- outputPath: string,
46
- ): string | undefined {
47
- const frames: SpeedscopeFrame[] = [];
48
- const frameIndex = new Map<string, number>();
49
- const profiles: SpeedscopeProfile[] = [];
50
-
51
- for (const group of groups) {
52
- for (const report of groupReports(group)) {
53
- const { heapProfile } = report.measuredResults;
54
- if (!heapProfile) continue;
55
- const resolved = resolveProfile(heapProfile);
56
- profiles.push(buildProfile(report.name, resolved, frames, frameIndex));
57
- }
58
- }
59
-
60
- if (profiles.length === 0) {
61
- console.log("No heap profiles to export.");
62
- return undefined;
63
- }
64
-
65
- const file: SpeedscopeFile = {
66
- $schema: "https://www.speedscope.app/file-format-schema.json",
67
- shared: { frames },
68
- profiles,
69
- exporter: "benchforge",
70
- };
71
-
72
- const absPath = resolve(outputPath);
73
- writeFileSync(absPath, JSON.stringify(file));
74
- console.log(`Speedscope profile exported to: ${outputPath}`);
75
- return absPath;
76
- }
77
-
78
- /** Export to a temp file and open in speedscope via npx */
79
- export function exportAndLaunchSpeedscope(groups: ReportGroup[]): void {
80
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
81
- const outputPath = join(tmpdir(), `benchforge-${timestamp}.speedscope.json`);
82
- const absPath = exportSpeedscope(groups, outputPath);
83
- if (absPath) {
84
- launchSpeedscope(absPath);
85
- }
86
- }
87
-
88
- /** Launch speedscope viewer on a file via npx */
89
- export function launchSpeedscope(filePath: string): void {
90
- console.log("Opening speedscope...");
91
- const child = spawn("npx", ["speedscope", filePath], {
92
- detached: true,
93
- stdio: "ignore",
94
- });
95
- child.unref();
96
- child.on("error", () => {
97
- console.error(
98
- `Failed to launch speedscope. Run manually:\n npx speedscope ${filePath}`,
99
- );
100
- });
101
- }
102
-
103
- /** Convert a single HeapProfile to speedscope format (for standalone use) */
104
- export function heapProfileToSpeedscope(
105
- name: string,
106
- profile: HeapProfile,
107
- ): SpeedscopeFile {
108
- const frames: SpeedscopeFrame[] = [];
109
- const frameIndex = new Map<string, number>();
110
- const resolved = resolveProfile(profile);
111
- const p = buildProfile(name, resolved, frames, frameIndex);
112
-
113
- return {
114
- $schema: "https://www.speedscope.app/file-format-schema.json",
115
- shared: { frames },
116
- profiles: [p],
117
- exporter: "benchforge",
118
- };
119
- }
120
-
121
- /** Build a single speedscope profile from a resolved heap profile */
122
- function buildProfile(
123
- name: string,
124
- resolved: ResolvedProfile,
125
- sharedFrames: SpeedscopeFrame[],
126
- frameIndex: Map<string, number>,
127
- ): SpeedscopeProfile {
128
- // Build nodeId -> stack of frame indices
129
- const nodeStacks = new Map<number, number[]>();
130
- for (const node of resolved.nodes) {
131
- const stack = node.stack.map(f => internFrame(f, sharedFrames, frameIndex));
132
- nodeStacks.set(node.nodeId, stack);
133
- }
134
-
135
- const samples: number[][] = [];
136
- const weights: number[] = [];
137
-
138
- if (!resolved.sortedSamples || resolved.sortedSamples.length === 0) {
139
- console.error(
140
- `Speedscope export: no samples in heap profile for "${name}", skipping`,
141
- );
142
- return {
143
- type: "sampled",
144
- name,
145
- unit: "bytes",
146
- startValue: 0,
147
- endValue: 0,
148
- samples,
149
- weights,
150
- };
151
- }
152
-
153
- for (const sample of resolved.sortedSamples) {
154
- const stack = nodeStacks.get(sample.nodeId);
155
- if (stack) {
156
- samples.push(stack);
157
- weights.push(sample.size);
158
- }
159
- }
160
-
161
- const totalBytes = weights.reduce((sum, w) => sum + w, 0);
162
-
163
- return {
164
- type: "sampled",
165
- name,
166
- unit: "bytes",
167
- startValue: 0,
168
- endValue: totalBytes,
169
- samples,
170
- weights,
171
- };
172
- }
173
-
174
- /** Intern a call frame, returning its index in the shared frames array */
175
- function internFrame(
176
- frame: ResolvedFrame,
177
- sharedFrames: SpeedscopeFrame[],
178
- frameIndex: Map<string, number>,
179
- ): number {
180
- const { name, url, line, col } = frame;
181
- const key = `${name}\0${url}\0${line}\0${col}`;
182
-
183
- let idx = frameIndex.get(key);
184
- if (idx === undefined) {
185
- idx = sharedFrames.length;
186
- // Match speedscope's convention: anonymous functions include location in name
187
- const shortFile = url ? url.split("/").pop() : undefined;
188
- const displayName =
189
- name !== "(anonymous)"
190
- ? name
191
- : shortFile
192
- ? `(anonymous ${shortFile}:${line})`
193
- : "(anonymous)";
194
- const entry: SpeedscopeFrame = { name: displayName };
195
- if (url) entry.file = url;
196
- if (line > 0) entry.line = line;
197
- if (col != null) entry.col = col;
198
- sharedFrames.push(entry);
199
- frameIndex.set(key, idx);
200
- }
201
- return idx;
202
- }