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.
- package/README.md +40 -6
- 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 +102 -46
- 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-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +2 -1
- 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 +6 -3
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +526 -498
- 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 +18 -18
- package/src/test/TestUtils.ts +24 -24
- 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-Cf_LXwlp.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
|
|
|
@@ -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
|
-
|
|
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"}
|