Skip to content

Commit aa6509c

Browse files
committed
feat: support filePath in performance tools
1 parent e8c9192 commit aa6509c

3 files changed

Lines changed: 116 additions & 7 deletions

File tree

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: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import util from 'node:util';
8+
import zlib from 'node:zlib';
9+
710
import {logger} from '../logger.js';
811
import {zod} from '../third_party/index.js';
912
import type {Page} from '../third_party/index.js';
@@ -19,13 +22,20 @@ import {ToolCategory} from './categories.js';
1922
import type {Context, Response} from './ToolDefinition.js';
2023
import {defineTool} from './ToolDefinition.js';
2124

25+
const filePathSchema = zod
26+
.string()
27+
.optional()
28+
.describe(
29+
'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).',
30+
);
31+
2232
export const startTrace = defineTool({
2333
name: 'performance_start_trace',
2434
description:
2535
'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.',
2636
annotations: {
2737
category: ToolCategory.PERFORMANCE,
28-
readOnlyHint: true,
38+
readOnlyHint: false,
2939
},
3040
schema: {
3141
reload: zod
@@ -38,6 +48,7 @@ export const startTrace = defineTool({
3848
.describe(
3949
'Determines if the trace recording should be automatically stopped.',
4050
),
51+
filePath: filePathSchema,
4152
},
4253
handler: async (request, response, context) => {
4354
if (context.isRunningPerformanceTrace()) {
@@ -91,7 +102,12 @@ export const startTrace = defineTool({
91102

92103
if (request.params.autoStop) {
93104
await new Promise(resolve => setTimeout(resolve, 5_000));
94-
await stopTracingAndAppendOutput(page, response, context);
105+
await stopTracingAndAppendOutput(
106+
page,
107+
response,
108+
context,
109+
request.params.filePath,
110+
);
95111
} else {
96112
response.appendResponseLine(
97113
`The performance trace is being recorded. Use performance_stop_trace to stop it.`,
@@ -106,15 +122,22 @@ export const stopTrace = defineTool({
106122
'Stops the active performance trace recording on the selected page.',
107123
annotations: {
108124
category: ToolCategory.PERFORMANCE,
109-
readOnlyHint: true,
125+
readOnlyHint: false,
126+
},
127+
schema: {
128+
filePath: filePathSchema,
110129
},
111-
schema: {},
112-
handler: async (_request, response, context) => {
130+
handler: async (request, response, context) => {
113131
if (!context.isRunningPerformanceTrace()) {
114132
return;
115133
}
116134
const page = context.getSelectedPage();
117-
await stopTracingAndAppendOutput(page, response, context);
135+
await stopTracingAndAppendOutput(
136+
page,
137+
response,
138+
context,
139+
request.params.filePath,
140+
);
118141
},
119142
});
120143

@@ -165,9 +188,21 @@ async function stopTracingAndAppendOutput(
165188
page: Page,
166189
response: Response,
167190
context: Context,
191+
filePath?: string,
168192
): Promise<void> {
169193
try {
170194
const traceEventsBuffer = await page.tracing.stop();
195+
if (filePath && traceEventsBuffer) {
196+
let dataToWrite: Uint8Array = traceEventsBuffer;
197+
if (filePath.endsWith('.gz')) {
198+
const gzip = util.promisify(zlib.gzip);
199+
dataToWrite = await gzip(traceEventsBuffer);
200+
}
201+
const file = await context.saveFile(dataToWrite, filePath);
202+
response.appendResponseLine(
203+
`The raw trace data was saved to ${file.filename}.`,
204+
);
205+
}
171206
const result = await parseRawTraceBuffer(traceEventsBuffer);
172207
response.appendResponseLine('The performance trace has been stopped.');
173208
if (traceResultIsSuccess(result)) {

tests/tools/performance.test.ts

Lines changed: 71 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,50 @@ describe('performance', () => {
138139
);
139140
});
140141
});
142+
143+
it('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 clock = sinon.useFakeTimers({shouldClearNativeTimers: true});
161+
try {
162+
const handlerPromise = startTrace.handler(
163+
{params: {reload: true, autoStop: true, filePath}},
164+
response,
165+
context,
166+
);
167+
await clock.tickAsync(6_000);
168+
await handlerPromise;
169+
170+
assert.ok(
171+
response.responseLines.includes(
172+
`The raw trace data was saved to ${filePath}.`,
173+
),
174+
);
175+
sinon.assert.calledOnce(saveFileStub);
176+
const [savedData, savedPath] = saveFileStub.firstCall.args;
177+
assert.strictEqual(savedPath, filePath);
178+
// Compare the saved data with expected compressed data
179+
// We can't compare buffers directly with strictEqual easily if they are different instances, but deepStrictEqual works for Buffers.
180+
assert.deepStrictEqual(savedData, expectedCompressedData);
181+
} finally {
182+
clock.restore();
183+
}
184+
});
185+
});
141186
});
142187

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

0 commit comments

Comments
 (0)