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,160 @@
1
+ import type { GcStats } from "../../runners/GcStats.ts";
2
+ import type { CoverageData } from "../node/CoverageTypes.ts";
3
+ import type { HeapProfile, HeapSampleOptions } from "../node/HeapSampler.ts";
4
+ import type { TimeProfile } from "../node/TimeSampler.ts";
5
+ import { runBenchLoop } from "./BenchLoop.ts";
6
+ import { collectTracing, startGcTracing } from "./BrowserCDP.ts";
7
+ import { type CdpClient, connectCdp } from "./CdpClient.ts";
8
+ import { type CdpPage, createCdpPage } from "./CdpPage.ts";
9
+ import {
10
+ type ChromeInstance,
11
+ closeTab,
12
+ createTab,
13
+ launchChrome,
14
+ } from "./ChromeLauncher.ts";
15
+ import { runPageLoad } from "./PageLoadMode.ts";
16
+
17
+ /** Options for a browser benchmark run. */
18
+ export interface BrowserProfileParams {
19
+ /** URL to benchmark */
20
+ url: string;
21
+ /** Enable heap allocation profiling */
22
+ alloc?: boolean;
23
+ /** Heap sampling options (interval, depth) */
24
+ allocOptions?: HeapSampleOptions;
25
+ /** Enable CPU time profiling */
26
+ profile?: boolean;
27
+ /** CPU profiling sample interval in microseconds (default 1000) */
28
+ profileInterval?: number;
29
+ /** Track function call counts via V8 coverage */
30
+ callCounts?: boolean;
31
+ /** Collect GC statistics via CDP tracing */
32
+ gcStats?: boolean;
33
+ /** Run Chrome in headless mode */
34
+ headless?: boolean;
35
+ /** Path to Chrome executable */
36
+ chromePath?: string;
37
+ /** Chrome user data directory for persistent profile */
38
+ chromeProfile?: string;
39
+ /** Extra Chrome launch arguments */
40
+ chromeArgs?: string[];
41
+ /** Page timeout in seconds */
42
+ timeout?: number;
43
+ /** Bench function iteration time limit in ms */
44
+ maxTime?: number;
45
+ /** Exact iteration count for bench function mode */
46
+ maxIterations?: number;
47
+ /** Passive page-load profiling mode */
48
+ pageLoad?: boolean;
49
+ /** Completion signal: CSS selector, JS expression, "load", or "domcontentloaded" */
50
+ waitFor?: string;
51
+ /** Reuse an existing Chrome instance (caller manages lifecycle) */
52
+ chrome?: ChromeInstance;
53
+ }
54
+
55
+ /** Navigation timing metrics from the Performance API. */
56
+ export interface NavTiming {
57
+ /** DOMContentLoaded time in ms */
58
+ domContentLoaded: number;
59
+ /** Load event time in ms */
60
+ loadEvent: number;
61
+ /** Largest Contentful Paint time in ms */
62
+ lcp?: number;
63
+ }
64
+
65
+ /** Collected profiles, timing samples, and GC stats from a browser benchmark. */
66
+ export interface BrowserProfileResult {
67
+ /** Heap allocation profile */
68
+ heapProfile?: HeapProfile;
69
+ /** CPU time profile */
70
+ timeProfile?: TimeProfile;
71
+ /** V8 code coverage data */
72
+ coverage?: CoverageData;
73
+ /** Garbage collection statistics */
74
+ gcStats?: GcStats;
75
+ /** Wall-clock ms for the entire bench loop or page load */
76
+ wallTimeMs?: number;
77
+ /** Per-iteration timing samples (ms) from bench function mode */
78
+ samples?: number[];
79
+ /** Navigation timing from page-load mode */
80
+ navTiming?: NavTiming;
81
+ }
82
+
83
+ /** Shared context passed to bench/page-load mode runners. */
84
+ export interface ProfileCtx {
85
+ page: CdpPage;
86
+ cdp: CdpClient;
87
+ params: BrowserProfileParams;
88
+ samplingInterval: number;
89
+ }
90
+
91
+ /**
92
+ * Run browser benchmark, auto-detecting mode:
93
+ * - Bench function (window.__bench): CLI controls iteration and timing.
94
+ * - Page load (no __bench, or --page-load): measures navigation timing.
95
+ */
96
+ export async function profileBrowser(
97
+ params: BrowserProfileParams,
98
+ ): Promise<BrowserProfileResult> {
99
+ const {
100
+ headless = false,
101
+ chromePath,
102
+ chromeProfile,
103
+ chromeArgs: args,
104
+ } = params;
105
+ const owned = !params.chrome;
106
+ const launch = { headless, chromePath, chromeProfile, args };
107
+ const chrome = params.chrome ?? (await launchChrome(launch));
108
+ try {
109
+ const { wsUrl, targetId } = await createTab(chrome.port);
110
+ const cdp = await connectCdp(wsUrl);
111
+ try {
112
+ const timeout = (params.timeout ?? 60) * 1000;
113
+ const page = await createCdpPage(cdp, { timeout });
114
+ return await runProfile(page, cdp, params);
115
+ } finally {
116
+ cdp.close();
117
+ await closeTab(chrome.port, targetId);
118
+ }
119
+ } finally {
120
+ if (owned) await chrome.close();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Run profiling on an open CDP page, auto-detecting mode:
126
+ * - **bench**: page exports `window.__bench` ==> CLI iterates and times it
127
+ * - **page-load**: no `__bench` found (or `--page-load` flag) ==> profile navigation
128
+ *
129
+ * When auto-detecting, navigates once to check for `__bench`. If not found,
130
+ * reloads via `runPageLoad` which starts instruments before navigation.
131
+ */
132
+ async function runProfile(
133
+ page: CdpPage,
134
+ cdp: CdpClient,
135
+ params: BrowserProfileParams,
136
+ ): Promise<BrowserProfileResult> {
137
+ const samplingInterval = params.allocOptions?.samplingInterval ?? 32768;
138
+ const traceEvents = params.gcStats ? await startGcTracing(cdp) : [];
139
+ const ctx = { page, cdp, params, samplingInterval };
140
+
141
+ let result: BrowserProfileResult;
142
+ if (params.pageLoad) {
143
+ result = await runPageLoad(ctx);
144
+ } else {
145
+ await page.navigate(params.url, { waitUntil: "load" });
146
+ const hasBench = await page.evaluate(
147
+ () => typeof (globalThis as any).__bench === "function",
148
+ );
149
+ if (hasBench) {
150
+ result = await runBenchLoop(ctx);
151
+ } else {
152
+ console.warn("No __bench found. Reloading in --page-load mode.");
153
+ result = await runPageLoad(ctx);
154
+ }
155
+ }
156
+
157
+ if (params.gcStats)
158
+ return { ...result, gcStats: await collectTracing(cdp, traceEvents) };
159
+ return result;
160
+ }
@@ -0,0 +1,82 @@
1
+ /** Minimal CDP WebSocket client. */
2
+ export interface CdpClient {
3
+ send(method: string, params?: Record<string, unknown>): Promise<any>;
4
+ on(event: string, handler: (params: any) => void): void;
5
+ once(event: string, handler: (params: any) => void): void;
6
+ close(): void;
7
+ }
8
+
9
+ /** Connect to a CDP WebSocket endpoint and return a client. */
10
+ export async function connectCdp(wsUrl: string): Promise<CdpClient> {
11
+ const ws = await openWebSocket(wsUrl);
12
+ let nextId = 1;
13
+ type Pending = { resolve: (v: any) => void; reject: (e: Error) => void };
14
+ const pending = new Map<number, Pending>();
15
+ const listeners = new Map<string, Set<(params: any) => void>>();
16
+
17
+ ws.addEventListener("message", event => {
18
+ const msg = JSON.parse(String(event.data));
19
+ if ("id" in msg) {
20
+ const p = pending.get(msg.id);
21
+ if (!p) return;
22
+ pending.delete(msg.id);
23
+ if (msg.error) p.reject(new Error(`CDP: ${msg.error.message}`));
24
+ else p.resolve(msg.result ?? {});
25
+ } else if ("method" in msg) {
26
+ for (const h of listeners.get(msg.method) ?? []) h(msg.params ?? {});
27
+ }
28
+ });
29
+
30
+ const client: CdpClient = {
31
+ send(method, params) {
32
+ return new Promise((resolve, reject) => {
33
+ const id = nextId++;
34
+ const timer = setTimeout(() => {
35
+ if (pending.delete(id))
36
+ reject(new Error(`CDP timeout after 60s: ${method}`));
37
+ }, 60_000);
38
+ const clear = () => clearTimeout(timer);
39
+ pending.set(id, {
40
+ resolve(v) {
41
+ clear();
42
+ resolve(v);
43
+ },
44
+ reject(e) {
45
+ clear();
46
+ reject(e);
47
+ },
48
+ });
49
+ ws.send(JSON.stringify({ id, method, params }));
50
+ });
51
+ },
52
+ on(event, handler) {
53
+ const set = listeners.get(event) ?? new Set();
54
+ listeners.set(event, set);
55
+ set.add(handler);
56
+ },
57
+ once(event, handler) {
58
+ const wrap = (params: any) => {
59
+ listeners.get(event)?.delete(wrap);
60
+ handler(params);
61
+ };
62
+ client.on(event, wrap);
63
+ },
64
+ close() {
65
+ for (const [, p] of pending) p.reject(new Error("CDP connection closed"));
66
+ pending.clear();
67
+ ws.close();
68
+ },
69
+ };
70
+ return client;
71
+ }
72
+
73
+ /** Open a WebSocket connection, rejecting if the handshake fails. */
74
+ async function openWebSocket(wsUrl: string): Promise<WebSocket> {
75
+ const ws = new WebSocket(wsUrl);
76
+ const err = new Error(`CDP connect failed: ${wsUrl}`);
77
+ await new Promise<void>((resolve, reject) => {
78
+ ws.addEventListener("open", () => resolve());
79
+ ws.addEventListener("error", () => reject(err));
80
+ });
81
+ return ws;
82
+ }
@@ -0,0 +1,138 @@
1
+ import type { CdpClient } from "./CdpClient.ts";
2
+
3
+ /** Page-level CDP abstraction for navigation, evaluation, and event handling. */
4
+ export interface CdpPage {
5
+ navigate(
6
+ url: string,
7
+ opts?: { waitUntil?: "load" | "domcontentloaded" },
8
+ ): Promise<void>;
9
+ evaluate<R>(
10
+ fn: (...args: any[]) => R | Promise<R>,
11
+ arg?: unknown,
12
+ ): Promise<Awaited<R>>;
13
+ exposeFunction(name: string, fn: (...args: any[]) => any): Promise<void>;
14
+ addInitScript(fn: () => void): Promise<void>;
15
+ waitForSelector(selector: string): Promise<void>;
16
+ waitForFunction(expression: string): Promise<void>;
17
+ onPageError(handler: (message: string) => void): void;
18
+ }
19
+
20
+ /** Create a page abstraction over a CDP client connected to a page target. */
21
+ export async function createCdpPage(
22
+ cdp: CdpClient,
23
+ opts?: { timeout?: number },
24
+ ): Promise<CdpPage> {
25
+ const timeout = opts?.timeout ?? 30_000;
26
+
27
+ await cdp.send("Page.enable");
28
+ await cdp.send("Runtime.enable");
29
+
30
+ return {
31
+ navigate: (url, navOpts) => cdpNavigate(cdp, url, navOpts),
32
+ evaluate: (fn, arg) => cdpEvaluate(cdp, fn, arg),
33
+ exposeFunction: (name, fn) => cdpExpose(cdp, name, fn),
34
+ async addInitScript(fn) {
35
+ await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
36
+ source: `(${fn.toString()})()`,
37
+ });
38
+ },
39
+ waitForSelector(sel) {
40
+ const expr = `!!document.querySelector(${JSON.stringify(sel)})`;
41
+ return pollEval(cdp, expr, timeout);
42
+ },
43
+ waitForFunction: expr => pollEval(cdp, expr, timeout),
44
+ onPageError(handler) {
45
+ cdp.on("Runtime.exceptionThrown", ({ exceptionDetails: d }) => {
46
+ handler(d.exception?.description || d.text);
47
+ });
48
+ },
49
+ };
50
+ }
51
+
52
+ /** Navigate to a URL and wait for the specified load condition. */
53
+ async function cdpNavigate(
54
+ cdp: CdpClient,
55
+ url: string,
56
+ navOpts?: { waitUntil?: "load" | "domcontentloaded" },
57
+ ): Promise<void> {
58
+ const waitUntil = navOpts?.waitUntil ?? "load";
59
+ const event =
60
+ waitUntil === "domcontentloaded"
61
+ ? "Page.domContentEventFired"
62
+ : "Page.loadEventFired";
63
+ const loaded = new Promise<void>(r => cdp.once(event, () => r()));
64
+ await cdp.send("Page.navigate", { url });
65
+ await loaded;
66
+ }
67
+
68
+ /** Evaluate a function in the page and return the result. */
69
+ async function cdpEvaluate(
70
+ cdp: CdpClient,
71
+ fn: (...args: any[]) => any,
72
+ arg?: unknown,
73
+ ): Promise<any> {
74
+ const argStr = arg !== undefined ? JSON.stringify(arg) : "";
75
+ const expression = `(${fn.toString()})(${argStr})`;
76
+ const opts = { expression, awaitPromise: true, returnByValue: true };
77
+ const { result, exceptionDetails: err } = await cdp.send(
78
+ "Runtime.evaluate",
79
+ opts,
80
+ );
81
+ if (err) throw new Error(err.exception?.description || err.text);
82
+ return result.value;
83
+ }
84
+
85
+ /** Expose a Node function to the page via Runtime.addBinding. */
86
+ async function cdpExpose(
87
+ cdp: CdpClient,
88
+ name: string,
89
+ fn: (...args: any[]) => any,
90
+ ): Promise<void> {
91
+ const binding = `__cdp_${name}`;
92
+ await cdp.send("Runtime.addBinding", { name: binding });
93
+
94
+ // Wrapper: page calls window[name](...) ==> binding fires ==> Node runs fn ==> resolve
95
+ const wrapper = `(() => {
96
+ const g = globalThis;
97
+ if (!g.__cdpSeq) { g.__cdpSeq = 0; g.__cdpCbs = {}; }
98
+ g[${JSON.stringify(name)}] = (...args) => new Promise((resolve, reject) => {
99
+ const seq = ++g.__cdpSeq;
100
+ g.__cdpCbs[seq] = { resolve, reject };
101
+ g[${JSON.stringify(binding)}](JSON.stringify({ seq, args }));
102
+ });
103
+ })()`;
104
+
105
+ await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: wrapper });
106
+ await cdp.send("Runtime.evaluate", { expression: wrapper });
107
+
108
+ const pageEval = (expr: string) =>
109
+ cdp.send("Runtime.evaluate", { expression: expr });
110
+ cdp.on("Runtime.bindingCalled", async params => {
111
+ if (params.name !== binding) return;
112
+ const { seq, args } = JSON.parse(params.payload);
113
+ const cb = `globalThis.__cdpCbs[${seq}]`;
114
+ try {
115
+ const val = await fn(...args);
116
+ await pageEval(`${cb}?.resolve(${JSON.stringify(val ?? null)})`);
117
+ } catch (err: any) {
118
+ const msg = JSON.stringify(String(err.message));
119
+ await pageEval(`${cb}?.reject(new Error(${msg}))`);
120
+ }
121
+ });
122
+ }
123
+
124
+ /** Poll a JS expression until truthy, with timeout. */
125
+ async function pollEval(
126
+ cdp: CdpClient,
127
+ expression: string,
128
+ timeout: number,
129
+ ): Promise<void> {
130
+ const deadline = Date.now() + timeout;
131
+ const evalOpts = { expression, returnByValue: true };
132
+ while (Date.now() < deadline) {
133
+ const { result } = await cdp.send("Runtime.evaluate", evalOpts);
134
+ if (result.value) return;
135
+ await new Promise(r => setTimeout(r, 100));
136
+ }
137
+ throw new Error(`Timed out waiting for: ${expression}`);
138
+ }
@@ -0,0 +1,158 @@
1
+ import { type ChildProcess, execFileSync, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ /** Handle for a running Chrome instance. */
8
+ export interface ChromeInstance {
9
+ port: number;
10
+ process: ChildProcess;
11
+ close(): Promise<void>;
12
+ }
13
+
14
+ /** Flags to suppress background services irrelevant to benchmarking. */
15
+ const quietFlags = [
16
+ "--disable-background-networking",
17
+ "--disable-client-side-phishing-detection",
18
+ "--disable-component-update",
19
+ "--disable-field-trial-config",
20
+ "--disable-sync",
21
+ "--disable-breakpad",
22
+ "--noerrdialogs",
23
+ "--disable-features=OptimizationHints,Translate,MediaRouter,DialMediaRouteProvider",
24
+ "--disable-extensions",
25
+ "--disable-component-extensions-with-background-pages",
26
+ "--disable-default-apps",
27
+ "--metrics-recording-only",
28
+ "--no-service-autorun",
29
+ "--password-store=basic",
30
+ "--use-mock-keychain",
31
+ ];
32
+
33
+ /** Stderr patterns to suppress (irrelevant to benchmarking). */
34
+ const chromeNoise =
35
+ /SharedImageManager|skia_output_device_buffer_queue|task_policy_set/;
36
+
37
+ /** Launch Chrome with remote debugging and return instance handle. */
38
+ export async function launchChrome(opts: {
39
+ headless?: boolean;
40
+ chromePath?: string;
41
+ chromeProfile?: string;
42
+ args?: string[];
43
+ }): Promise<ChromeInstance> {
44
+ const { headless = false, chromeProfile, chromePath, args = [] } = opts;
45
+ const chrome = chromePath || process.env.CHROME_PATH || findChrome();
46
+
47
+ const tmpDir = chromeProfile
48
+ ? undefined
49
+ : await mkdtemp(join(tmpdir(), "benchforge-"));
50
+ const userDataDir = chromeProfile ?? tmpDir!;
51
+
52
+ const flags = [
53
+ "--remote-debugging-port=0",
54
+ `--user-data-dir=${userDataDir}`,
55
+ "--no-first-run",
56
+ "--no-default-browser-check",
57
+ ...quietFlags,
58
+ ...(headless ? ["--headless=new"] : []),
59
+ ...args,
60
+ ];
61
+
62
+ const proc = spawn(chrome, flags, { stdio: ["pipe", "pipe", "pipe"] });
63
+ const wsUrlPromise = parseWsUrl(proc);
64
+ pipeChromeOutput(proc);
65
+ const wsUrl = await wsUrlPromise;
66
+ const port = Number(new URL(wsUrl).port);
67
+
68
+ return {
69
+ port,
70
+ process: proc,
71
+ async close() {
72
+ proc.kill();
73
+ await new Promise<void>(r => proc.on("exit", () => r()));
74
+ if (tmpDir)
75
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
76
+ },
77
+ };
78
+ }
79
+
80
+ /** Create a new browser tab and return its CDP WebSocket URL and target ID. */
81
+ export async function createTab(
82
+ port: number,
83
+ ): Promise<{ wsUrl: string; targetId: string }> {
84
+ const url = `http://127.0.0.1:${port}/json/new`;
85
+ const resp = await fetch(url, { method: "PUT" });
86
+ const text = await resp.text();
87
+ try {
88
+ const json = JSON.parse(text);
89
+ return { wsUrl: json.webSocketDebuggerUrl, targetId: json.id };
90
+ } catch {
91
+ const msg = `Chrome /json/new returned non-JSON: ${text.slice(0, 200)}`;
92
+ throw new Error(msg);
93
+ }
94
+ }
95
+
96
+ /** Close a browser tab by target ID. */
97
+ export async function closeTab(port: number, targetId: string): Promise<void> {
98
+ const url = `http://127.0.0.1:${port}/json/close/${targetId}`;
99
+ await fetch(url).catch(() => {});
100
+ }
101
+
102
+ /** Find Chrome/Chromium on the system. */
103
+ function findChrome(): string {
104
+ if (process.platform === "darwin") {
105
+ const path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
106
+ if (existsSync(path)) return path;
107
+ }
108
+ if (process.platform === "win32") {
109
+ for (const env of ["ProgramFiles", "ProgramFiles(x86)"] as const) {
110
+ const base = process.env[env];
111
+ if (!base) continue;
112
+ const p = join(base, "Google", "Chrome", "Application", "chrome.exe");
113
+ if (existsSync(p)) return p;
114
+ }
115
+ }
116
+ for (const name of ["google-chrome", "chromium-browser", "chromium"]) {
117
+ try {
118
+ return execFileSync("which", [name], { encoding: "utf8" }).trim();
119
+ } catch {}
120
+ }
121
+ throw new Error(
122
+ "Chrome not found. Install Chrome or set CHROME_PATH, or use --chrome <path>.",
123
+ );
124
+ }
125
+
126
+ /** Parse the DevTools WebSocket URL from Chrome's stderr. */
127
+ function parseWsUrl(proc: ChildProcess): Promise<string> {
128
+ return new Promise((resolve, reject) => {
129
+ const wsPattern = /DevTools listening on (ws:\/\/\S+)/;
130
+ const onData = (chunk: Buffer) => {
131
+ const match = chunk.toString().match(wsPattern);
132
+ if (match) {
133
+ proc.stderr?.off("data", onData);
134
+ resolve(match[1]);
135
+ }
136
+ };
137
+ proc.stderr?.on("data", onData);
138
+ proc.on("error", reject);
139
+ proc.on("exit", code =>
140
+ reject(new Error(`Chrome exited (code ${code}) before DevTools ready`)),
141
+ );
142
+ });
143
+ }
144
+
145
+ /** Forward Chrome stdout/stderr to terminal, filtering known noise. */
146
+ function pipeChromeOutput(proc: ChildProcess): void {
147
+ const forward = (stream: NodeJS.ReadableStream | null) =>
148
+ stream?.on("data", (chunk: Buffer) => {
149
+ const lines = chunk
150
+ .toString()
151
+ .split("\n")
152
+ .map(l => l.trim())
153
+ .filter(l => l && !chromeNoise.test(l));
154
+ for (const line of lines) process.stderr.write(`[chrome] ${line}\n`);
155
+ });
156
+ forward(proc.stdout);
157
+ forward(proc.stderr);
158
+ }
@@ -0,0 +1,28 @@
1
+ /** Chrome Trace Event format (used by Perfetto and CDP tracing). */
2
+ export interface TraceEvent {
3
+ /** Event type: M=metadata, C=counter, i=instant, B/E=begin/end, X=complete */
4
+ ph: string;
5
+
6
+ /** Timestamp in microseconds */
7
+ ts: number;
8
+
9
+ /** Process ID */
10
+ pid?: number;
11
+
12
+ /** Thread ID */
13
+ tid?: number;
14
+
15
+ /** Event category */
16
+ cat?: string;
17
+
18
+ name: string;
19
+
20
+ /** Arbitrary event arguments */
21
+ args?: Record<string, unknown>;
22
+
23
+ /** Scope for instant events: "t"=thread, "p"=process, "g"=global */
24
+ s?: string;
25
+
26
+ /** Duration for complete events (microseconds) */
27
+ dur?: number;
28
+ }
@@ -0,0 +1,61 @@
1
+ import {
2
+ instrumentOpts,
3
+ startInstruments,
4
+ stopInstruments,
5
+ } from "./BrowserCDP.ts";
6
+ import type {
7
+ BrowserProfileResult,
8
+ NavTiming,
9
+ ProfileCtx,
10
+ } from "./BrowserProfiler.ts";
11
+ import type { CdpPage } from "./CdpPage.ts";
12
+
13
+ /** Run passive page-load profiling: instrument ==> navigate ==> wait ==> collect. */
14
+ export async function runPageLoad(
15
+ ctx: ProfileCtx,
16
+ ): Promise<BrowserProfileResult> {
17
+ const { page, cdp, params, samplingInterval } = ctx;
18
+ const opts = instrumentOpts(params, samplingInterval);
19
+ await startInstruments(cdp, opts);
20
+
21
+ // Observe LCP via PerformanceObserver (avoids deprecated getEntriesByType warning)
22
+ await page.addInitScript(() => {
23
+ const g = globalThis as any;
24
+ g.__lcpTime = undefined;
25
+ new PerformanceObserver(list => {
26
+ const entries = list.getEntries();
27
+ if (entries.length) g.__lcpTime = entries.at(-1)!.startTime;
28
+ }).observe({ type: "largest-contentful-paint" as any, buffered: true });
29
+ });
30
+
31
+ const { url, waitFor } = params;
32
+
33
+ const isBuiltinWait = waitFor === "load" || waitFor === "domcontentloaded";
34
+ const waitUntil = isBuiltinWait ? waitFor : "load";
35
+ await page.navigate(url, { waitUntil });
36
+
37
+ if (waitFor && !isBuiltinWait) {
38
+ if (/^[#.[]/.test(waitFor)) {
39
+ await page.waitForSelector(waitFor);
40
+ } else {
41
+ await page.waitForFunction(waitFor);
42
+ }
43
+ }
44
+
45
+ const navTiming = await readNavTiming(page);
46
+ const collected = await stopInstruments(cdp, opts);
47
+ return { ...collected, navTiming, wallTimeMs: navTiming.loadEvent };
48
+ }
49
+
50
+ /** Read navigation timing from the page via Performance API. */
51
+ export async function readNavTiming(page: CdpPage): Promise<NavTiming> {
52
+ return page.evaluate(() => {
53
+ const perf = performance as any;
54
+ const nav = perf.getEntriesByType("navigation")[0] ?? {};
55
+ return {
56
+ domContentLoaded: (nav.domContentLoadedEventEnd as number) ?? 0,
57
+ loadEvent: (nav.loadEventEnd as number) ?? 0,
58
+ lcp: (globalThis as any).__lcpTime as number | undefined,
59
+ };
60
+ });
61
+ }
@@ -0,0 +1,27 @@
1
+ import { Session } from "node:inspector/promises";
2
+ import type { CoverageData, ScriptCoverage } from "./CoverageTypes.ts";
3
+
4
+ /** Run a function while collecting precise V8 coverage, return execution counts.
5
+ * The session passed to `fn` can be shared with TimeSampler to avoid resetting counters. */
6
+ export async function withCoverageProfiling<T>(
7
+ fn: (session: Session) => Promise<T> | T,
8
+ ): Promise<{ result: T; coverage: CoverageData }> {
9
+ const session = new Session();
10
+ session.connect();
11
+
12
+ try {
13
+ await session.post("Profiler.enable");
14
+ await session.post("Profiler.startPreciseCoverage", {
15
+ callCount: true,
16
+ detailed: true,
17
+ });
18
+ const result = await fn(session);
19
+ const raw = await session.post("Profiler.takePreciseCoverage");
20
+ const scripts = raw.result as unknown as ScriptCoverage[];
21
+ return { result, coverage: { scripts } };
22
+ } finally {
23
+ await session.post("Profiler.stopPreciseCoverage");
24
+ await session.post("Profiler.disable");
25
+ session.disconnect();
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ /** Per-function execution counts from V8 Profiler.takePreciseCoverage (Node and CDP). */
2
+ export interface CoverageData {
3
+ scripts: ScriptCoverage[];
4
+ }
5
+
6
+ /** Coverage data for a single script (file) */
7
+ export interface ScriptCoverage {
8
+ url: string;
9
+ functions: FunctionCoverage[];
10
+ }
11
+
12
+ /** Coverage data for a single function within a script */
13
+ export interface FunctionCoverage {
14
+ functionName: string;
15
+ ranges: CoverageRange[];
16
+ }
17
+
18
+ /** A byte-offset range within a function with its execution count */
19
+ export interface CoverageRange {
20
+ startOffset: number;
21
+ endOffset: number;
22
+ count: number;
23
+ }