benchforge 0.1.9 → 0.1.11

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 (66) hide show
  1. package/README.md +40 -6
  2. package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
  3. package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
  4. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
  5. package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
  6. package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
  7. package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
  8. package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
  9. package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
  10. package/dist/bin/benchforge.mjs +1 -1
  11. package/dist/browser/index.js +210 -210
  12. package/dist/index.d.mts +102 -46
  13. package/dist/index.mjs +3 -3
  14. package/dist/runners/WorkerScript.d.mts +1 -1
  15. package/dist/runners/WorkerScript.mjs +66 -66
  16. package/dist/runners/WorkerScript.mjs.map +1 -1
  17. package/dist/{src-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
  18. package/dist/src-B-DDaCa9.mjs.map +1 -0
  19. package/package.json +2 -1
  20. package/src/BenchMatrix.ts +125 -125
  21. package/src/BenchmarkReport.ts +50 -45
  22. package/src/HtmlDataPrep.ts +21 -21
  23. package/src/PermutationTest.ts +24 -24
  24. package/src/StandardSections.ts +45 -45
  25. package/src/StatisticalUtils.ts +60 -61
  26. package/src/browser/BrowserGcStats.ts +5 -5
  27. package/src/browser/BrowserHeapSampler.ts +63 -63
  28. package/src/cli/CliArgs.ts +6 -3
  29. package/src/cli/FilterBenchmarks.ts +5 -5
  30. package/src/cli/RunBenchCLI.ts +526 -498
  31. package/src/export/JsonExport.ts +10 -10
  32. package/src/export/PerfettoExport.ts +74 -74
  33. package/src/export/SpeedscopeExport.ts +202 -0
  34. package/src/heap-sample/HeapSampleReport.ts +143 -70
  35. package/src/heap-sample/HeapSampler.ts +55 -12
  36. package/src/heap-sample/ResolvedProfile.ts +89 -0
  37. package/src/html/HtmlReport.ts +33 -33
  38. package/src/html/HtmlTemplate.ts +67 -67
  39. package/src/html/browser/CIPlot.ts +50 -50
  40. package/src/html/browser/HistogramKde.ts +13 -13
  41. package/src/html/browser/LegendUtils.ts +48 -48
  42. package/src/html/browser/RenderPlots.ts +98 -98
  43. package/src/html/browser/SampleTimeSeries.ts +79 -79
  44. package/src/index.ts +6 -0
  45. package/src/matrix/MatrixFilter.ts +6 -6
  46. package/src/matrix/MatrixReport.ts +96 -96
  47. package/src/matrix/VariantLoader.ts +5 -5
  48. package/src/runners/AdaptiveWrapper.ts +151 -151
  49. package/src/runners/BasicRunner.ts +175 -175
  50. package/src/runners/BenchRunner.ts +8 -8
  51. package/src/runners/GcStats.ts +22 -22
  52. package/src/runners/RunnerOrchestrator.ts +168 -168
  53. package/src/runners/WorkerScript.ts +96 -96
  54. package/src/table-util/Formatters.ts +41 -36
  55. package/src/table-util/TableReport.ts +122 -122
  56. package/src/table-util/test/TableValueExtractor.ts +9 -9
  57. package/src/test/AdaptiveStatistics.integration.ts +7 -39
  58. package/src/test/HeapAttribution.test.ts +51 -0
  59. package/src/test/RunBenchCLI.test.ts +18 -18
  60. package/src/test/TestUtils.ts +24 -24
  61. package/src/tests/BenchMatrix.test.ts +12 -12
  62. package/src/tests/MatrixFilter.test.ts +15 -15
  63. package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
  64. package/dist/GcStats-ByEovUi1.mjs.map +0 -1
  65. package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
  66. package/dist/src-Cf_LXwlp.mjs.map +0 -1
package/README.md CHANGED
@@ -1,10 +1,23 @@
1
1
  # Benchforge
2
2
 
3
- A TypeScript benchmarking library with CLI support for running performance tests.
3
+ Traditional benchmarking tools either ignore GC or try to avoid it.
4
+ Benchforge captures GC impact.
4
5
 
5
- ## Browser Profiling
6
+ Garbage collection makes benchmarks noisy — statistics like mean and max
7
+ stabilize poorly when collection is intermittent. Most tools work around
8
+ this by isolating microbenchmarks from GC, but that hides a key part of
9
+ real-world performance. And heap snapshots are useful for finding leaks,
10
+ but they can't show you where garbage is being generated.
6
11
 
7
- See [Browser Heap Profiling](README-browser.md) for profiling code running in a browser.
12
+ - **Heap allocation profiling** attribute allocations to call sites, including short-lived objects already collected by GC.
13
+ - **GC-aware statistics** — bootstrap confidence intervals and baseline comparison that account for GC variance instead of hiding it.
14
+ - **GC collection reports** — allocation rates, scavenge/full GC counts, promotion %, and pause times per iteration.
15
+
16
+ Also:
17
+ - **Zero-config CLI** — export a function, run `benchforge file.ts`.
18
+ - **Multiple export formats** — HTML reports, Perfetto traces, Speedscope flame charts, JSON.
19
+ - **Worker isolation** — node benchmarks run in child processes by default.
20
+ - **Browser support** — benchmark in Chromium via [Playwright + CDP](README-browser.md).
8
21
 
9
22
  ## Installation
10
23
 
@@ -59,6 +72,8 @@ export default suite;
59
72
  benchforge sorting.ts --gc-stats
60
73
  ```
61
74
 
75
+ A `MatrixSuite` export (`.matrices`) is also recognized and runs via `matrixBenchExports`.
76
+
62
77
  See `examples/simple-cli.ts` for a complete runnable example.
63
78
 
64
79
  ### Worker Mode with Module Imports
@@ -108,7 +123,9 @@ This eliminates manual caching boilerplate in worker modules.
108
123
  - `--html` - Generate HTML report, start server, and open in browser
109
124
  - `--export-html <file>` - Export HTML report to file
110
125
  - `--json <file>` - Export benchmark data to JSON
111
- - `--perfetto <file>` - Export Perfetto trace file
126
+ - `--export-perfetto <file>` - Export Perfetto trace file
127
+ - `--speedscope` - Open heap profile in speedscope viewer (via npx)
128
+ - `--export-speedscope <file>` - Export heap profile as speedscope JSON
112
129
 
113
130
  ## CLI Usage
114
131
 
@@ -175,11 +192,11 @@ Export benchmark data as a Perfetto-compatible trace file for detailed analysis:
175
192
 
176
193
  ```bash
177
194
  # Export trace file
178
- benchforge my-bench.ts --perfetto trace.json
195
+ benchforge my-bench.ts --export-perfetto trace.json
179
196
 
180
197
  # With V8 GC events (automatically merged after exit)
181
198
  node --expose-gc --trace-events-enabled --trace-event-categories=v8,v8.gc \
182
- benchforge my-bench.ts --perfetto trace.json
199
+ benchforge my-bench.ts --export-perfetto trace.json
183
200
  ```
184
201
 
185
202
  View the trace at https://ui.perfetto.dev by dragging the JSON file.
@@ -190,6 +207,20 @@ The trace includes:
190
207
  - **Pause markers**: V8 optimization pause points
191
208
  - **V8 GC events**: Automatically merged after process exit (when run with `--trace-events-enabled`)
192
209
 
210
+ ### Speedscope Export
211
+
212
+ View heap allocation profiles as flame charts in speedscope:
213
+
214
+ ```bash
215
+ # Open directly in speedscope (launches via npx)
216
+ benchforge my-bench.ts --heap-sample --speedscope
217
+
218
+ # Export to file
219
+ benchforge my-bench.ts --heap-sample --export-speedscope profile.json
220
+ ```
221
+
222
+ Each benchmark with a heap profile becomes a separate speedscope profile, with samples ordered temporally and weighted by allocation size in bytes.
223
+
193
224
  ### GC Statistics
194
225
 
195
226
  Collect detailed garbage collection statistics via V8's `--trace-gc-nvp`:
@@ -231,6 +262,8 @@ benchforge my-bench.ts --heap-sample --heap-stack 5
231
262
  - `--heap-rows <n>` - Number of top allocation sites to show (default: 20)
232
263
  - `--heap-stack <n>` - Call stack depth to display (default: 3)
233
264
  - `--heap-verbose` - Show full file:// paths with line numbers (cmd-clickable)
265
+ - `--heap-raw` - Dump every raw heap sample (ordinal, size, stack)
266
+ - `--heap-user-only` - Filter to user code only (hide node internals)
234
267
 
235
268
  **Output (default compact):**
236
269
  ```
@@ -261,6 +294,7 @@ V8's sampling profiler uses Poisson-distributed sampling. When an allocation occ
261
294
 
262
295
  **Limitations:**
263
296
  - **Function-level attribution only**: V8 reports the function where allocation occurred, not the specific line. The line:column shown is where the function is *defined*.
297
+ - **Inlining shifts attribution**: V8 may inline a function into its caller, causing allocations to be reported against the caller instead. If attribution looks wrong, disable inlining to isolate: `node --js-flags='--no-turbo-inlining --no-maglev-inlining' benchforge ...` (or `--jitless` to disable JIT entirely, though this changes performance characteristics).
264
298
  - **Statistical sampling**: Results vary between runs. More iterations = more stable results.
265
299
  - **~50% filtered**: Node.js internals account for roughly half of allocations. Use "Total (all)" to see the full picture.
266
300
 
@@ -1,17 +1,41 @@
1
1
  //#region src/heap-sample/HeapSampler.d.ts
2
+ /** V8 call frame location within a profiled script */
3
+ interface CallFrame {
4
+ /** Function name (empty string for anonymous) */
5
+ functionName: string;
6
+ /** Script URL or file path */
7
+ url: string;
8
+ /** Zero-based line number */
9
+ lineNumber: number;
10
+ /** Zero-based column number */
11
+ columnNumber?: number;
12
+ }
13
+ /** Node in the V8 sampling heap profile tree */
2
14
  interface ProfileNode {
3
- callFrame: {
4
- functionName: string;
5
- url: string;
6
- lineNumber: number;
7
- columnNumber?: number;
8
- };
15
+ /** Call site for this allocation node */
16
+ callFrame: CallFrame;
17
+ /** Bytes allocated directly at this node (not children) */
9
18
  selfSize: number;
19
+ /** Unique node ID, links to {@link HeapSample.nodeId} */
20
+ id: number;
21
+ /** Child nodes in the call tree */
10
22
  children?: ProfileNode[];
11
23
  }
24
+ /** Individual heap allocation sample from V8's SamplingHeapProfiler */
25
+ interface HeapSample {
26
+ /** Links to {@link ProfileNode.id} for stack lookup */
27
+ nodeId: number;
28
+ /** Allocation size in bytes */
29
+ size: number;
30
+ /** Monotonically increasing, gives temporal ordering */
31
+ ordinal: number;
32
+ }
33
+ /** V8 sampling heap profile tree with optional per-allocation samples */
12
34
  interface HeapProfile {
35
+ /** Root of the profile call tree */
13
36
  head: ProfileNode;
14
- samples?: number[];
37
+ /** Per-allocation samples, if collected */
38
+ samples?: HeapSample[];
15
39
  }
16
40
  //#endregion
17
41
  //#region src/NodeGC.d.ts
@@ -222,4 +246,4 @@ interface RunnerOptions {
222
246
  }
223
247
  //#endregion
224
248
  export { MeasuredResults as a, BenchmarkSpec as i, BenchGroup as n, HeapProfile as o, BenchSuite as r, RunnerOptions as t };
225
- //# sourceMappingURL=BenchRunner-CSKN9zPy.d.mts.map
249
+ //# sourceMappingURL=BenchRunner-BzyUfiyB.d.mts.map
@@ -1,4 +1,4 @@
1
- import { t as aggregateGcStats } from "./GcStats-ByEovUi1.mjs";
1
+ import { t as aggregateGcStats } from "./GcStats-wX7Xyblu.mjs";
2
2
  import { chromium } from "playwright";
3
3
 
4
4
  //#region src/browser/BrowserGcStats.ts
@@ -18,14 +18,14 @@ function parseGcTraceEvents(traceEvents) {
18
18
  }];
19
19
  });
20
20
  }
21
- function gcType(name) {
22
- if (name === "MinorGC") return "scavenge";
23
- if (name === "MajorGC") return "mark-compact";
24
- }
25
21
  /** Parse CDP trace events and aggregate into GcStats */
26
22
  function browserGcStats(traceEvents) {
27
23
  return aggregateGcStats(parseGcTraceEvents(traceEvents));
28
24
  }
25
+ function gcType(name) {
26
+ if (name === "MinorGC") return "scavenge";
27
+ if (name === "MajorGC") return "mark-compact";
28
+ }
29
29
 
30
30
  //#endregion
31
31
  //#region src/browser/BrowserHeapSampler.ts
@@ -71,6 +71,27 @@ async function profileBrowser(params) {
71
71
  await server.close();
72
72
  }
73
73
  }
74
+ /** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */
75
+ function pipeChromeOutput(server) {
76
+ const proc = server.process();
77
+ const pipe = (stream) => stream?.on("data", (chunk) => {
78
+ for (const line of chunk.toString().split("\n")) {
79
+ const text = line.trim();
80
+ if (text) process.stderr.write(`[chrome] ${text}\n`);
81
+ }
82
+ });
83
+ pipe(proc.stdout);
84
+ pipe(proc.stderr);
85
+ }
86
+ /** Start CDP GC tracing, returns the event collector array. */
87
+ async function startGcTracing(cdp) {
88
+ const events = [];
89
+ cdp.on("Tracing.dataCollected", ({ value }) => {
90
+ for (const e of value) events.push(e);
91
+ });
92
+ await cdp.send("Tracing.start", { traceConfig: { includedCategories: ["v8", "v8.gc"] } });
93
+ return events;
94
+ }
74
95
  /** Inject __start/__lap as in-page functions, expose __done for results collection.
75
96
  * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
76
97
  * instrument start. __done() stops instruments and collects timing data. */
@@ -104,47 +125,6 @@ async function setupLapMode(page, cdp, params, samplingInterval, timeout, pageEr
104
125
  cancel: () => clearTimeout(timer)
105
126
  };
106
127
  }
107
- /** In-page timing functions injected via addInitScript (zero CDP overhead).
108
- * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
109
- function injectLapFunctions() {
110
- const g = globalThis;
111
- g.__benchSamples = [];
112
- g.__benchLastTime = 0;
113
- g.__benchFirstStart = 0;
114
- g.__start = () => {
115
- const now = performance.now();
116
- g.__benchLastTime = now;
117
- if (!g.__benchFirstStart) {
118
- g.__benchFirstStart = now;
119
- return g.__benchInstrumentStart();
120
- }
121
- };
122
- g.__lap = () => {
123
- const now = performance.now();
124
- g.__benchSamples.push(now - g.__benchLastTime);
125
- g.__benchLastTime = now;
126
- };
127
- g.__done = () => {
128
- const wall = g.__benchFirstStart ? performance.now() - g.__benchFirstStart : 0;
129
- return g.__benchCollect(g.__benchSamples.slice(), wall);
130
- };
131
- }
132
- function heapSamplingParams(samplingInterval) {
133
- return {
134
- samplingInterval,
135
- includeObjectsCollectedByMajorGC: true,
136
- includeObjectsCollectedByMinorGC: true
137
- };
138
- }
139
- /** Start CDP GC tracing, returns the event collector array. */
140
- async function startGcTracing(cdp) {
141
- const events = [];
142
- cdp.on("Tracing.dataCollected", ({ value }) => {
143
- for (const e of value) events.push(e);
144
- });
145
- await cdp.send("Tracing.start", { traceConfig: { includedCategories: ["v8", "v8.gc"] } });
146
- return events;
147
- }
148
128
  /** Bench function mode: run window.__bench in a timed iteration loop. */
149
129
  async function runBenchLoop(page, cdp, params, samplingInterval) {
150
130
  const { heapSample } = params;
@@ -184,19 +164,39 @@ async function collectTracing(cdp, traceEvents) {
184
164
  await complete;
185
165
  return browserGcStats(traceEvents);
186
166
  }
187
- /** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */
188
- function pipeChromeOutput(server) {
189
- const proc = server.process();
190
- const pipe = (stream) => stream?.on("data", (chunk) => {
191
- for (const line of chunk.toString().split("\n")) {
192
- const text = line.trim();
193
- if (text) process.stderr.write(`[chrome] ${text}\n`);
167
+ function heapSamplingParams(samplingInterval) {
168
+ return {
169
+ samplingInterval,
170
+ includeObjectsCollectedByMajorGC: true,
171
+ includeObjectsCollectedByMinorGC: true
172
+ };
173
+ }
174
+ /** In-page timing functions injected via addInitScript (zero CDP overhead).
175
+ * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
176
+ function injectLapFunctions() {
177
+ const g = globalThis;
178
+ g.__benchSamples = [];
179
+ g.__benchLastTime = 0;
180
+ g.__benchFirstStart = 0;
181
+ g.__start = () => {
182
+ const now = performance.now();
183
+ g.__benchLastTime = now;
184
+ if (!g.__benchFirstStart) {
185
+ g.__benchFirstStart = now;
186
+ return g.__benchInstrumentStart();
194
187
  }
195
- });
196
- pipe(proc.stdout);
197
- pipe(proc.stderr);
188
+ };
189
+ g.__lap = () => {
190
+ const now = performance.now();
191
+ g.__benchSamples.push(now - g.__benchLastTime);
192
+ g.__benchLastTime = now;
193
+ };
194
+ g.__done = () => {
195
+ const wall = g.__benchFirstStart ? performance.now() - g.__benchFirstStart : 0;
196
+ return g.__benchCollect(g.__benchSamples.slice(), wall);
197
+ };
198
198
  }
199
199
 
200
200
  //#endregion
201
201
  export { profileBrowser, profileBrowser as profileBrowserHeap };
202
- //# sourceMappingURL=BrowserHeapSampler-DCeL42RE.mjs.map
202
+ //# sourceMappingURL=BrowserHeapSampler-B6asLKWQ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserHeapSampler-B6asLKWQ.mjs","names":[],"sources":["../src/browser/BrowserGcStats.ts","../src/browser/BrowserHeapSampler.ts"],"sourcesContent":["import {\n aggregateGcStats,\n type GcEvent,\n type GcStats,\n} from \"../runners/GcStats.ts\";\n\n/** CDP trace event from Tracing.dataCollected */\nexport interface TraceEvent {\n cat: string;\n name: string;\n ph: string;\n dur?: number; // microseconds\n args?: Record<string, any>;\n}\n\n/** Parse CDP trace events (MinorGC/MajorGC) into GcEvent[] */\nexport function parseGcTraceEvents(traceEvents: TraceEvent[]): GcEvent[] {\n return traceEvents.flatMap(e => {\n if (e.ph !== \"X\") return [];\n const type = gcType(e.name);\n if (!type) return [];\n const durUs = e.dur ?? 0;\n const heapBefore: number = e.args?.usedHeapSizeBefore ?? 0;\n const heapAfter: number = e.args?.usedHeapSizeAfter ?? 0;\n return [\n {\n type,\n pauseMs: durUs / 1000,\n collected: Math.max(0, heapBefore - heapAfter),\n },\n ];\n });\n}\n\n/** Parse CDP trace events and aggregate into GcStats */\nexport function browserGcStats(traceEvents: TraceEvent[]): GcStats {\n return aggregateGcStats(parseGcTraceEvents(traceEvents));\n}\n\nfunction gcType(name: string): GcEvent[\"type\"] | undefined {\n if (name === \"MinorGC\") return \"scavenge\";\n if (name === \"MajorGC\") return \"mark-compact\";\n return undefined;\n}\n","import {\n type BrowserServer,\n type CDPSession,\n chromium,\n type Page,\n} from \"playwright\";\nimport type {\n HeapProfile,\n HeapSampleOptions,\n} from \"../heap-sample/HeapSampler.ts\";\nimport type { GcStats } from \"../runners/GcStats.ts\";\nimport { browserGcStats, type TraceEvent } from \"./BrowserGcStats.ts\";\n\nexport interface BrowserProfileParams {\n url: string;\n heapSample?: boolean;\n heapOptions?: HeapSampleOptions;\n gcStats?: boolean;\n headless?: boolean;\n chromeArgs?: string[];\n timeout?: number; // seconds\n maxTime?: number; // ms, bench function iteration time limit\n maxIterations?: number; // exact iteration count (bench function mode)\n}\n\nexport interface BrowserProfileResult {\n heapProfile?: HeapProfile;\n gcStats?: GcStats;\n /** Wall-clock ms (lap mode: first start to done, bench function: total loop) */\n wallTimeMs?: number;\n /** Per-iteration timing samples (ms) from bench function or lap mode */\n samples?: number[];\n}\n\ninterface LapModeHandle {\n promise: Promise<BrowserProfileResult>;\n cancel: () => void;\n}\n\n/** Run browser benchmark, auto-detecting page API mode.\n * Bench function (window.__bench): CLI controls iteration and timing.\n * Lap mode (__start/__lap/__done): page controls the measured region. */\nexport async function profileBrowser(\n params: BrowserProfileParams,\n): Promise<BrowserProfileResult> {\n const { url, headless = true, chromeArgs, timeout = 60 } = params;\n const { gcStats: collectGc } = params;\n const { samplingInterval = 32768 } = params.heapOptions ?? {};\n\n const server = await chromium.launchServer({ headless, args: chromeArgs });\n pipeChromeOutput(server);\n const browser = await chromium.connect(server.wsEndpoint());\n try {\n const page = await browser.newPage();\n page.setDefaultTimeout(timeout * 1000);\n const cdp = await page.context().newCDPSession(page);\n\n const pageErrors: string[] = [];\n page.on(\"pageerror\", err => pageErrors.push(err.message));\n\n const traceEvents = collectGc ? await startGcTracing(cdp) : [];\n const lapMode = await setupLapMode(\n page,\n cdp,\n params,\n samplingInterval,\n timeout,\n pageErrors,\n );\n\n await page.goto(url, { waitUntil: \"load\" });\n const hasBench = await page.evaluate(\n () => typeof (globalThis as any).__bench === \"function\",\n );\n\n let result: BrowserProfileResult;\n if (hasBench) {\n lapMode.cancel();\n lapMode.promise.catch(() => {}); // suppress unused rejection\n result = await runBenchLoop(page, cdp, params, samplingInterval);\n } else {\n result = await lapMode.promise;\n lapMode.cancel();\n }\n\n if (collectGc) {\n result = { ...result, gcStats: await collectTracing(cdp, traceEvents) };\n }\n return result;\n } finally {\n await browser.close();\n await server.close();\n }\n}\n\n/** Forward Chrome's stdout/stderr to the terminal so V8 flag output is visible. */\nfunction pipeChromeOutput(server: BrowserServer): void {\n const proc = server.process();\n const pipe = (stream: NodeJS.ReadableStream | null) =>\n stream?.on(\"data\", (chunk: Buffer) => {\n for (const line of chunk.toString().split(\"\\n\")) {\n const text = line.trim();\n if (text) process.stderr.write(`[chrome] ${text}\\n`);\n }\n });\n pipe(proc.stdout);\n pipe(proc.stderr);\n}\n\n/** Start CDP GC tracing, returns the event collector array. */\nasync function startGcTracing(cdp: CDPSession): Promise<TraceEvent[]> {\n const events: TraceEvent[] = [];\n cdp.on(\"Tracing.dataCollected\", ({ value }) => {\n for (const e of value) events.push(e as unknown as TraceEvent);\n });\n await cdp.send(\"Tracing.start\", {\n traceConfig: { includedCategories: [\"v8\", \"v8.gc\"] },\n });\n return events;\n}\n\n/** Inject __start/__lap as in-page functions, expose __done for results collection.\n * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers\n * instrument start. __done() stops instruments and collects timing data. */\nasync function setupLapMode(\n page: Page,\n cdp: CDPSession,\n params: BrowserProfileParams,\n samplingInterval: number,\n timeout: number,\n pageErrors: string[],\n): Promise<LapModeHandle> {\n const { heapSample } = params;\n const { promise, resolve, reject } =\n Promise.withResolvers<BrowserProfileResult>();\n let instrumentsStarted = false;\n\n await page.exposeFunction(\"__benchInstrumentStart\", async () => {\n if (instrumentsStarted) return;\n instrumentsStarted = true;\n if (heapSample) {\n await cdp.send(\n \"HeapProfiler.startSampling\",\n heapSamplingParams(samplingInterval),\n );\n }\n });\n\n await page.exposeFunction(\n \"__benchCollect\",\n async (samples: number[], wallTimeMs: number) => {\n let heapProfile: HeapProfile | undefined;\n if (heapSample && instrumentsStarted) {\n const result = await cdp.send(\"HeapProfiler.stopSampling\");\n heapProfile = result.profile as unknown as HeapProfile;\n }\n resolve({ samples, heapProfile, wallTimeMs });\n },\n );\n\n await page.addInitScript(injectLapFunctions);\n\n const timer = setTimeout(() => {\n const lines = [`Timed out after ${timeout}s`];\n if (pageErrors.length) {\n lines.push(\"Page JS errors:\", ...pageErrors.map(e => ` ${e}`));\n } else {\n lines.push(\"Page did not call __done() or define window.__bench\");\n }\n reject(new Error(lines.join(\"\\n\")));\n }, timeout * 1000);\n\n return { promise, cancel: () => clearTimeout(timer) };\n}\n\n/** Bench function mode: run window.__bench in a timed iteration loop. */\nasync function runBenchLoop(\n page: Page,\n cdp: CDPSession,\n params: BrowserProfileParams,\n samplingInterval: number,\n): Promise<BrowserProfileResult> {\n const { heapSample } = params;\n const maxTime = params.maxTime ?? 642;\n const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;\n\n if (heapSample) {\n await cdp.send(\n \"HeapProfiler.startSampling\",\n heapSamplingParams(samplingInterval),\n );\n }\n\n const { samples, totalMs } = await page.evaluate(\n async ({ maxTime, maxIter }) => {\n const bench = (globalThis as any).__bench;\n const samples: number[] = [];\n const startAll = performance.now();\n const deadline = startAll + maxTime;\n for (let i = 0; i < maxIter && performance.now() < deadline; i++) {\n const t0 = performance.now();\n await bench();\n samples.push(performance.now() - t0);\n }\n return { samples, totalMs: performance.now() - startAll };\n },\n { maxTime, maxIter },\n );\n\n let heapProfile: HeapProfile | undefined;\n if (heapSample) {\n const result = await cdp.send(\"HeapProfiler.stopSampling\");\n heapProfile = result.profile as unknown as HeapProfile;\n }\n\n return { samples, heapProfile, wallTimeMs: totalMs };\n}\n\n/** Stop CDP tracing and parse GC events into GcStats. */\nasync function collectTracing(\n cdp: CDPSession,\n traceEvents: TraceEvent[],\n): Promise<GcStats> {\n const complete = new Promise<void>(resolve =>\n cdp.once(\"Tracing.tracingComplete\", () => resolve()),\n );\n await cdp.send(\"Tracing.end\");\n await complete;\n return browserGcStats(traceEvents);\n}\n\nfunction heapSamplingParams(samplingInterval: number) {\n return {\n samplingInterval,\n includeObjectsCollectedByMajorGC: true,\n includeObjectsCollectedByMinorGC: true,\n };\n}\n\n/** In-page timing functions injected via addInitScript (zero CDP overhead).\n * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */\nfunction injectLapFunctions(): void {\n const g = globalThis as any;\n g.__benchSamples = [];\n g.__benchLastTime = 0;\n g.__benchFirstStart = 0;\n\n g.__start = () => {\n const now = performance.now();\n g.__benchLastTime = now;\n if (!g.__benchFirstStart) {\n g.__benchFirstStart = now;\n return g.__benchInstrumentStart();\n }\n };\n\n g.__lap = () => {\n const now = performance.now();\n g.__benchSamples.push(now - g.__benchLastTime);\n g.__benchLastTime = now;\n };\n\n g.__done = () => {\n const wall = g.__benchFirstStart\n ? performance.now() - g.__benchFirstStart\n : 0;\n return g.__benchCollect(g.__benchSamples.slice(), wall);\n };\n}\n\nexport { profileBrowser as profileBrowserHeap };\n"],"mappings":"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"}
@@ -23,20 +23,6 @@ function parseGcLine(line) {
23
23
  survived
24
24
  };
25
25
  }
26
- /** Parse name=value pairs from trace-gc-nvp line */
27
- function parseNvpFields(line) {
28
- const fields = {};
29
- const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
30
- for (const [, key, value] of matches) fields[key] = value;
31
- return fields;
32
- }
33
- /** Map V8 gc type codes to our types */
34
- function parseGcType(gcField) {
35
- if (gcField === "s" || gcField === "scavenge") return "scavenge";
36
- if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact") return "mark-compact";
37
- if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms") return "minor-ms";
38
- return "unknown";
39
- }
40
26
  /** Aggregate GC events into summary stats */
41
27
  function aggregateGcStats(events) {
42
28
  let scavenges = 0;
@@ -71,7 +57,21 @@ function aggregateGcStats(events) {
71
57
  }
72
58
  };
73
59
  }
60
+ /** Parse name=value pairs from trace-gc-nvp line */
61
+ function parseNvpFields(line) {
62
+ const fields = {};
63
+ const matches = line.matchAll(/(\w+)=([^\s,]+)/g);
64
+ for (const [, key, value] of matches) fields[key] = value;
65
+ return fields;
66
+ }
67
+ /** Map V8 gc type codes to our types */
68
+ function parseGcType(gcField) {
69
+ if (gcField === "s" || gcField === "scavenge") return "scavenge";
70
+ if (gcField === "mc" || gcField === "ms" || gcField === "mark-compact") return "mark-compact";
71
+ if (gcField === "mmc" || gcField === "minor-mc" || gcField === "minor-ms") return "minor-ms";
72
+ return "unknown";
73
+ }
74
74
 
75
75
  //#endregion
76
76
  export { parseGcLine as n, aggregateGcStats as t };
77
- //# sourceMappingURL=GcStats-ByEovUi1.mjs.map
77
+ //# sourceMappingURL=GcStats-wX7Xyblu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GcStats-wX7Xyblu.mjs","names":[],"sources":["../src/runners/GcStats.ts"],"sourcesContent":["/** GC statistics aggregated from V8 trace events.\n * Node (--trace-gc-nvp) provides all fields.\n * Browser (CDP Tracing) provides counts, collected, and pause only. */\nexport interface GcStats {\n scavenges: number;\n markCompacts: number;\n totalCollected: number; // bytes freed\n gcPauseTime: number; // total pause time (ms)\n totalAllocated?: number; // bytes allocated (Node only)\n totalPromoted?: number; // bytes promoted to old gen (Node only)\n totalSurvived?: number; // bytes survived in young gen (Node only)\n}\n\n/** Single GC event. Node provides all fields; browser provides type, pauseMs, collected. */\nexport interface GcEvent {\n type: \"scavenge\" | \"mark-compact\" | \"minor-ms\" | \"unknown\";\n pauseMs: number;\n collected: number;\n allocated?: number; // Node only\n promoted?: number; // Node only\n survived?: number; // Node only\n}\n\n/** Parse a single --trace-gc-nvp stderr line */\nexport function parseGcLine(line: string): GcEvent | undefined {\n // V8 format: [pid:addr:gen] N ms: pause=X gc=s ... allocated=N promoted=N ...\n if (!line.includes(\"pause=\")) return undefined;\n\n const fields = parseNvpFields(line);\n if (!fields.gc) return undefined;\n\n const int = (k: string) => Number.parseInt(fields[k] || \"0\", 10);\n const type = parseGcType(fields.gc);\n const pauseMs = Number.parseFloat(fields.pause || \"0\");\n const allocated = int(\"allocated\");\n const promoted = int(\"promoted\");\n // V8 uses \"new_space_survived\" not \"survived\"\n const survived = int(\"new_space_survived\") || int(\"survived\");\n // Calculate collected from start/end object size if available\n const startSize = int(\"start_object_size\");\n const endSize = int(\"end_object_size\");\n const collected = startSize > endSize ? startSize - endSize : 0;\n\n if (Number.isNaN(pauseMs)) return undefined;\n\n return { type, pauseMs, allocated, collected, promoted, survived };\n}\n\n/** Aggregate GC events into summary stats */\nexport function aggregateGcStats(events: GcEvent[]): GcStats {\n let scavenges = 0;\n let markCompacts = 0;\n let gcPauseTime = 0;\n let totalCollected = 0;\n let hasNodeFields = false;\n let totalAllocated = 0;\n let totalPromoted = 0;\n let totalSurvived = 0;\n\n for (const e of events) {\n if (e.type === \"scavenge\" || e.type === \"minor-ms\") scavenges++;\n else if (e.type === \"mark-compact\") markCompacts++;\n gcPauseTime += e.pauseMs;\n totalCollected += e.collected;\n if (e.allocated != null) {\n hasNodeFields = true;\n totalAllocated += e.allocated;\n totalPromoted += e.promoted ?? 0;\n totalSurvived += e.survived ?? 0;\n }\n }\n\n return {\n scavenges,\n markCompacts,\n totalCollected,\n gcPauseTime,\n ...(hasNodeFields && { totalAllocated, totalPromoted, totalSurvived }),\n };\n}\n\n/** @return GcStats with all counters zeroed */\nexport function emptyGcStats(): GcStats {\n return { scavenges: 0, markCompacts: 0, totalCollected: 0, gcPauseTime: 0 };\n}\n\n/** Parse name=value pairs from trace-gc-nvp line */\nfunction parseNvpFields(line: string): Record<string, string> {\n const fields: Record<string, string> = {};\n // Format: \"key=value, key=value, ...\" or \"key=value key=value\"\n const matches = line.matchAll(/(\\w+)=([^\\s,]+)/g);\n for (const [, key, value] of matches) {\n fields[key] = value;\n }\n return fields;\n}\n\n/** Map V8 gc type codes to our types */\nfunction parseGcType(gcField: string): GcEvent[\"type\"] {\n // V8 uses: s=scavenge, mc=mark-compact, mmc=minor-mc (young gen mark-compact)\n if (gcField === \"s\" || gcField === \"scavenge\") return \"scavenge\";\n if (gcField === \"mc\" || gcField === \"ms\" || gcField === \"mark-compact\")\n return \"mark-compact\";\n if (gcField === \"mmc\" || gcField === \"minor-mc\" || gcField === \"minor-ms\")\n return \"minor-ms\";\n return \"unknown\";\n}\n"],"mappings":";;AAwBA,SAAgB,YAAY,MAAmC;AAE7D,KAAI,CAAC,KAAK,SAAS,SAAS,CAAE,QAAO;CAErC,MAAM,SAAS,eAAe,KAAK;AACnC,KAAI,CAAC,OAAO,GAAI,QAAO;CAEvB,MAAM,OAAO,MAAc,OAAO,SAAS,OAAO,MAAM,KAAK,GAAG;CAChE,MAAM,OAAO,YAAY,OAAO,GAAG;CACnC,MAAM,UAAU,OAAO,WAAW,OAAO,SAAS,IAAI;CACtD,MAAM,YAAY,IAAI,YAAY;CAClC,MAAM,WAAW,IAAI,WAAW;CAEhC,MAAM,WAAW,IAAI,qBAAqB,IAAI,IAAI,WAAW;CAE7D,MAAM,YAAY,IAAI,oBAAoB;CAC1C,MAAM,UAAU,IAAI,kBAAkB;CACtC,MAAM,YAAY,YAAY,UAAU,YAAY,UAAU;AAE9D,KAAI,OAAO,MAAM,QAAQ,CAAE,QAAO;AAElC,QAAO;EAAE;EAAM;EAAS;EAAW;EAAW;EAAU;EAAU;;;AAIpE,SAAgB,iBAAiB,QAA4B;CAC3D,IAAI,YAAY;CAChB,IAAI,eAAe;CACnB,IAAI,cAAc;CAClB,IAAI,iBAAiB;CACrB,IAAI,gBAAgB;CACpB,IAAI,iBAAiB;CACrB,IAAI,gBAAgB;CACpB,IAAI,gBAAgB;AAEpB,MAAK,MAAM,KAAK,QAAQ;AACtB,MAAI,EAAE,SAAS,cAAc,EAAE,SAAS,WAAY;WAC3C,EAAE,SAAS,eAAgB;AACpC,iBAAe,EAAE;AACjB,oBAAkB,EAAE;AACpB,MAAI,EAAE,aAAa,MAAM;AACvB,mBAAgB;AAChB,qBAAkB,EAAE;AACpB,oBAAiB,EAAE,YAAY;AAC/B,oBAAiB,EAAE,YAAY;;;AAInC,QAAO;EACL;EACA;EACA;EACA;EACA,GAAI,iBAAiB;GAAE;GAAgB;GAAe;GAAe;EACtE;;;AASH,SAAS,eAAe,MAAsC;CAC5D,MAAM,SAAiC,EAAE;CAEzC,MAAM,UAAU,KAAK,SAAS,mBAAmB;AACjD,MAAK,MAAM,GAAG,KAAK,UAAU,QAC3B,QAAO,OAAO;AAEhB,QAAO;;;AAIT,SAAS,YAAY,SAAkC;AAErD,KAAI,YAAY,OAAO,YAAY,WAAY,QAAO;AACtD,KAAI,YAAY,QAAQ,YAAY,QAAQ,YAAY,eACtD,QAAO;AACT,KAAI,YAAY,SAAS,YAAY,cAAc,YAAY,WAC7D,QAAO;AACT,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"HeapSampler-B8dtKHn1.mjs","names":[],"sources":["../src/heap-sample/HeapSampler.ts"],"sourcesContent":["import { Session } from \"node:inspector/promises\";\n\nexport interface HeapSampleOptions {\n samplingInterval?: number; // bytes between samples, default 32768\n stackDepth?: number; // max stack frames, default 64\n includeMinorGC?: boolean; // keep objects collected by minor GC, default true\n includeMajorGC?: boolean; // keep objects collected by major GC, default true\n}\n\nexport interface ProfileNode {\n callFrame: {\n functionName: string;\n url: string;\n lineNumber: number;\n columnNumber?: number;\n };\n selfSize: number;\n children?: ProfileNode[];\n}\n\nexport interface HeapProfile {\n head: ProfileNode;\n samples?: number[]; // sample IDs (length = number of samples taken)\n}\n\nconst defaultOptions: Required<HeapSampleOptions> = {\n samplingInterval: 32768,\n stackDepth: 64,\n includeMinorGC: true,\n includeMajorGC: true,\n};\n\n/** Run a function while sampling heap allocations, return profile */\nexport async function withHeapSampling<T>(\n options: HeapSampleOptions,\n fn: () => Promise<T> | T,\n): Promise<{ result: T; profile: HeapProfile }> {\n const opts = { ...defaultOptions, ...options };\n const session = new Session();\n session.connect();\n\n try {\n await startSampling(session, opts);\n const result = await fn();\n const profile = await stopSampling(session);\n return { result, profile };\n } finally {\n session.disconnect();\n }\n}\n\n/** Start heap sampling, falling back if include-collected params aren't supported */\nasync function startSampling(\n session: Session,\n opts: Required<HeapSampleOptions>,\n): Promise<void> {\n const { samplingInterval, stackDepth } = opts;\n const base = { samplingInterval, stackDepth };\n const params = {\n ...base,\n includeObjectsCollectedByMinorGC: opts.includeMinorGC,\n includeObjectsCollectedByMajorGC: opts.includeMajorGC,\n };\n\n try {\n await session.post(\"HeapProfiler.startSampling\", params);\n } catch {\n console.warn(\n \"HeapProfiler: include-collected params not supported, falling back\",\n );\n await session.post(\"HeapProfiler.startSampling\", base);\n }\n}\n\nasync function stopSampling(session: Session): Promise<HeapProfile> {\n const { profile } = await session.post(\"HeapProfiler.stopSampling\");\n return profile as HeapProfile;\n}\n"],"mappings":";;;AAyBA,MAAM,iBAA8C;CAClD,kBAAkB;CAClB,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CACjB;;AAGD,eAAsB,iBACpB,SACA,IAC8C;CAC9C,MAAM,OAAO;EAAE,GAAG;EAAgB,GAAG;EAAS;CAC9C,MAAM,UAAU,IAAI,SAAS;AAC7B,SAAQ,SAAS;AAEjB,KAAI;AACF,QAAM,cAAc,SAAS,KAAK;AAGlC,SAAO;GAAE,QAFM,MAAM,IAAI;GAER,SADD,MAAM,aAAa,QAAQ;GACjB;WAClB;AACR,UAAQ,YAAY;;;;AAKxB,eAAe,cACb,SACA,MACe;CACf,MAAM,EAAE,kBAAkB,eAAe;CACzC,MAAM,OAAO;EAAE;EAAkB;EAAY;CAC7C,MAAM,SAAS;EACb,GAAG;EACH,kCAAkC,KAAK;EACvC,kCAAkC,KAAK;EACxC;AAED,KAAI;AACF,QAAM,QAAQ,KAAK,8BAA8B,OAAO;SAClD;AACN,UAAQ,KACN,qEACD;AACD,QAAM,QAAQ,KAAK,8BAA8B,KAAK;;;AAI1D,eAAe,aAAa,SAAwC;CAClE,MAAM,EAAE,YAAY,MAAM,QAAQ,KAAK,4BAA4B;AACnE,QAAO"}
1
+ {"version":3,"file":"HeapSampler-B8dtKHn1.mjs","names":[],"sources":["../src/heap-sample/HeapSampler.ts"],"sourcesContent":["import { Session } from \"node:inspector/promises\";\n\nexport interface HeapSampleOptions {\n /** Bytes between samples (default 32768) */\n samplingInterval?: number;\n\n /** Max stack frames (default 64) */\n stackDepth?: number;\n\n /** Keep objects collected by minor GC (default true) */\n includeMinorGC?: boolean;\n\n /** Keep objects collected by major GC (default true) */\n includeMajorGC?: boolean;\n}\n\n/** V8 call frame location within a profiled script */\nexport interface CallFrame {\n /** Function name (empty string for anonymous) */\n functionName: string;\n\n /** Script URL or file path */\n url: string;\n\n /** Zero-based line number */\n lineNumber: number;\n\n /** Zero-based column number */\n columnNumber?: number;\n}\n\n/** Node in the V8 sampling heap profile tree */\nexport interface ProfileNode {\n /** Call site for this allocation node */\n callFrame: CallFrame;\n\n /** Bytes allocated directly at this node (not children) */\n selfSize: number;\n\n /** Unique node ID, links to {@link HeapSample.nodeId} */\n id: number;\n\n /** Child nodes in the call tree */\n children?: ProfileNode[];\n}\n\n/** Individual heap allocation sample from V8's SamplingHeapProfiler */\nexport interface HeapSample {\n /** Links to {@link ProfileNode.id} for stack lookup */\n nodeId: number;\n\n /** Allocation size in bytes */\n size: number;\n\n /** Monotonically increasing, gives temporal ordering */\n ordinal: number;\n}\n\n/** V8 sampling heap profile tree with optional per-allocation samples */\nexport interface HeapProfile {\n /** Root of the profile call tree */\n head: ProfileNode;\n\n /** Per-allocation samples, if collected */\n samples?: HeapSample[];\n}\n\nconst defaultOptions: Required<HeapSampleOptions> = {\n samplingInterval: 32768,\n stackDepth: 64,\n includeMinorGC: true,\n includeMajorGC: true,\n};\n\n/** Run a function while sampling heap allocations, return profile */\nexport async function withHeapSampling<T>(\n options: HeapSampleOptions,\n fn: () => Promise<T> | T,\n): Promise<{ result: T; profile: HeapProfile }> {\n const opts = { ...defaultOptions, ...options };\n const session = new Session();\n session.connect();\n\n try {\n await startSampling(session, opts);\n const result = await fn();\n const profile = await stopSampling(session);\n return { result, profile };\n } finally {\n session.disconnect();\n }\n}\n\n/** Start heap sampling, falling back if include-collected params aren't supported */\nasync function startSampling(\n session: Session,\n opts: Required<HeapSampleOptions>,\n): Promise<void> {\n const { samplingInterval, stackDepth } = opts;\n const base = { samplingInterval, stackDepth };\n const params = {\n ...base,\n includeObjectsCollectedByMinorGC: opts.includeMinorGC,\n includeObjectsCollectedByMajorGC: opts.includeMajorGC,\n };\n\n try {\n await session.post(\"HeapProfiler.startSampling\", params);\n } catch {\n console.warn(\n \"HeapProfiler: include-collected params not supported, falling back\",\n );\n await session.post(\"HeapProfiler.startSampling\", base);\n }\n}\n\nasync function stopSampling(session: Session): Promise<HeapProfile> {\n const { profile } = await session.post(\"HeapProfiler.stopSampling\");\n // V8 returns id/samples fields not in @types/node's incomplete SamplingHeapProfile\n return profile as unknown as HeapProfile;\n}\n"],"mappings":";;;AAmEA,MAAM,iBAA8C;CAClD,kBAAkB;CAClB,YAAY;CACZ,gBAAgB;CAChB,gBAAgB;CACjB;;AAGD,eAAsB,iBACpB,SACA,IAC8C;CAC9C,MAAM,OAAO;EAAE,GAAG;EAAgB,GAAG;EAAS;CAC9C,MAAM,UAAU,IAAI,SAAS;AAC7B,SAAQ,SAAS;AAEjB,KAAI;AACF,QAAM,cAAc,SAAS,KAAK;AAGlC,SAAO;GAAE,QAFM,MAAM,IAAI;GAER,SADD,MAAM,aAAa,QAAQ;GACjB;WAClB;AACR,UAAQ,YAAY;;;;AAKxB,eAAe,cACb,SACA,MACe;CACf,MAAM,EAAE,kBAAkB,eAAe;CACzC,MAAM,OAAO;EAAE;EAAkB;EAAY;CAC7C,MAAM,SAAS;EACb,GAAG;EACH,kCAAkC,KAAK;EACvC,kCAAkC,KAAK;EACxC;AAED,KAAI;AACF,QAAM,QAAQ,KAAK,8BAA8B,OAAO;SAClD;AACN,UAAQ,KACN,qEACD;AACD,QAAM,QAAQ,KAAK,8BAA8B,KAAK;;;AAI1D,eAAe,aAAa,SAAwC;CAClE,MAAM,EAAE,YAAY,MAAM,QAAQ,KAAK,4BAA4B;AAEnE,QAAO"}