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 +15 -129
- package/dist/{BenchRunner-BLfGX2wQ.d.mts → BenchRunner-CSKN9zPy.d.mts} +1 -1
- package/dist/BrowserHeapSampler-DQwmmuDu.mjs +187 -0
- package/dist/BrowserHeapSampler-DQwmmuDu.mjs.map +1 -0
- package/dist/GcStats-ByEovUi1.mjs +77 -0
- package/dist/GcStats-ByEovUi1.mjs.map +1 -0
- package/dist/{HeapSampler-BX3de22o.mjs → HeapSampler-B8dtKHn1.mjs} +1 -1
- package/dist/{HeapSampler-BX3de22o.mjs.map → HeapSampler-B8dtKHn1.mjs.map} +1 -1
- package/dist/{TimingUtils-D4z1jpp2.mjs → TimingUtils-ClclVQ7E.mjs} +276 -278
- package/dist/TimingUtils-ClclVQ7E.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/index.d.mts +10 -6
- package/dist/index.mjs +2 -2
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +2 -2
- package/dist/{src-D7zxOFGA.mjs → src-B06_i1RD.mjs} +21 -270
- package/dist/src-B06_i1RD.mjs.map +1 -0
- package/package.json +10 -2
- package/src/StandardSections.ts +1 -8
- package/src/browser/BrowserHeapSampler.ts +3 -2
- package/src/cli/CliArgs.ts +4 -3
- package/src/cli/RunBenchCLI.ts +16 -8
- package/src/runners/BasicRunner.ts +0 -4
- package/dist/TimingUtils-D4z1jpp2.mjs.map +0 -1
- package/dist/src-D7zxOFGA.mjs.map +0 -1
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 │
|
|
157
|
-
|
|
158
|
-
║ quicksort │ 0.17 +5.5% [+4.7%, +6.2%] 0.15 0.63 │
|
|
159
|
-
║ insertion sort │ 0.24 +25.9% [+25.3%, +27.4%] 0.18 0.36 │
|
|
160
|
-
║ --> native sort │ 0.16 0.15 0.41 │
|
|
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
|
|
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
|
-
|
|
290
|
+
## Interpreting Results
|
|
292
291
|
|
|
293
|
-
|
|
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
|
-
|
|
296
|
+
The benchmark is 5.5% slower than baseline, with a bootstrap confidence interval of [+4.7%, +6.2%].
|
|
388
297
|
|
|
389
|
-
|
|
298
|
+
### Percentiles
|
|
390
299
|
```
|
|
391
300
|
p50: 0.15ms, p99: 0.27ms
|
|
392
301
|
```
|
|
393
|
-
|
|
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-
|
|
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":"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"}
|
|
@@ -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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HeapSampler-
|
|
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"}
|