benchforge 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -151,18 +151,17 @@ The `--profile` flag executes exactly one iteration with no warmup, making it id
151
151
  Results are displayed in a formatted table:
152
152
 
153
153
  ```
154
- ╔═════════════════╤═══════════════════════════════════════════╤═══════╤═════════╗
155
- ║ │ time │
156
- ║ name │ mean Δ% CI p50 p99 │ conv% │ runs ║
157
- ╟─────────────────┼───────────────────────────────────────────┼───────┼─────────╢
158
- ║ quicksort │ 0.17 +5.5% [+4.7%, +6.2%] 0.15 0.63 │ 100% │ 1,134 ║
159
- ║ insertion sort │ 0.24 +25.9% [+25.3%, +27.4%] 0.18 0.36 │ 100% │ 807 ║
160
- ║ --> native sort │ 0.16 0.15 0.41 │ 100% │ 1,210 ║
161
- ╚═════════════════╧═══════════════════════════════════════════╧═══════╧═════════╝
154
+ ╔═════════════════╤═══════════════════════════════════════════╤═════════╗
155
+ ║ │ time │ ║
156
+ ║ name │ mean Δ% CI p50 p99 │ runs ║
157
+ ╟─────────────────┼───────────────────────────────────────────┼─────────╢
158
+ ║ quicksort │ 0.17 +5.5% [+4.7%, +6.2%] 0.15 0.63 │ 1,134 ║
159
+ ║ insertion sort │ 0.24 +25.9% [+25.3%, +27.4%] 0.18 0.36 │ 807 ║
160
+ ║ --> native sort │ 0.16 0.15 0.41 │ 1,210 ║
161
+ ╚═════════════════╧═══════════════════════════════════════════╧═════════╝
162
162
  ```
163
163
 
164
164
  - **Δ% CI**: Percentage difference from baseline with bootstrap confidence interval
165
- - **conv%**: Convergence percentage (100% = stable measurements)
166
165
 
167
166
  ### HTML
168
167
 
@@ -284,136 +283,23 @@ V8's sampling profiler uses Poisson-distributed sampling. When an allocation occ
284
283
  - Node.js 22.6+ (for native TypeScript support)
285
284
  - Use `--expose-gc --allow-natives-syntax` flags for garbage collection monitoring and V8 native functions
286
285
 
287
- ## Adaptive Mode
286
+ ## Adaptive Mode (Experimental)
288
287
 
289
- Adaptive mode automatically adjusts the number of benchmark iterations until measurements stabilize, providing statistically significant results without excessive runtime.
288
+ Adaptive mode (`--adaptive`) automatically adjusts iteration count until measurements stabilize. The algorithm is still being tuned — use `--help` for available options.
290
289
 
291
- ### Using Adaptive Mode
290
+ ## Interpreting Results
292
291
 
293
- ```bash
294
- # Enable adaptive benchmarking with default settings
295
- simple-cli.ts --adaptive
296
-
297
- # Customize time limits
298
- simple-cli.ts --adaptive --time 60 --min-time 5
299
-
300
- # Combine with other options
301
- simple-cli.ts --adaptive --filter "quicksort"
302
- ```
303
-
304
- ### CLI Options for Adaptive Mode
305
-
306
- - `--adaptive` - Enable adaptive sampling mode
307
- - `--min-time <seconds>` - Minimum time before convergence can stop (default: 1s)
308
- - `--convergence <percent>` - Confidence threshold 0-100 (default: 95)
309
- - `--time <seconds>` - Maximum time limit (default: 20s in adaptive mode)
310
-
311
- ### How It Works
312
-
313
- 1. **Initial Sampling**: Collects initial batch of ~100 samples (includes warmup)
314
- 2. **Window Comparison**: Compares recent samples against previous window
315
- 3. **Stability Detection**: Checks median drift and outlier impact between windows
316
- 4. **Convergence**: Stops when both metrics are stable (<5% drift) or reaches threshold
317
-
318
- Progress is shown during execution:
319
- ```
320
- ◊ quicksort: 75% confident (2.1s)
321
- ```
322
-
323
- ### Output with Adaptive Mode
324
-
325
- ```
326
- ╔═════════════════╤═════════════════════════════════════════════╤═══════╤═════════╤══════╗
327
- ║ │ time │ │ │ ║
328
- ║ name │ median Δ% CI mean p99 │ conv% │ runs │ time ║
329
- ╟─────────────────┼─────────────────────────────────────────────┼───────┼─────────┼──────╢
330
- ║ quicksort │ 0.17 +17.3% [+15.4%, +20.0%] 0.20 0.65 │ 100% │ 526 │ 0.0s ║
331
- ║ insertion sort │ 0.18 +24.2% [+23.9%, +24.6%] 0.19 0.36 │ 100% │ 529 │ 0.0s ║
332
- ║ --> native sort │ 0.15 0.15 0.25 │ 100% │ 647 │ 0.0s ║
333
- ╚═════════════════╧═════════════════════════════════════════════╧═══════╧═════════╧══════╝
334
- ```
335
-
336
- - **conv%**: Convergence percentage (100% = stable measurements)
337
- - **time**: Total sampling duration for that benchmark
338
-
339
- ## Statistical Considerations: Mean vs Median
340
-
341
- ### When to Use Mean with Confidence Intervals
342
-
343
- **Best for:**
344
- - **Normally distributed data** - When benchmark times follow a bell curve
345
- - **Statistical comparison** - Comparing performance between implementations
346
- - **Throughput analysis** - Understanding average system performance
347
- - **Resource planning** - Estimating typical resource usage
348
-
349
- **Advantages:**
350
- - Provides confidence intervals for statistical significance
351
- - Captures the full distribution including outliers
352
- - Better for detecting small but consistent performance differences
353
- - Standard in academic performance research
354
-
355
- **Example use cases:**
356
- - Comparing algorithm implementations
357
- - Measuring API response times under normal load
358
- - Evaluating compiler optimizations
359
- - Benchmarking pure computational functions
360
-
361
- ### When to Use Median (p50)
362
-
363
- **Best for:**
364
- - **Skewed distributions** - When outliers are common
365
- - **Latency-sensitive applications** - Where typical user experience matters
366
- - **Noisy environments** - Systems with unpredictable interference
367
- - **Service Level Agreements** - "50% of requests complete within X ms"
368
-
369
- **Advantages:**
370
- - Robust to outliers and system noise
371
- - Better represents "typical" performance
372
- - More stable in virtualized/cloud environments
373
- - Less affected by GC pauses and OS scheduling
374
-
375
- **Example use cases:**
376
- - Web server response times
377
- - Database query performance
378
- - UI responsiveness metrics
379
- - Real-time system benchmarks
380
-
381
- ### Interpreting Results
382
-
383
- #### Baseline Comparison (Δ% CI)
292
+ ### Baseline Comparison (Δ% CI)
384
293
  ```
385
294
  0.17 +5.5% [+4.7%, +6.2%]
386
295
  ```
387
- This shows the benchmark is 5.5% slower than baseline, with a bootstrap confidence interval of [+4.7%, +6.2%]. Use this for comparing implementations.
296
+ The benchmark is 5.5% slower than baseline, with a bootstrap confidence interval of [+4.7%, +6.2%].
388
297
 
389
- #### Percentiles
298
+ ### Percentiles
390
299
  ```
391
300
  p50: 0.15ms, p99: 0.27ms
392
301
  ```
393
- This shows that 50% of runs completed in ≤0.15ms and 99% in ≤0.27ms. Use this when you care about consistency and tail latencies.
394
-
395
- ### Practical Guidelines
396
-
397
- 1. **Use adaptive mode when:**
398
- - You want automatic convergence detection
399
- - Benchmarks have varying execution times
400
- - You need stable measurements without guessing iteration counts
401
-
402
- 2. **Use fixed iterations when:**
403
- - Comparing across runs/machines (reproducibility)
404
- - You know roughly how many samples you need
405
- - Running in CI pipelines with time constraints
406
-
407
- 3. **Interpreting conv%:**
408
- - 100% = measurements are stable
409
- - <100% = still converging or high variance
410
- - Red color indicates low confidence
411
-
412
- ### Statistical Notes
413
-
414
- - **Bootstrap CI**: Baseline comparison uses permutation testing with bootstrap confidence intervals
415
- - **Window Stability**: Adaptive mode compares sliding windows for median drift and outlier impact
416
- - **Independence**: Assumes benchmark iterations are independent (use `--worker` flag for better isolation)
302
+ 50% of runs completed in ≤0.15ms and 99% in ≤0.27ms. Use percentiles when you care about consistency and tail latencies.
417
303
 
418
304
  ## Understanding GC Time Measurements
419
305
 
@@ -222,4 +222,4 @@ interface RunnerOptions {
222
222
  }
223
223
  //#endregion
224
224
  export { MeasuredResults as a, BenchmarkSpec as i, BenchGroup as n, HeapProfile as o, BenchSuite as r, RunnerOptions as t };
225
- //# sourceMappingURL=BenchRunner-BLfGX2wQ.d.mts.map
225
+ //# sourceMappingURL=BenchRunner-CSKN9zPy.d.mts.map
@@ -0,0 +1,187 @@
1
+ import { t as aggregateGcStats } from "./GcStats-ByEovUi1.mjs";
2
+ import { chromium } from "playwright";
3
+
4
+ //#region src/browser/BrowserGcStats.ts
5
+ /** Parse CDP trace events (MinorGC/MajorGC) into GcEvent[] */
6
+ function parseGcTraceEvents(traceEvents) {
7
+ return traceEvents.flatMap((e) => {
8
+ if (e.ph !== "X") return [];
9
+ const type = gcType(e.name);
10
+ if (!type) return [];
11
+ const durUs = e.dur ?? 0;
12
+ const heapBefore = e.args?.usedHeapSizeBefore ?? 0;
13
+ const heapAfter = e.args?.usedHeapSizeAfter ?? 0;
14
+ return [{
15
+ type,
16
+ pauseMs: durUs / 1e3,
17
+ collected: Math.max(0, heapBefore - heapAfter)
18
+ }];
19
+ });
20
+ }
21
+ function gcType(name) {
22
+ if (name === "MinorGC") return "scavenge";
23
+ if (name === "MajorGC") return "mark-compact";
24
+ }
25
+ /** Parse CDP trace events and aggregate into GcStats */
26
+ function browserGcStats(traceEvents) {
27
+ return aggregateGcStats(parseGcTraceEvents(traceEvents));
28
+ }
29
+
30
+ //#endregion
31
+ //#region src/browser/BrowserHeapSampler.ts
32
+ /** Run browser benchmark, auto-detecting page API mode.
33
+ * Bench function (window.__bench): CLI controls iteration and timing.
34
+ * Lap mode (__start/__lap/__done): page controls the measured region. */
35
+ async function profileBrowser(params) {
36
+ const { url, headless = true, chromeArgs, timeout = 60 } = params;
37
+ const { gcStats: collectGc } = params;
38
+ const { samplingInterval = 32768 } = params.heapOptions ?? {};
39
+ const browser = await chromium.launch({
40
+ headless,
41
+ args: chromeArgs
42
+ });
43
+ try {
44
+ const page = await browser.newPage();
45
+ page.setDefaultTimeout(timeout * 1e3);
46
+ const cdp = await page.context().newCDPSession(page);
47
+ const pageErrors = [];
48
+ page.on("pageerror", (err) => pageErrors.push(err.message));
49
+ const traceEvents = collectGc ? await startGcTracing(cdp) : [];
50
+ const lapMode = await setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors);
51
+ await page.goto(url, { waitUntil: "load" });
52
+ const hasBench = await page.evaluate(() => typeof globalThis.__bench === "function");
53
+ let result;
54
+ if (hasBench) {
55
+ lapMode.cancel();
56
+ lapMode.promise.catch(() => {});
57
+ result = await runBenchLoop(page, cdp, params, samplingInterval);
58
+ } else {
59
+ result = await lapMode.promise;
60
+ lapMode.cancel();
61
+ }
62
+ if (collectGc) result = {
63
+ ...result,
64
+ gcStats: await collectTracing(cdp, traceEvents)
65
+ };
66
+ return result;
67
+ } finally {
68
+ await browser.close();
69
+ }
70
+ }
71
+ /** Inject __start/__lap as in-page functions, expose __done for results collection.
72
+ * __start/__lap are pure in-page (zero CDP overhead). First __start() triggers
73
+ * instrument start. __done() stops instruments and collects timing data. */
74
+ async function setupLapMode(page, cdp, params, samplingInterval, timeout, pageErrors) {
75
+ const { heapSample } = params;
76
+ const { promise, resolve, reject } = Promise.withResolvers();
77
+ let instrumentsStarted = false;
78
+ await page.exposeFunction("__benchInstrumentStart", async () => {
79
+ if (instrumentsStarted) return;
80
+ instrumentsStarted = true;
81
+ if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
82
+ });
83
+ await page.exposeFunction("__benchCollect", async (samples, wallTimeMs) => {
84
+ let heapProfile;
85
+ if (heapSample && instrumentsStarted) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
86
+ resolve({
87
+ samples,
88
+ heapProfile,
89
+ wallTimeMs
90
+ });
91
+ });
92
+ await page.addInitScript(injectLapFunctions);
93
+ const timer = setTimeout(() => {
94
+ const lines = [`Timed out after ${timeout}s`];
95
+ if (pageErrors.length) lines.push("Page JS errors:", ...pageErrors.map((e) => ` ${e}`));
96
+ else lines.push("Page did not call __done() or define window.__bench");
97
+ reject(new Error(lines.join("\n")));
98
+ }, timeout * 1e3);
99
+ return {
100
+ promise,
101
+ cancel: () => clearTimeout(timer)
102
+ };
103
+ }
104
+ /** In-page timing functions injected via addInitScript (zero CDP overhead).
105
+ * __start/__lap collect timestamps, __done delegates to exposed __benchCollect. */
106
+ function injectLapFunctions() {
107
+ const g = globalThis;
108
+ g.__benchSamples = [];
109
+ g.__benchLastTime = 0;
110
+ g.__benchFirstStart = 0;
111
+ g.__start = () => {
112
+ const now = performance.now();
113
+ g.__benchLastTime = now;
114
+ if (!g.__benchFirstStart) {
115
+ g.__benchFirstStart = now;
116
+ return g.__benchInstrumentStart();
117
+ }
118
+ };
119
+ g.__lap = () => {
120
+ const now = performance.now();
121
+ g.__benchSamples.push(now - g.__benchLastTime);
122
+ g.__benchLastTime = now;
123
+ };
124
+ g.__done = () => {
125
+ const wall = g.__benchFirstStart ? performance.now() - g.__benchFirstStart : 0;
126
+ return g.__benchCollect(g.__benchSamples.slice(), wall);
127
+ };
128
+ }
129
+ function heapSamplingParams(samplingInterval) {
130
+ return {
131
+ samplingInterval,
132
+ includeObjectsCollectedByMajorGC: true,
133
+ includeObjectsCollectedByMinorGC: true
134
+ };
135
+ }
136
+ /** Start CDP GC tracing, returns the event collector array. */
137
+ async function startGcTracing(cdp) {
138
+ const events = [];
139
+ cdp.on("Tracing.dataCollected", ({ value }) => {
140
+ for (const e of value) events.push(e);
141
+ });
142
+ await cdp.send("Tracing.start", { traceConfig: { includedCategories: ["v8", "v8.gc"] } });
143
+ return events;
144
+ }
145
+ /** Bench function mode: run window.__bench in a timed iteration loop. */
146
+ async function runBenchLoop(page, cdp, params, samplingInterval) {
147
+ const { heapSample } = params;
148
+ const maxTime = params.maxTime ?? 642;
149
+ const maxIter = params.maxIterations ?? Number.MAX_SAFE_INTEGER;
150
+ if (heapSample) await cdp.send("HeapProfiler.startSampling", heapSamplingParams(samplingInterval));
151
+ const { samples, totalMs } = await page.evaluate(async ({ maxTime, maxIter }) => {
152
+ const bench = globalThis.__bench;
153
+ const samples = [];
154
+ const startAll = performance.now();
155
+ const deadline = startAll + maxTime;
156
+ for (let i = 0; i < maxIter && performance.now() < deadline; i++) {
157
+ const t0 = performance.now();
158
+ await bench();
159
+ samples.push(performance.now() - t0);
160
+ }
161
+ return {
162
+ samples,
163
+ totalMs: performance.now() - startAll
164
+ };
165
+ }, {
166
+ maxTime,
167
+ maxIter
168
+ });
169
+ let heapProfile;
170
+ if (heapSample) heapProfile = (await cdp.send("HeapProfiler.stopSampling")).profile;
171
+ return {
172
+ samples,
173
+ heapProfile,
174
+ wallTimeMs: totalMs
175
+ };
176
+ }
177
+ /** Stop CDP tracing and parse GC events into GcStats. */
178
+ async function collectTracing(cdp, traceEvents) {
179
+ const complete = new Promise((resolve) => cdp.once("Tracing.tracingComplete", () => resolve()));
180
+ await cdp.send("Tracing.end");
181
+ await complete;
182
+ return browserGcStats(traceEvents);
183
+ }
184
+
185
+ //#endregion
186
+ export { profileBrowser, profileBrowser as profileBrowserHeap };
187
+ //# sourceMappingURL=BrowserHeapSampler-DQwmmuDu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserHeapSampler-DQwmmuDu.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\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\n/** Parse CDP trace events and aggregate into GcStats */\nexport function browserGcStats(traceEvents: TraceEvent[]): GcStats {\n return aggregateGcStats(parseGcTraceEvents(traceEvents));\n}\n","import { type CDPSession, chromium, type Page } 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 browser = await chromium.launch({ headless, args: chromeArgs });\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 }\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/** 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\nfunction heapSamplingParams(samplingInterval: number) {\n return {\n samplingInterval,\n includeObjectsCollectedByMajorGC: true,\n includeObjectsCollectedByMinorGC: true,\n };\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/** 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\nexport { profileBrowser as profileBrowserHeap };\n"],"mappings":";;;;;AAgBA,SAAgB,mBAAmB,aAAsC;AACvE,QAAO,YAAY,SAAQ,MAAK;AAC9B,MAAI,EAAE,OAAO,IAAK,QAAO,EAAE;EAC3B,MAAM,OAAO,OAAO,EAAE,KAAK;AAC3B,MAAI,CAAC,KAAM,QAAO,EAAE;EACpB,MAAM,QAAQ,EAAE,OAAO;EACvB,MAAM,aAAqB,EAAE,MAAM,sBAAsB;EACzD,MAAM,YAAoB,EAAE,MAAM,qBAAqB;AACvD,SAAO,CACL;GACE;GACA,SAAS,QAAQ;GACjB,WAAW,KAAK,IAAI,GAAG,aAAa,UAAU;GAC/C,CACF;GACD;;AAGJ,SAAS,OAAO,MAA2C;AACzD,KAAI,SAAS,UAAW,QAAO;AAC/B,KAAI,SAAS,UAAW,QAAO;;;AAKjC,SAAgB,eAAe,aAAoC;AACjE,QAAO,iBAAiB,mBAAmB,YAAY,CAAC;;;;;;;;ACL1D,eAAsB,eACpB,QAC+B;CAC/B,MAAM,EAAE,KAAK,WAAW,MAAM,YAAY,UAAU,OAAO;CAC3D,MAAM,EAAE,SAAS,cAAc;CAC/B,MAAM,EAAE,mBAAmB,UAAU,OAAO,eAAe,EAAE;CAE7D,MAAM,UAAU,MAAM,SAAS,OAAO;EAAE;EAAU,MAAM;EAAY,CAAC;AACrE,KAAI;EACF,MAAM,OAAO,MAAM,QAAQ,SAAS;AACpC,OAAK,kBAAkB,UAAU,IAAK;EACtC,MAAM,MAAM,MAAM,KAAK,SAAS,CAAC,cAAc,KAAK;EAEpD,MAAM,aAAuB,EAAE;AAC/B,OAAK,GAAG,cAAa,QAAO,WAAW,KAAK,IAAI,QAAQ,CAAC;EAEzD,MAAM,cAAc,YAAY,MAAM,eAAe,IAAI,GAAG,EAAE;EAC9D,MAAM,UAAU,MAAM,aACpB,MACA,KACA,QACA,kBACA,SACA,WACD;AAED,QAAM,KAAK,KAAK,KAAK,EAAE,WAAW,QAAQ,CAAC;EAC3C,MAAM,WAAW,MAAM,KAAK,eACpB,OAAQ,WAAmB,YAAY,WAC9C;EAED,IAAI;AACJ,MAAI,UAAU;AACZ,WAAQ,QAAQ;AAChB,WAAQ,QAAQ,YAAY,GAAG;AAC/B,YAAS,MAAM,aAAa,MAAM,KAAK,QAAQ,iBAAiB;SAC3D;AACL,YAAS,MAAM,QAAQ;AACvB,WAAQ,QAAQ;;AAGlB,MAAI,UACF,UAAS;GAAE,GAAG;GAAQ,SAAS,MAAM,eAAe,KAAK,YAAY;GAAE;AAEzE,SAAO;WACC;AACR,QAAM,QAAQ,OAAO;;;;;;AAOzB,eAAe,aACb,MACA,KACA,QACA,kBACA,SACA,YACwB;CACxB,MAAM,EAAE,eAAe;CACvB,MAAM,EAAE,SAAS,SAAS,WACxB,QAAQ,eAAqC;CAC/C,IAAI,qBAAqB;AAEzB,OAAM,KAAK,eAAe,0BAA0B,YAAY;AAC9D,MAAI,mBAAoB;AACxB,uBAAqB;AACrB,MAAI,WACF,OAAM,IAAI,KACR,8BACA,mBAAmB,iBAAiB,CACrC;GAEH;AAEF,OAAM,KAAK,eACT,kBACA,OAAO,SAAmB,eAAuB;EAC/C,IAAI;AACJ,MAAI,cAAc,mBAEhB,gBADe,MAAM,IAAI,KAAK,4BAA4B,EACrC;AAEvB,UAAQ;GAAE;GAAS;GAAa;GAAY,CAAC;GAEhD;AAED,OAAM,KAAK,cAAc,mBAAmB;CAE5C,MAAM,QAAQ,iBAAiB;EAC7B,MAAM,QAAQ,CAAC,mBAAmB,QAAQ,GAAG;AAC7C,MAAI,WAAW,OACb,OAAM,KAAK,mBAAmB,GAAG,WAAW,KAAI,MAAK,KAAK,IAAI,CAAC;MAE/D,OAAM,KAAK,sDAAsD;AAEnE,SAAO,IAAI,MAAM,MAAM,KAAK,KAAK,CAAC,CAAC;IAClC,UAAU,IAAK;AAElB,QAAO;EAAE;EAAS,cAAc,aAAa,MAAM;EAAE;;;;AAKvD,SAAS,qBAA2B;CAClC,MAAM,IAAI;AACV,GAAE,iBAAiB,EAAE;AACrB,GAAE,kBAAkB;AACpB,GAAE,oBAAoB;AAEtB,GAAE,gBAAgB;EAChB,MAAM,MAAM,YAAY,KAAK;AAC7B,IAAE,kBAAkB;AACpB,MAAI,CAAC,EAAE,mBAAmB;AACxB,KAAE,oBAAoB;AACtB,UAAO,EAAE,wBAAwB;;;AAIrC,GAAE,cAAc;EACd,MAAM,MAAM,YAAY,KAAK;AAC7B,IAAE,eAAe,KAAK,MAAM,EAAE,gBAAgB;AAC9C,IAAE,kBAAkB;;AAGtB,GAAE,eAAe;EACf,MAAM,OAAO,EAAE,oBACX,YAAY,KAAK,GAAG,EAAE,oBACtB;AACJ,SAAO,EAAE,eAAe,EAAE,eAAe,OAAO,EAAE,KAAK;;;AAI3D,SAAS,mBAAmB,kBAA0B;AACpD,QAAO;EACL;EACA,kCAAkC;EAClC,kCAAkC;EACnC;;;AAIH,eAAe,eAAe,KAAwC;CACpE,MAAM,SAAuB,EAAE;AAC/B,KAAI,GAAG,0BAA0B,EAAE,YAAY;AAC7C,OAAK,MAAM,KAAK,MAAO,QAAO,KAAK,EAA2B;GAC9D;AACF,OAAM,IAAI,KAAK,iBAAiB,EAC9B,aAAa,EAAE,oBAAoB,CAAC,MAAM,QAAQ,EAAE,EACrD,CAAC;AACF,QAAO;;;AAIT,eAAe,aACb,MACA,KACA,QACA,kBAC+B;CAC/B,MAAM,EAAE,eAAe;CACvB,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,UAAU,OAAO,iBAAiB,OAAO;AAE/C,KAAI,WACF,OAAM,IAAI,KACR,8BACA,mBAAmB,iBAAiB,CACrC;CAGH,MAAM,EAAE,SAAS,YAAY,MAAM,KAAK,SACtC,OAAO,EAAE,SAAS,cAAc;EAC9B,MAAM,QAAS,WAAmB;EAClC,MAAM,UAAoB,EAAE;EAC5B,MAAM,WAAW,YAAY,KAAK;EAClC,MAAM,WAAW,WAAW;AAC5B,OAAK,IAAI,IAAI,GAAG,IAAI,WAAW,YAAY,KAAK,GAAG,UAAU,KAAK;GAChE,MAAM,KAAK,YAAY,KAAK;AAC5B,SAAM,OAAO;AACb,WAAQ,KAAK,YAAY,KAAK,GAAG,GAAG;;AAEtC,SAAO;GAAE;GAAS,SAAS,YAAY,KAAK,GAAG;GAAU;IAE3D;EAAE;EAAS;EAAS,CACrB;CAED,IAAI;AACJ,KAAI,WAEF,gBADe,MAAM,IAAI,KAAK,4BAA4B,EACrC;AAGvB,QAAO;EAAE;EAAS;EAAa,YAAY;EAAS;;;AAItD,eAAe,eACb,KACA,aACkB;CAClB,MAAM,WAAW,IAAI,SAAc,YACjC,IAAI,KAAK,iCAAiC,SAAS,CAAC,CACrD;AACD,OAAM,IAAI,KAAK,cAAc;AAC7B,OAAM;AACN,QAAO,eAAe,YAAY"}
@@ -0,0 +1,77 @@
1
+ //#region src/runners/GcStats.ts
2
+ /** Parse a single --trace-gc-nvp stderr line */
3
+ function parseGcLine(line) {
4
+ if (!line.includes("pause=")) return void 0;
5
+ const fields = parseNvpFields(line);
6
+ if (!fields.gc) return void 0;
7
+ const int = (k) => Number.parseInt(fields[k] || "0", 10);
8
+ const type = parseGcType(fields.gc);
9
+ const pauseMs = Number.parseFloat(fields.pause || "0");
10
+ const allocated = int("allocated");
11
+ const promoted = int("promoted");
12
+ const survived = int("new_space_survived") || int("survived");
13
+ const startSize = int("start_object_size");
14
+ const endSize = int("end_object_size");
15
+ const collected = startSize > endSize ? startSize - endSize : 0;
16
+ if (Number.isNaN(pauseMs)) return void 0;
17
+ return {
18
+ type,
19
+ pauseMs,
20
+ allocated,
21
+ collected,
22
+ promoted,
23
+ survived
24
+ };
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
+ /** Aggregate GC events into summary stats */
41
+ function aggregateGcStats(events) {
42
+ let scavenges = 0;
43
+ let markCompacts = 0;
44
+ let gcPauseTime = 0;
45
+ let totalCollected = 0;
46
+ let hasNodeFields = false;
47
+ let totalAllocated = 0;
48
+ let totalPromoted = 0;
49
+ let totalSurvived = 0;
50
+ for (const e of events) {
51
+ if (e.type === "scavenge" || e.type === "minor-ms") scavenges++;
52
+ else if (e.type === "mark-compact") markCompacts++;
53
+ gcPauseTime += e.pauseMs;
54
+ totalCollected += e.collected;
55
+ if (e.allocated != null) {
56
+ hasNodeFields = true;
57
+ totalAllocated += e.allocated;
58
+ totalPromoted += e.promoted ?? 0;
59
+ totalSurvived += e.survived ?? 0;
60
+ }
61
+ }
62
+ return {
63
+ scavenges,
64
+ markCompacts,
65
+ totalCollected,
66
+ gcPauseTime,
67
+ ...hasNodeFields && {
68
+ totalAllocated,
69
+ totalPromoted,
70
+ totalSurvived
71
+ }
72
+ };
73
+ }
74
+
75
+ //#endregion
76
+ export { parseGcLine as n, aggregateGcStats as t };
77
+ //# sourceMappingURL=GcStats-ByEovUi1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GcStats-ByEovUi1.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/** 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\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"],"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,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;;;AAIT,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"}
@@ -51,4 +51,4 @@ async function stopSampling(session) {
51
51
 
52
52
  //#endregion
53
53
  export { withHeapSampling };
54
- //# sourceMappingURL=HeapSampler-BX3de22o.mjs.map
54
+ //# sourceMappingURL=HeapSampler-B8dtKHn1.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"HeapSampler-BX3de22o.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 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"}