benchforge 0.1.8 → 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.
- package/README.md +69 -42
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +106 -48
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-HfimYuW_.mjs → src-B-DDaCa9.mjs} +1250 -991
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +4 -3
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +20 -6
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +533 -476
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +36 -11
- package/src/test/TestUtils.ts +24 -24
- package/src/test/fixtures/fn-export-bench.ts +3 -0
- package/src/test/fixtures/suite-export-bench.ts +16 -0
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-HfimYuW_.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
# Benchforge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Traditional benchmarking tools either ignore GC or try to avoid it.
|
|
4
|
+
Benchforge captures GC impact.
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -16,39 +29,31 @@ pnpm add benchforge
|
|
|
16
29
|
|
|
17
30
|
## Quick Start
|
|
18
31
|
|
|
19
|
-
|
|
20
|
-
import { parseBenchArgs, runBenchmarks, reportResults, timeSection, runsSection, type BenchSuite } from 'benchforge';
|
|
32
|
+
The simplest way to benchmark a function: export it as the default export and pass the file to `benchforge`.
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ name: "plus", fn: () => "a" + "b" },
|
|
29
|
-
{ name: "template", fn: () => `a${"b"}` },
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
};
|
|
34
|
+
```typescript
|
|
35
|
+
// my-bench.ts
|
|
36
|
+
export default function (): string {
|
|
37
|
+
return "a" + "b";
|
|
38
|
+
}
|
|
39
|
+
```
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const table = reportResults(results, [timeSection, runsSection]);
|
|
38
|
-
console.log(table);
|
|
41
|
+
```bash
|
|
42
|
+
benchforge my-bench.ts --gc-stats
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
###
|
|
45
|
+
### BenchSuite Export
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
For multiple benchmarks with groups, setup data, and baseline comparison, export a `BenchSuite`:
|
|
44
48
|
|
|
45
49
|
```typescript
|
|
46
|
-
|
|
50
|
+
// sorting.ts
|
|
51
|
+
import type { BenchGroup, BenchSuite } from 'benchforge';
|
|
47
52
|
|
|
48
53
|
const sortingGroup: BenchGroup<number[]> = {
|
|
49
54
|
name: "Array Sorting (1000 numbers)",
|
|
50
55
|
setup: () => Array.from({ length: 1000 }, () => Math.random()),
|
|
51
|
-
baseline: { name: "native sort", fn:
|
|
56
|
+
baseline: { name: "native sort", fn: (arr) => [...arr].sort((a, b) => a - b) },
|
|
52
57
|
benchmarks: [
|
|
53
58
|
{ name: "quicksort", fn: quickSort },
|
|
54
59
|
{ name: "insertion sort", fn: insertionSort },
|
|
@@ -60,12 +65,15 @@ const suite: BenchSuite = {
|
|
|
60
65
|
groups: [sortingGroup],
|
|
61
66
|
};
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
const results = await runBenchmarks(suite, args);
|
|
65
|
-
const report = defaultReport(results, args);
|
|
66
|
-
console.log(report);
|
|
68
|
+
export default suite;
|
|
67
69
|
```
|
|
68
70
|
|
|
71
|
+
```bash
|
|
72
|
+
benchforge sorting.ts --gc-stats
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A `MatrixSuite` export (`.matrices`) is also recognized and runs via `matrixBenchExports`.
|
|
76
|
+
|
|
69
77
|
See `examples/simple-cli.ts` for a complete runnable example.
|
|
70
78
|
|
|
71
79
|
### Worker Mode with Module Imports
|
|
@@ -115,15 +123,17 @@ This eliminates manual caching boilerplate in worker modules.
|
|
|
115
123
|
- `--html` - Generate HTML report, start server, and open in browser
|
|
116
124
|
- `--export-html <file>` - Export HTML report to file
|
|
117
125
|
- `--json <file>` - Export benchmark data to JSON
|
|
118
|
-
- `--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
|
|
119
129
|
|
|
120
130
|
## CLI Usage
|
|
121
131
|
|
|
122
132
|
### Filter benchmarks by name
|
|
123
133
|
|
|
124
134
|
```bash
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
benchforge my-bench.ts --filter "concat"
|
|
136
|
+
benchforge my-bench.ts --filter "^parse" --time 2
|
|
127
137
|
```
|
|
128
138
|
|
|
129
139
|
### Profiling with external debuggers
|
|
@@ -132,10 +142,10 @@ Use `--profile` to run benchmarks once for attaching external profilers:
|
|
|
132
142
|
|
|
133
143
|
```bash
|
|
134
144
|
# Use with Chrome DevTools profiler
|
|
135
|
-
node --inspect-brk
|
|
145
|
+
node --inspect-brk $(which benchforge) my-bench.ts --profile
|
|
136
146
|
|
|
137
147
|
# Use with other profiling tools
|
|
138
|
-
node --prof
|
|
148
|
+
node --prof $(which benchforge) my-bench.ts --profile
|
|
139
149
|
```
|
|
140
150
|
|
|
141
151
|
The `--profile` flag executes exactly one iteration with no warmup, making it ideal for debugging and performance profiling.
|
|
@@ -172,7 +182,7 @@ The HTML report displays:
|
|
|
172
182
|
|
|
173
183
|
```bash
|
|
174
184
|
# Generate HTML report, start server, and open in browser
|
|
175
|
-
|
|
185
|
+
benchforge my-bench.ts --html
|
|
176
186
|
# Press Ctrl+C to exit when done viewing
|
|
177
187
|
```
|
|
178
188
|
|
|
@@ -182,11 +192,11 @@ Export benchmark data as a Perfetto-compatible trace file for detailed analysis:
|
|
|
182
192
|
|
|
183
193
|
```bash
|
|
184
194
|
# Export trace file
|
|
185
|
-
|
|
195
|
+
benchforge my-bench.ts --export-perfetto trace.json
|
|
186
196
|
|
|
187
197
|
# With V8 GC events (automatically merged after exit)
|
|
188
198
|
node --expose-gc --trace-events-enabled --trace-event-categories=v8,v8.gc \
|
|
189
|
-
|
|
199
|
+
benchforge my-bench.ts --export-perfetto trace.json
|
|
190
200
|
```
|
|
191
201
|
|
|
192
202
|
View the trace at https://ui.perfetto.dev by dragging the JSON file.
|
|
@@ -197,13 +207,27 @@ The trace includes:
|
|
|
197
207
|
- **Pause markers**: V8 optimization pause points
|
|
198
208
|
- **V8 GC events**: Automatically merged after process exit (when run with `--trace-events-enabled`)
|
|
199
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
|
+
|
|
200
224
|
### GC Statistics
|
|
201
225
|
|
|
202
226
|
Collect detailed garbage collection statistics via V8's `--trace-gc-nvp`:
|
|
203
227
|
|
|
204
228
|
```bash
|
|
205
229
|
# Collect GC allocation/collection stats (requires worker mode)
|
|
206
|
-
|
|
230
|
+
benchforge my-bench.ts --gc-stats
|
|
207
231
|
```
|
|
208
232
|
|
|
209
233
|
Adds these columns to the output table:
|
|
@@ -219,16 +243,16 @@ For allocation profiling including garbage (short-lived objects), use `--heap-sa
|
|
|
219
243
|
|
|
220
244
|
```bash
|
|
221
245
|
# Basic heap sampling
|
|
222
|
-
|
|
246
|
+
benchforge my-bench.ts --heap-sample --iterations 100
|
|
223
247
|
|
|
224
248
|
# Smaller interval = more samples = better coverage of rare allocations
|
|
225
|
-
|
|
249
|
+
benchforge my-bench.ts --heap-sample --heap-interval 4096 --iterations 100
|
|
226
250
|
|
|
227
251
|
# Verbose output with clickable file:// paths
|
|
228
|
-
|
|
252
|
+
benchforge my-bench.ts --heap-sample --heap-verbose
|
|
229
253
|
|
|
230
254
|
# Control call stack display depth
|
|
231
|
-
|
|
255
|
+
benchforge my-bench.ts --heap-sample --heap-stack 5
|
|
232
256
|
```
|
|
233
257
|
|
|
234
258
|
**CLI Options:**
|
|
@@ -238,6 +262,8 @@ simple-cli.ts --heap-sample --heap-stack 5
|
|
|
238
262
|
- `--heap-rows <n>` - Number of top allocation sites to show (default: 20)
|
|
239
263
|
- `--heap-stack <n>` - Call stack depth to display (default: 3)
|
|
240
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)
|
|
241
267
|
|
|
242
268
|
**Output (default compact):**
|
|
243
269
|
```
|
|
@@ -268,6 +294,7 @@ V8's sampling profiler uses Poisson-distributed sampling. When an allocation occ
|
|
|
268
294
|
|
|
269
295
|
**Limitations:**
|
|
270
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).
|
|
271
298
|
- **Statistical sampling**: Results vary between runs. More iterations = more stable results.
|
|
272
299
|
- **~50% filtered**: Node.js internals account for roughly half of allocations. Use "Total (all)" to see the full picture.
|
|
273
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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-
|
|
249
|
+
//# sourceMappingURL=BenchRunner-BzyUfiyB.d.mts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as aggregateGcStats } from "./GcStats-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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"}
|