feat: Enable capture of profiling data (#290)

* feat: Enable capture of profiling data

Adding a new set of commands:
```
agent-browser profiler start

agent-browser profiler stop trace.json
```

With this, agents can start a profiling trace, perform a set of actions, and then extract the profiling data for analysis.

**Note:** I was originally going to call it `agent-browser profile` but I realized that might cause confusion with the `--profile` flag

CDP supports a couple commands for starting/stopping a trace.
When a trace is running, it emits events that need to be picked up.
We store these locally in the daemon until the trace is completed.
When the final event is received, we dump all of them into an output file.

That file can be loaded directly into chrome devtools or another analysis tool to visualize what happened during the agentic run.

Added some basic rust tests for parsing the commands (since they have some optional / required args)

TS daemon adds ~6 tests to make sure the profiling lifecycle (including saving the output file) works as intended

* add docs

* fixes

* fixes

---------

Co-authored-by: Chris Tate <chris@ctate.dev>
This commit is contained in:
Andrew Imm
2026-02-17 23:11:11 -06:00
committed by GitHub
co-authored by Chris Tate
parent 9ca182a4df
commit 59fa36b6e2
14 changed files with 729 additions and 6 deletions
+2
View File
@@ -258,6 +258,8 @@ agent-browser dialog dismiss # Dismiss
```bash
agent-browser trace start [path] # Start recording trace
agent-browser trace stop [path] # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop [path] # Stop and save profile (.json)
agent-browser console # View console messages (log, error, warn, info)
agent-browser console --clear # Clear console
agent-browser errors # View page errors (uncaught JavaScript exceptions)
+104
View File
@@ -765,6 +765,43 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
}
}
// === Profiler (CDP Tracing / Chromium profiling) ===
"profiler" => {
const VALID: &[&str] = &["start", "stop"];
match rest.first().copied() {
Some("start") => {
let mut cmd = json!({ "id": id, "action": "profiler_start" });
if let Some(idx) = rest.iter().position(|s| *s == "--categories") {
if let Some(cats) = rest.get(idx + 1) {
let categories: Vec<&str> = cats.split(',').collect();
cmd["categories"] = json!(categories);
} else {
return Err(ParseError::MissingArguments {
context: "profiler start --categories".to_string(),
usage: "--categories <list>",
});
}
}
Ok(cmd)
}
Some("stop") => {
let mut cmd = json!({ "id": id, "action": "profiler_stop" });
if let Some(path) = rest.get(1) {
cmd["path"] = json!(path);
}
Ok(cmd)
}
Some(sub) => Err(ParseError::UnknownSubcommand {
subcommand: sub.to_string(),
valid_options: VALID,
}),
None => Err(ParseError::MissingArguments {
context: "profiler".to_string(),
usage: "profiler <start|stop> [options]",
}),
}
}
// === Recording (Playwright native video recording) ===
"record" => {
const VALID: &[&str] = &["start", "stop", "restart"];
@@ -2249,6 +2286,73 @@ mod tests {
));
}
// === Profile (CDP Tracing) Tests ===
#[test]
fn test_profiler_start() {
let cmd = parse_command(&args("profiler start"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "profiler_start");
assert!(cmd.get("categories").is_none());
}
#[test]
fn test_profiler_start_with_categories() {
let cmd = parse_command(
&args("profiler start --categories devtools.timeline,v8.execute"),
&default_flags(),
)
.unwrap();
assert_eq!(cmd["action"], "profiler_start");
let categories = cmd["categories"].as_array().unwrap();
assert_eq!(categories.len(), 2);
assert_eq!(categories[0], "devtools.timeline");
assert_eq!(categories[1], "v8.execute");
}
#[test]
fn test_profiler_start_categories_missing_value() {
let result = parse_command(&args("profiler start --categories"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
#[test]
fn test_profiler_stop_with_path() {
let cmd = parse_command(&args("profiler stop trace.json"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "profiler_stop");
assert_eq!(cmd["path"], "trace.json");
}
#[test]
fn test_profiler_stop_no_path() {
let cmd = parse_command(&args("profiler stop"), &default_flags()).unwrap();
assert_eq!(cmd["action"], "profiler_stop");
assert!(cmd.get("path").is_none());
}
#[test]
fn test_profiler_invalid_subcommand() {
let result = parse_command(&args("profiler foo"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::UnknownSubcommand { .. }
));
}
#[test]
fn test_profiler_missing_subcommand() {
let result = parse_command(&args("profiler"), &default_flags());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ParseError::MissingArguments { .. }
));
}
// === Eval Tests ===
#[test]
+60 -2
View File
@@ -275,11 +275,22 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
// Recording start (has "started" field)
if let Some(started) = data.get("started").and_then(|v| v.as_bool()) {
if started {
match action {
Some("profiler_start") => {
println!("{} Profiling started", color::success_indicator());
}
_ => {
if let Some(path) = data.get("path").and_then(|v| v.as_str()) {
println!("{} Recording started: {}", color::success_indicator(), path);
println!(
"{} Recording started: {}",
color::success_indicator(),
path
);
} else {
println!("{} Recording started", color::success_indicator());
}
}
}
return;
}
}
@@ -367,6 +378,12 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
color::success_indicator(),
color::green(path)
),
"profiler_stop" => println!(
"{} Profile saved to {} ({} events)",
color::success_indicator(),
color::green(path),
data.get("eventCount").and_then(|c| c.as_u64()).unwrap_or(0)
),
"har_stop" => println!(
"{} HAR saved to {}",
color::success_indicator(),
@@ -1471,6 +1488,46 @@ Examples:
"##
}
// === Profile (CDP Tracing) ===
"profiler" => {
r##"
agent-browser profiler - Record Chrome DevTools performance profile
Usage: agent-browser profiler <operation> [options]
Record a performance profile using Chrome DevTools Protocol (CDP) Tracing.
The output JSON file can be loaded into Chrome DevTools Performance panel,
Perfetto UI (https://ui.perfetto.dev/), or other trace analysis tools.
Operations:
start Start profiling
stop [path] Stop profiling and save to file
Start Options:
--categories <list> Comma-separated trace categories (default includes
devtools.timeline, v8.execute, blink, and others)
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
# Basic profiling
agent-browser profiler start
agent-browser navigate https://example.com
agent-browser click "#button"
agent-browser profiler stop ./trace.json
# With custom categories
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
agent-browser profiler stop ./custom-trace.json
The output file can be viewed in:
- Chrome DevTools: Performance panel > Load profile
- Perfetto: https://ui.perfetto.dev/
"##
}
// === Record (video) ===
"record" => {
r##"
@@ -1833,7 +1890,8 @@ Tabs:
tab [new|list|close|<n>] Manage tabs
Debug:
trace start|stop [path] Record trace
trace start|stop [path] Record Playwright trace
profiler start|stop [path] Record Chrome DevTools profile
record start <path> [url] Start video recording (WebM)
record stop Stop and save video
console [--clear] View console logs
+2
View File
@@ -176,6 +176,8 @@ agent-browser dialog dismiss # Dismiss dialog
```bash
agent-browser trace start [path] # Start trace
agent-browser trace stop [path] # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop [path] # Stop and save profile (.json)
agent-browser record start <path> # Start video recording (WebM)
agent-browser record stop # Stop and save video
agent-browser record restart <path> # Stop current and start new recording
+102
View File
@@ -0,0 +1,102 @@
export const metadata = { title: "Profiler" }
# Profiler
Capture Chrome DevTools performance profiles during browser automation.
Use profiles to diagnose slow page loads, expensive JavaScript, layout thrashing,
and other performance bottlenecks in agentic workflows.
## Basic usage
```bash
# Start profiling
agent-browser profiler start
# Perform actions
agent-browser navigate https://example.com
agent-browser click "#button"
# Stop and save profile
agent-browser profiler stop ./trace.json
```
The output JSON file can be loaded into Chrome DevTools, Perfetto UI, or any
tool that accepts Chrome Trace Event format.
## Commands
| Command | Description |
|---------|-------------|
| `profiler start` | Start recording a performance profile |
| `profiler start --categories <list>` | Start with custom trace categories |
| `profiler stop [path]` | Stop profiling and save to file |
## Trace categories
The `--categories` flag accepts a comma-separated list of Chrome trace categories.
```bash
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
```
Default categories include `devtools.timeline`, `v8.execute`, `blink`,
`blink.user_timing`, `latencyInfo`, `renderer.scheduler`, `toplevel`, and
several `disabled-by-default-*` categories for detailed CPU profiling and
call stack analysis.
### Common categories
| Category | What it captures |
|----------|-----------------|
| `devtools.timeline` | Standard DevTools performance events |
| `v8.execute` | Time spent running JavaScript |
| `blink` | Renderer events (layout, paint, style) |
| `blink.user_timing` | `performance.mark()` and `performance.measure()` calls |
| `latencyInfo` | Input-to-display latency |
| `disabled-by-default-v8.cpu_profiler` | Sampling-based JS CPU profiling |
## Output format
The output is a JSON file in Chrome Trace Event format:
```json
{
"traceEvents": [
{
"cat": "devtools.timeline",
"name": "RunTask",
"ph": "X",
"ts": 12345,
"dur": 100,
"pid": 1,
"tid": 1
}
],
"metadata": {
"clock-domain": "LINUX_CLOCK_MONOTONIC"
}
}
```
The `metadata.clock-domain` field reflects the host platform (Linux or macOS).
On Windows it is omitted.
## Viewing profiles
- **Chrome DevTools** -- Performance panel > Load profile
- **Perfetto** -- https://ui.perfetto.dev/ (drag and drop the JSON file)
- **Trace Viewer** -- `chrome://tracing` in any Chromium browser
## Use cases
- **Page load analysis** -- Profile navigation to identify slow resources, long tasks, or layout shifts
- **Interaction profiling** -- Measure the cost of clicks, form fills, and other user interactions
- **CI regression checks** -- Capture profiles per build and compare trace data over time
- **Agent workflow optimization** -- Find which steps in an agentic flow are most expensive
## Limitations
- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.
- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.
- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.
- When no output path is provided, the profile is saved to an auto-generated path under the agent-browser temp directory.
+1
View File
@@ -32,6 +32,7 @@ export const navigation: NavSection[] = [
{ name: "Sessions", href: "/sessions" },
{ name: "CDP Mode", href: "/cdp-mode" },
{ name: "Streaming", href: "/streaming" },
{ name: "Profiler", href: "/profiler" },
{ name: "iOS Simulator", href: "/ios" },
],
},
+3
View File
@@ -162,6 +162,8 @@ agent-browser --cdp 9222 snapshot
agent-browser --headed open https://example.com
agent-browser highlight @e1 # Highlight element
agent-browser record start demo.webm # Record session
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop trace.json # Stop and save profile (path optional)
```
### Local Files (PDFs, HTML)
@@ -323,6 +325,7 @@ Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
## Ready-to-Use Templates
@@ -247,6 +247,8 @@ agent-browser errors --clear # Clear errors
agent-browser highlight @e1 # Highlight element
agent-browser trace start # Start recording trace
agent-browser trace stop trace.zip # Stop and save trace
agent-browser profiler start # Start Chrome DevTools profiling
agent-browser profiler stop trace.json # Stop and save profile
```
## Environment Variables
@@ -0,0 +1,120 @@
# Profiling
Capture Chrome DevTools performance profiles during browser automation for performance analysis.
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
## Contents
- [Basic Profiling](#basic-profiling)
- [Profiler Commands](#profiler-commands)
- [Categories](#categories)
- [Use Cases](#use-cases)
- [Output Format](#output-format)
- [Viewing Profiles](#viewing-profiles)
- [Limitations](#limitations)
## Basic Profiling
```bash
# Start profiling
agent-browser profiler start
# Perform actions
agent-browser navigate https://example.com
agent-browser click "#button"
agent-browser wait 1000
# Stop and save
agent-browser profiler stop ./trace.json
```
## Profiler Commands
```bash
# Start profiling with default categories
agent-browser profiler start
# Start with custom trace categories
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
# Stop profiling and save to file
agent-browser profiler stop ./trace.json
```
## Categories
The `--categories` flag accepts a comma-separated list of Chrome trace categories. Default categories include:
- `devtools.timeline` -- standard DevTools performance traces
- `v8.execute` -- time spent running JavaScript
- `blink` -- renderer events
- `blink.user_timing` -- `performance.mark()` / `performance.measure()` calls
- `latencyInfo` -- input-to-latency tracking
- `renderer.scheduler` -- task scheduling and execution
- `toplevel` -- broad-spectrum basic events
Several `disabled-by-default-*` categories are also included for detailed timeline, call stack, and V8 CPU profiling data.
## Use Cases
### Diagnosing Slow Page Loads
```bash
agent-browser profiler start
agent-browser navigate https://app.example.com
agent-browser wait --load networkidle
agent-browser profiler stop ./page-load-profile.json
```
### Profiling User Interactions
```bash
agent-browser navigate https://app.example.com
agent-browser profiler start
agent-browser click "#submit"
agent-browser wait 2000
agent-browser profiler stop ./interaction-profile.json
```
### CI Performance Regression Checks
```bash
#!/bin/bash
agent-browser profiler start
agent-browser navigate https://app.example.com
agent-browser wait --load networkidle
agent-browser profiler stop "./profiles/build-${BUILD_ID}.json"
```
## Output Format
The output is a JSON file in Chrome Trace Event format:
```json
{
"traceEvents": [
{ "cat": "devtools.timeline", "name": "RunTask", "ph": "X", "ts": 12345, "dur": 100, ... },
...
],
"metadata": {
"clock-domain": "LINUX_CLOCK_MONOTONIC"
}
}
```
The `metadata.clock-domain` field is set based on the host platform (Linux or macOS). On Windows it is omitted.
## Viewing Profiles
Load the output JSON file in any of these tools:
- **Chrome DevTools**: Performance panel > Load profile (Ctrl+Shift+I > Performance)
- **Perfetto UI**: https://ui.perfetto.dev/ -- drag and drop the JSON file
- **Trace Viewer**: `chrome://tracing` in any Chromium browser
## Limitations
- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.
- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.
- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.
+31
View File
@@ -65,6 +65,8 @@ import type {
StylesCommand,
TraceStartCommand,
TraceStopCommand,
ProfilerStartCommand,
ProfilerStopCommand,
HarStopCommand,
StorageStateSaveCommand,
StateListCommand,
@@ -355,6 +357,10 @@ export async function executeCommand(command: Command, browser: BrowserManager):
return await handleTraceStart(command, browser);
case 'trace_stop':
return await handleTraceStop(command, browser);
case 'profiler_start':
return await handleProfilerStart(command, browser);
case 'profiler_stop':
return await handleProfilerStop(command, browser);
case 'har_start':
return await handleHarStart(command, browser);
case 'har_stop':
@@ -1448,6 +1454,31 @@ async function handleTraceStop(
);
}
async function handleProfilerStart(
command: ProfilerStartCommand,
browser: BrowserManager
): Promise<Response> {
await browser.startProfiling({ categories: command.categories });
return successResponse(command.id, { started: true });
}
async function handleProfilerStop(
command: ProfilerStopCommand,
browser: BrowserManager
): Promise<Response> {
let outputPath = command.path;
if (!outputPath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8);
const filename = `profile-${timestamp}-${random}.json`;
const profileDir = path.join(getAppDir(), 'tmp', 'profiles');
mkdirSync(profileDir, { recursive: true });
outputPath = path.join(profileDir, filename);
}
const result = await browser.stopProfiling(outputPath);
return successResponse(command.id, result);
}
async function handleHarStart(
command: Command & { action: 'har_start' },
browser: BrowserManager
+77
View File
@@ -866,6 +866,83 @@ describe('BrowserManager', () => {
});
});
describe('profiling (CDP tracing)', () => {
const fs = require('node:fs/promises');
const path = require('node:path');
const testOutputDir = '/tmp/agent-browser-test';
beforeAll(async () => {
// Ensure test output directory exists
await fs.mkdir(testOutputDir, { recursive: true }).catch(() => {});
});
afterEach(async () => {
// Stop profiling if still active
if (browser.isProfilingActive()) {
const tempPath = path.join(testOutputDir, 'cleanup.json');
await browser.stopProfiling(tempPath).catch(() => {});
}
});
it('should report profiling state correctly', () => {
expect(browser.isProfilingActive()).toBe(false);
});
it('should start profiling', async () => {
await browser.startProfiling();
expect(browser.isProfilingActive()).toBe(true);
});
it('should throw when starting profiling twice', async () => {
await browser.startProfiling();
await expect(browser.startProfiling()).rejects.toThrow('Profiling already active');
});
it('should stop profiling and write file', async () => {
await browser.startProfiling();
const outputPath = path.join(testOutputDir, 'test-profile.json');
const result = await browser.stopProfiling(outputPath);
expect(result.path).toBe(outputPath);
expect(typeof result.eventCount).toBe('number');
expect(browser.isProfilingActive()).toBe(false);
// Verify file was written
const fileExists = await fs
.access(outputPath)
.then(() => true)
.catch(() => false);
expect(fileExists).toBe(true);
// Verify file content is valid JSON with traceEvents
const content = await fs.readFile(outputPath, 'utf-8');
const data = JSON.parse(content);
expect(data).toHaveProperty('traceEvents');
expect(Array.isArray(data.traceEvents)).toBe(true);
// Cleanup
await fs.unlink(outputPath).catch(() => {});
});
it('should throw when stopping without start', async () => {
expect(browser.isProfilingActive()).toBe(false);
await expect(browser.stopProfiling('/tmp/should-not-exist.json')).rejects.toThrow(
'No profiling session active'
);
});
it('should start profiling with custom categories', async () => {
await browser.startProfiling({ categories: ['devtools.timeline', 'v8.execute'] });
expect(browser.isProfilingActive()).toBe(true);
// Stop and cleanup
const outputPath = path.join(testOutputDir, 'custom-categories.json');
await browser.stopProfiling(outputPath);
await fs.unlink(outputPath).catch(() => {});
});
});
describe('input injection', () => {
it('should inject mouse move event', async () => {
await expect(
+181 -1
View File
@@ -17,7 +17,8 @@ import {
import path from 'node:path';
import os from 'node:os';
import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs';
import type { LaunchCommand } from './types.js';
import { writeFile, mkdir } from 'node:fs/promises';
import type { LaunchCommand, TraceEvent } from './types.js';
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
import { safeHeaderMerge } from './state-utils.js';
import {
@@ -120,6 +121,15 @@ export class BrowserManager {
return warnings;
}
// CDP profiling state
private static readonly MAX_PROFILE_EVENTS = 5_000_000;
private profilingActive: boolean = false;
private profileChunks: TraceEvent[] = [];
private profileEventsDropped: boolean = false;
private profileCompleteResolver: (() => void) | null = null;
private profileDataHandler: ((params: { value?: TraceEvent[] }) => void) | null = null;
private profileCompleteHandler: (() => void) | null = null;
/**
* Check if browser is launched
*/
@@ -1818,6 +1828,156 @@ export class BrowserManager {
this.screencastFrameHandler = null;
}
/**
* Check if profiling is currently active
*/
isProfilingActive(): boolean {
return this.profilingActive;
}
/**
* Start CDP profiling (Tracing)
*/
async startProfiling(options?: { categories?: string[] }): Promise<void> {
if (this.profilingActive) {
throw new Error('Profiling already active');
}
const cdp = await this.getCDPSession();
const dataHandler = (params: { value?: TraceEvent[] }) => {
if (params.value) {
for (const evt of params.value) {
if (this.profileChunks.length >= BrowserManager.MAX_PROFILE_EVENTS) {
if (!this.profileEventsDropped) {
this.profileEventsDropped = true;
console.warn(
`Profiling: exceeded ${BrowserManager.MAX_PROFILE_EVENTS} events, dropping further data`
);
}
return;
}
this.profileChunks.push(evt);
}
}
};
const completeHandler = () => {
if (this.profileCompleteResolver) {
this.profileCompleteResolver();
}
};
cdp.on('Tracing.dataCollected', dataHandler);
cdp.on('Tracing.tracingComplete', completeHandler);
const categories = options?.categories ?? [
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame',
'disabled-by-default-devtools.timeline.stack',
'v8.execute',
'disabled-by-default-v8.cpu_profiler',
'disabled-by-default-v8.cpu_profiler.hires',
'v8',
'disabled-by-default-v8.runtime_stats',
'blink',
'blink.user_timing',
'latencyInfo',
'renderer.scheduler',
'sequence_manager',
'toplevel',
];
try {
await cdp.send('Tracing.start', {
traceConfig: {
includedCategories: categories,
enableSampling: true,
},
transferMode: 'ReportEvents',
});
} catch (error) {
cdp.off('Tracing.dataCollected', dataHandler);
cdp.off('Tracing.tracingComplete', completeHandler);
throw error;
}
// Only commit state after the CDP call succeeds
this.profilingActive = true;
this.profileChunks = [];
this.profileEventsDropped = false;
this.profileDataHandler = dataHandler;
this.profileCompleteHandler = completeHandler;
}
/**
* Stop CDP profiling and save to file
*/
async stopProfiling(outputPath: string): Promise<{ path: string; eventCount: number }> {
if (!this.profilingActive) {
throw new Error('No profiling session active');
}
const cdp = await this.getCDPSession();
const TRACE_TIMEOUT_MS = 30_000;
const completePromise = new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('Profiling data collection timed out')),
TRACE_TIMEOUT_MS
);
this.profileCompleteResolver = () => {
clearTimeout(timer);
resolve();
};
});
await cdp.send('Tracing.end');
let chunks: TraceEvent[];
try {
await completePromise;
chunks = this.profileChunks;
} finally {
if (this.profileDataHandler) {
cdp.off('Tracing.dataCollected', this.profileDataHandler);
}
if (this.profileCompleteHandler) {
cdp.off('Tracing.tracingComplete', this.profileCompleteHandler);
}
this.profilingActive = false;
this.profileChunks = [];
this.profileEventsDropped = false;
this.profileCompleteResolver = null;
this.profileDataHandler = null;
this.profileCompleteHandler = null;
}
const clockDomain =
process.platform === 'linux'
? 'LINUX_CLOCK_MONOTONIC'
: process.platform === 'darwin'
? 'MAC_MACH_ABSOLUTE_TIME'
: undefined;
const traceData: Record<string, unknown> = {
traceEvents: chunks,
};
if (clockDomain) {
traceData.metadata = { 'clock-domain': clockDomain };
}
const dir = path.dirname(outputPath);
await mkdir(dir, { recursive: true });
await writeFile(outputPath, JSON.stringify(traceData));
const eventCount = chunks.length;
return { path: outputPath, eventCount };
}
/**
* Inject a mouse event via CDP
*/
@@ -2131,6 +2291,26 @@ export class BrowserManager {
await this.stopScreencast();
}
// Clean up profiling state if active (without saving)
if (this.profilingActive) {
const cdp = this.cdpSession;
if (cdp) {
if (this.profileDataHandler) {
cdp.off('Tracing.dataCollected', this.profileDataHandler);
}
if (this.profileCompleteHandler) {
cdp.off('Tracing.tracingComplete', this.profileCompleteHandler);
}
await cdp.send('Tracing.end').catch(() => {});
}
this.profilingActive = false;
this.profileChunks = [];
this.profileEventsDropped = false;
this.profileCompleteResolver = null;
this.profileDataHandler = null;
this.profileCompleteHandler = null;
}
// Clean up CDP session
if (this.cdpSession) {
await this.cdpSession.detach().catch(() => {});
+12
View File
@@ -374,6 +374,16 @@ const traceStopSchema = baseCommandSchema.extend({
path: z.string().min(1).optional(),
});
const profilerStartSchema = baseCommandSchema.extend({
action: z.literal('profiler_start'),
categories: z.array(z.string()).optional(),
});
const profilerStopSchema = baseCommandSchema.extend({
action: z.literal('profiler_stop'),
path: z.string().min(1).optional(),
});
const harStartSchema = baseCommandSchema.extend({
action: z.literal('har_start'),
});
@@ -898,6 +908,8 @@ const commandSchema = z.discriminatedUnion('action', [
recordingRestartSchema,
traceStartSchema,
traceStopSchema,
profilerStartSchema,
profilerStopSchema,
harStartSchema,
harStopSchema,
stateSaveSchema,
+29
View File
@@ -579,6 +579,33 @@ export interface TraceStopCommand extends BaseCommand {
path?: string;
}
/**
* Chrome Trace Event format. All fields are optional because CDP trace event
* shapes vary across categories and event phases -- this type is intentionally
* loose to accept any valid trace event without data loss.
*/
export interface TraceEvent {
cat?: string;
name?: string;
ph?: string;
pid?: number;
tid?: number;
ts?: number;
dur?: number;
args?: Record<string, unknown>;
[key: string]: unknown;
}
export interface ProfilerStartCommand extends BaseCommand {
action: 'profiler_start';
categories?: string[]; // Optional trace categories (e.g., "devtools.timeline")
}
export interface ProfilerStopCommand extends BaseCommand {
action: 'profiler_stop';
path?: string;
}
// HAR recording
export interface HarStartCommand extends BaseCommand {
action: 'har_start';
@@ -924,6 +951,8 @@ export type Command =
| RecordingRestartCommand
| TraceStartCommand
| TraceStopCommand
| ProfilerStartCommand
| ProfilerStopCommand
| HarStartCommand
| HarStopCommand
| StorageStateSaveCommand