benchforge 0.1.11 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
@@ -0,0 +1,133 @@
1
+ import type { GcStats } from "../../runners/GcStats.ts";
2
+ import type { CoverageData, ScriptCoverage } from "../node/CoverageTypes.ts";
3
+ import type { HeapProfile } from "../node/HeapSampler.ts";
4
+ import type { TimeProfile } from "../node/TimeSampler.ts";
5
+ import { browserGcStats } from "./BrowserGcStats.ts";
6
+ import type { CdpClient } from "./CdpClient.ts";
7
+ import type { TraceEvent } from "./ChromeTraceEvent.ts";
8
+
9
+ /** Options controlling which CDP instruments (heap, CPU, coverage) to enable. */
10
+ export interface InstrumentOpts {
11
+ alloc: boolean;
12
+ profile: boolean;
13
+ callCounts: boolean;
14
+ samplingInterval: number;
15
+ profileInterval?: number;
16
+ }
17
+
18
+ /** Build InstrumentOpts from profile params and heap sampling interval. */
19
+ export function instrumentOpts(
20
+ params: {
21
+ alloc?: boolean;
22
+ profile?: boolean;
23
+ callCounts?: boolean;
24
+ profileInterval?: number;
25
+ },
26
+ samplingInterval: number,
27
+ ): InstrumentOpts {
28
+ const {
29
+ alloc = false,
30
+ profile = false,
31
+ callCounts = false,
32
+ profileInterval,
33
+ } = params;
34
+ return { alloc, profile, callCounts, samplingInterval, profileInterval };
35
+ }
36
+
37
+ /** Start CDP GC tracing; returns the mutable array that collects trace events. */
38
+ export async function startGcTracing(cdp: CdpClient): Promise<TraceEvent[]> {
39
+ const events: TraceEvent[] = [];
40
+ cdp.on("Tracing.dataCollected", ({ value }) => {
41
+ events.push(...(value as unknown as TraceEvent[]));
42
+ });
43
+ await cdp.send("Tracing.start", {
44
+ traceConfig: { includedCategories: ["v8", "v8.gc"] },
45
+ });
46
+ return events;
47
+ }
48
+
49
+ /** End CDP tracing and aggregate collected events into GcStats. */
50
+ export async function collectTracing(
51
+ cdp: CdpClient,
52
+ traceEvents: TraceEvent[],
53
+ ): Promise<GcStats> {
54
+ const done = new Promise<void>(r =>
55
+ cdp.once("Tracing.tracingComplete", () => r()),
56
+ );
57
+ await cdp.send("Tracing.end");
58
+ await done;
59
+ return browserGcStats(traceEvents);
60
+ }
61
+
62
+ /** Start CDP Profiler for CPU time sampling (caller manages Profiler.enable/disable) */
63
+ export async function startTimeProfiling(
64
+ cdp: CdpClient,
65
+ interval?: number,
66
+ ): Promise<void> {
67
+ if (interval) await cdp.send("Profiler.setSamplingInterval", { interval });
68
+ await cdp.send("Profiler.start");
69
+ }
70
+
71
+ /** Stop CDP CPU sampling and return the profile. */
72
+ export async function stopTimeProfiling(cdp: CdpClient): Promise<TimeProfile> {
73
+ const { profile } = await cdp.send("Profiler.stop");
74
+ return profile as unknown as TimeProfile;
75
+ }
76
+
77
+ /** Start precise coverage (caller manages Profiler.enable/disable). */
78
+ export async function startCoverageCollection(cdp: CdpClient): Promise<void> {
79
+ await cdp.send("Profiler.startPreciseCoverage", {
80
+ callCount: true,
81
+ detailed: true,
82
+ });
83
+ }
84
+
85
+ /** Collect precise coverage, filtering out browser-internal scripts. */
86
+ export async function collectCoverage(cdp: CdpClient): Promise<CoverageData> {
87
+ const { result } = await cdp.send("Profiler.takePreciseCoverage");
88
+ await cdp.send("Profiler.stopPreciseCoverage");
89
+ const scripts = (result as unknown as ScriptCoverage[]).filter(isPageScript);
90
+ return { scripts };
91
+ }
92
+
93
+ /** Stop active instruments and return collected profiles/coverage. */
94
+ export async function stopInstruments(
95
+ cdp: CdpClient,
96
+ opts: InstrumentOpts,
97
+ ): Promise<{
98
+ heapProfile?: HeapProfile;
99
+ timeProfile?: TimeProfile;
100
+ coverage?: CoverageData;
101
+ }> {
102
+ const heapProfile = opts.alloc
103
+ ? ((await cdp.send("HeapProfiler.stopSampling")).profile as HeapProfile)
104
+ : undefined;
105
+ const timeProfile = opts.profile ? await stopTimeProfiling(cdp) : undefined;
106
+ const coverage = opts.callCounts ? await collectCoverage(cdp) : undefined;
107
+ if (opts.profile || opts.callCounts) await cdp.send("Profiler.disable");
108
+ return { heapProfile, timeProfile, coverage };
109
+ }
110
+
111
+ /** Start requested CDP instruments (heap, CPU, coverage). */
112
+ export async function startInstruments(
113
+ cdp: CdpClient,
114
+ opts: InstrumentOpts,
115
+ ): Promise<void> {
116
+ if (opts.alloc) {
117
+ await cdp.send("HeapProfiler.startSampling", {
118
+ samplingInterval: opts.samplingInterval,
119
+ includeObjectsCollectedByMajorGC: true,
120
+ includeObjectsCollectedByMinorGC: true,
121
+ });
122
+ }
123
+ if (opts.profile || opts.callCounts) await cdp.send("Profiler.enable");
124
+ if (opts.profile) await startTimeProfiling(cdp, opts.profileInterval);
125
+ if (opts.callCounts) await startCoverageCollection(cdp);
126
+ }
127
+
128
+ /** Exclude chrome:// and devtools:// internal scripts. */
129
+ function isPageScript(s: ScriptCoverage): boolean {
130
+ return (
131
+ !!s.url && !s.url.startsWith("chrome") && !s.url.startsWith("devtools")
132
+ );
133
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ aggregateGcStats,
3
+ type GcEvent,
4
+ type GcStats,
5
+ } from "../../runners/GcStats.ts";
6
+ import type { TraceEvent } from "./ChromeTraceEvent.ts";
7
+
8
+ /** Convert MinorGC/MajorGC trace events into GcEvent[]. */
9
+ export function parseGcTraceEvents(traceEvents: TraceEvent[]): GcEvent[] {
10
+ return traceEvents
11
+ .filter(e => e.ph === "X" && gcType(e.name))
12
+ .map(e => ({
13
+ type: gcType(e.name)!,
14
+ pauseMs: (e.dur ?? 0) / 1000,
15
+ collected: Math.max(
16
+ 0,
17
+ Number(e.args?.usedHeapSizeBefore ?? 0) -
18
+ Number(e.args?.usedHeapSizeAfter ?? 0),
19
+ ),
20
+ }));
21
+ }
22
+
23
+ /** Parse and aggregate CDP trace events into GcStats. */
24
+ export function browserGcStats(traceEvents: TraceEvent[]): GcStats {
25
+ return aggregateGcStats(parseGcTraceEvents(traceEvents));
26
+ }
27
+
28
+ /** Map CDP event names (MinorGC/MajorGC) to GcEvent type. */
29
+ function gcType(name: string): GcEvent["type"] | undefined {
30
+ if (name === "MinorGC") return "scavenge";
31
+ if (name === "MajorGC") return "mark-compact";
32
+ return undefined;
33
+ }
@@ -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
+ }