Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions scripts/profile/scenarios/performance_trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {ScenarioArgs, ScenarioIterations, ToolCall} from '../types.ts';

/**
* Defines iteration counts for the performance trace profiling scenario.
*
* Keeps iteration counts bounded so profiling remains fast while still providing
* enough repetitions to detect memory retention across consecutive traces.
*/
export function getNumIterations(): ScenarioIterations {
return {
iterations: 5,
warmupIterations: 3,
};
}

/**
* Returns the sequence of MCP tool calls to execute per iteration.
*
* Each iteration navigates to the target URL to ensure a known page state,
* begins a performance trace with page reload (`autoStop: false` to avoid
* the default 5-second sleep), and explicitly stops the trace to trigger
* parsing, summary generation, and model teardown.
*
* @param args Arguments supplied to the scenario, including the target URL.
*/
export function get(args: ScenarioArgs): ToolCall[] {
return [
{
name: 'navigate_page',
arguments: {type: 'url', url: args.targetUrl},
},
{
name: 'performance_start_trace',
arguments: {reload: true, autoStop: false},
},
{
name: 'performance_stop_trace',
arguments: {},
},
];
}
21 changes: 17 additions & 4 deletions src/processors/PerformanceTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import {DevTools} from '../third_party/index.js';
import {logger} from '../utils/logger.js';

const engine = DevTools.TraceEngine.TraceModel.Model.createWithAllHandlers();

export interface TraceResult {
parsedTrace: DevTools.TraceEngine.TraceModel.ParsedTrace;
insights: DevTools.TraceEngine.Insights.Types.TraceInsightSets | null;
Expand All @@ -24,14 +22,24 @@ export interface TraceParseError {
error: string;
}

/**
* Parses raw JSON trace buffer bytes into a DevTools TraceEngine representation.
*
* Accepts either a JSON array of trace events or an object with a `traceEvents` field.
* A new trace engine model is created per call to ensure session isolation and prevent
* memory retention.
*
* @param buffer Raw UTF-8 encoded JSON bytes representing trace data.
* @param metadata Optional throttling configurations applied during recording.
* @returns A {@link TraceResult} with parsed traces and insights, or a {@link TraceParseError} on failure.
*/
export async function parseRawTraceBuffer(
buffer: Uint8Array<ArrayBufferLike> | undefined,
metadata?: {
cpuThrottling?: number;
networkThrottling?: string;
},
): Promise<TraceResult | TraceParseError> {
engine.resetProcessor();
if (!buffer) {
return {
error: 'No buffer was provided.',
Expand All @@ -51,6 +59,11 @@ export async function parseRawTraceBuffer(
| DevTools.TraceEngine.Types.Events.Event[];

const events = Array.isArray(data) ? data : data.traceEvents;
// Instantiate a fresh TraceModel per invocation because Model permanently
// retains parsed traces in its internal `#traces` array, which causes an
// unbounded memory leak if reused across sessions.
const engine =
DevTools.TraceEngine.TraceModel.Model.createWithAllHandlers();
await engine.parse(events, {metadata});
const parsedTrace = engine.parsedTrace();
if (!parsedTrace) {
Expand All @@ -59,7 +72,7 @@ export async function parseRawTraceBuffer(
};
}

const insights = parsedTrace?.insights ?? null;
const insights = parsedTrace.insights ?? null;

return {
parsedTrace,
Expand Down
48 changes: 41 additions & 7 deletions tests/trace-processing/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,58 @@
*/

import assert from 'node:assert';
import {describe, it} from 'node:test';
import {afterEach, describe, it} from 'node:test';

import sinon from 'sinon';

import {
getTraceSummary,
parseRawTraceBuffer,
} from '../../src/processors/PerformanceTrace.js';
import {DevTools} from '../../src/third_party/index.js';

import {loadTraceAsBuffer} from './fixtures/load.js';

describe('Trace parsing', async () => {
it('can parse a Uint8Array from Tracing.stop())', async () => {
describe('Trace parsing', () => {
afterEach(() => {
sinon.restore();
});

it('creates an isolated model instance for each parsed trace', async () => {
const createModelSpy = sinon.spy(
DevTools.TraceEngine.TraceModel.Model,
'createWithAllHandlers',
);
const rawData = loadTraceAsBuffer('basic-trace.json.gz');

const result1 = await parseRawTraceBuffer(rawData);
const result2 = await parseRawTraceBuffer(rawData);

if ('error' in result1) {
assert.fail(`Unexpected parse failure on first trace: ${result1.error}`);
}
if ('error' in result2) {
assert.fail(`Unexpected parse failure on second trace: ${result2.error}`);
}

sinon.assert.calledTwice(createModelSpy);
const firstModel = createModelSpy.firstCall.returnValue;
const secondModel = createModelSpy.secondCall.returnValue;
assert.notStrictEqual(firstModel, secondModel);
// Verify that each model instance retains only its own trace. If the model
// were shared, subsequent parses would accumulate in `#traces` and increase size.
assert.strictEqual(firstModel.size(), 1);
assert.strictEqual(secondModel.size(), 1);
});

it('can parse a Uint8Array from Tracing.stop()', async () => {
const rawData = loadTraceAsBuffer('basic-trace.json.gz');
const result = await parseRawTraceBuffer(rawData);
if ('error' in result) {
assert.fail(`Unexpected parse failure: ${result.error}`);
}
assert.ok(result?.parsedTrace);
assert.ok(result?.insights);
assert.ok(result.parsedTrace);
assert.ok(result.insights);
});

it('can format results of a trace', async t => {
Expand All @@ -31,8 +65,8 @@ describe('Trace parsing', async () => {
if ('error' in result) {
assert.fail(`Unexpected parse failure: ${result.error}`);
}
assert.ok(result?.parsedTrace);
assert.ok(result?.insights);
assert.ok(result.parsedTrace);
assert.ok(result.insights);

const output = getTraceSummary(result);
t.assert.snapshot(output);
Expand Down