Skip to content

Commit 68ae2f8

Browse files
authored
feat: support filePath in performance tools (#686)
Closes #680
1 parent 0abd95c commit 68ae2f8

File tree

3 files changed

+121
-7
lines changed

3 files changed

+121
-7
lines changed

docs/tool-reference.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,17 @@
234234

235235
- **autoStop** (boolean) **(required)**: Determines if the trace recording should be automatically stopped.
236236
- **reload** (boolean) **(required)**: Determines if, once tracing has started, the page should be automatically reloaded.
237+
- **filePath** (string) _(optional)_: The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).
237238

238239
---
239240

240241
### `performance_stop_trace`
241242

242243
**Description:** Stops the active performance trace recording on the selected page.
243244

244-
**Parameters:** None
245+
**Parameters:**
246+
247+
- **filePath** (string) _(optional)_: The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).
245248

246249
---
247250

src/tools/performance.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import zlib from 'node:zlib';
8+
79
import {logger} from '../logger.js';
810
import {zod} from '../third_party/index.js';
911
import type {Page} from '../third_party/index.js';
@@ -19,13 +21,20 @@ import {ToolCategory} from './categories.js';
1921
import type {Context, Response} from './ToolDefinition.js';
2022
import {defineTool} from './ToolDefinition.js';
2123

24+
const filePathSchema = zod
25+
.string()
26+
.optional()
27+
.describe(
28+
'The absolute file path, or a file path relative to the current working directory, to save the raw trace data. For example, trace.json.gz (compressed) or trace.json (uncompressed).',
29+
);
30+
2231
export const startTrace = defineTool({
2332
name: 'performance_start_trace',
2433
description:
2534
'Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.',
2635
annotations: {
2736
category: ToolCategory.PERFORMANCE,
28-
readOnlyHint: true,
37+
readOnlyHint: false,
2938
},
3039
schema: {
3140
reload: zod
@@ -38,6 +47,7 @@ export const startTrace = defineTool({
3847
.describe(
3948
'Determines if the trace recording should be automatically stopped.',
4049
),
50+
filePath: filePathSchema,
4151
},
4252
handler: async (request, response, context) => {
4353
if (context.isRunningPerformanceTrace()) {
@@ -91,7 +101,12 @@ export const startTrace = defineTool({
91101

92102
if (request.params.autoStop) {
93103
await new Promise(resolve => setTimeout(resolve, 5_000));
94-
await stopTracingAndAppendOutput(page, response, context);
104+
await stopTracingAndAppendOutput(
105+
page,
106+
response,
107+
context,
108+
request.params.filePath,
109+
);
95110
} else {
96111
response.appendResponseLine(
97112
`The performance trace is being recorded. Use performance_stop_trace to stop it.`,
@@ -106,15 +121,22 @@ export const stopTrace = defineTool({
106121
'Stops the active performance trace recording on the selected page.',
107122
annotations: {
108123
category: ToolCategory.PERFORMANCE,
109-
readOnlyHint: true,
124+
readOnlyHint: false,
125+
},
126+
schema: {
127+
filePath: filePathSchema,
110128
},
111-
schema: {},
112-
handler: async (_request, response, context) => {
129+
handler: async (request, response, context) => {
113130
if (!context.isRunningPerformanceTrace()) {
114131
return;
115132
}
116133
const page = context.getSelectedPage();
117-
await stopTracingAndAppendOutput(page, response, context);
134+
await stopTracingAndAppendOutput(
135+
page,
136+
response,
137+
context,
138+
request.params.filePath,
139+
);
118140
},
119141
});
120142

@@ -165,9 +187,28 @@ async function stopTracingAndAppendOutput(
165187
page: Page,
166188
response: Response,
167189
context: Context,
190+
filePath?: string,
168191
): Promise<void> {
169192
try {
170193
const traceEventsBuffer = await page.tracing.stop();
194+
if (filePath && traceEventsBuffer) {
195+
let dataToWrite: Uint8Array = traceEventsBuffer;
196+
if (filePath.endsWith('.gz')) {
197+
dataToWrite = await new Promise((resolve, reject) => {
198+
zlib.gzip(traceEventsBuffer, (error, result) => {
199+
if (error) {
200+
reject(error);
201+
} else {
202+
resolve(result);
203+
}
204+
});
205+
});
206+
}
207+
const file = await context.saveFile(dataToWrite, filePath);
208+
response.appendResponseLine(
209+
`The raw trace data was saved to ${file.filename}.`,
210+
);
211+
}
171212
const result = await parseRawTraceBuffer(traceEventsBuffer);
172213
response.appendResponseLine('The performance trace has been stopped.');
173214
if (traceResultIsSuccess(result)) {

tests/tools/performance.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import assert from 'node:assert';
88
import {describe, it, afterEach} from 'node:test';
9+
import zlib from 'node:zlib';
910

1011
import sinon from 'sinon';
1112

@@ -138,6 +139,49 @@ describe('performance', () => {
138139
);
139140
});
140141
});
142+
143+
it.only('supports filePath', async () => {
144+
const rawData = loadTraceAsBuffer('basic-trace.json.gz');
145+
// rawData is the decompressed buffer (based on loadTraceAsBuffer implementation).
146+
// We want to simulate saving it as a .gz file, so the tool should compress it.
147+
const expectedCompressedData = zlib.gzipSync(rawData);
148+
149+
await withMcpContext(async (response, context) => {
150+
const filePath = 'test-trace.json.gz';
151+
const selectedPage = context.getSelectedPage();
152+
sinon.stub(selectedPage, 'url').callsFake(() => 'https://www.test.com');
153+
sinon.stub(selectedPage, 'goto').callsFake(() => Promise.resolve(null));
154+
sinon.stub(selectedPage.tracing, 'start');
155+
sinon.stub(selectedPage.tracing, 'stop').resolves(rawData);
156+
const saveFileStub = sinon
157+
.stub(context, 'saveFile')
158+
.resolves({filename: filePath});
159+
160+
const handlerPromise = startTrace.handler(
161+
{params: {reload: true, autoStop: true, filePath}},
162+
response,
163+
context,
164+
);
165+
// In the handler we wait 5 seconds after the page load event (which is
166+
// what DevTools does), hence we now fake-progress time to allow
167+
// the handler to complete. We allow extra time because the Trace
168+
// Engine also uses some timers to yield updates and we need those to
169+
// execute.
170+
await handlerPromise;
171+
172+
assert.ok(
173+
response.responseLines.includes(
174+
`The raw trace data was saved to ${filePath}.`,
175+
),
176+
);
177+
sinon.assert.calledOnce(saveFileStub);
178+
const [savedData, savedPath] = saveFileStub.firstCall.args;
179+
assert.strictEqual(savedPath, filePath);
180+
// Compare the saved data with expected compressed data
181+
// We can't compare buffers directly with strictEqual easily if they are different instances, but deepStrictEqual works for Buffers.
182+
assert.deepStrictEqual(savedData, expectedCompressedData);
183+
});
184+
});
141185
});
142186

143187
describe('performance_analyze_insight', () => {
@@ -275,5 +319,31 @@ describe('performance', () => {
275319
t.assert.snapshot?.(response.responseLines.join('\n'));
276320
});
277321
});
322+
323+
it('supports filePath', async () => {
324+
const rawData = loadTraceAsBuffer('basic-trace.json.gz');
325+
await withMcpContext(async (response, context) => {
326+
const filePath = 'test-trace.json';
327+
context.setIsRunningPerformanceTrace(true);
328+
const selectedPage = context.getSelectedPage();
329+
const stopTracingStub = sinon
330+
.stub(selectedPage.tracing, 'stop')
331+
.resolves(rawData);
332+
const saveFileStub = sinon
333+
.stub(context, 'saveFile')
334+
.resolves({filename: filePath});
335+
336+
await stopTrace.handler({params: {filePath}}, response, context);
337+
338+
sinon.assert.calledOnce(stopTracingStub);
339+
sinon.assert.calledOnce(saveFileStub);
340+
sinon.assert.calledWith(saveFileStub, rawData, filePath);
341+
assert.ok(
342+
response.responseLines.includes(
343+
`The raw trace data was saved to ${filePath}.`,
344+
),
345+
);
346+
});
347+
});
278348
});
279349
});

0 commit comments

Comments
 (0)