Skip to content

Commit b601b77

Browse files
committed
feat: add performance_analyze_file tool
1 parent 2c1061b commit b601b77

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
299299
- **Emulation** (2 tools)
300300
- [`emulate`](docs/tool-reference.md#emulate)
301301
- [`resize_page`](docs/tool-reference.md#resize_page)
302-
- **Performance** (3 tools)
302+
- **Performance** (4 tools)
303+
- [`performance_analyze_file`](docs/tool-reference.md#performance_analyze_file)
303304
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
304305
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
305306
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)

docs/tool-reference.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
- **[Emulation](#emulation)** (2 tools)
2222
- [`emulate`](#emulate)
2323
- [`resize_page`](#resize_page)
24-
- **[Performance](#performance)** (3 tools)
24+
- **[Performance](#performance)** (4 tools)
25+
- [`performance_analyze_file`](#performance_analyze_file)
2526
- [`performance_analyze_insight`](#performance_analyze_insight)
2627
- [`performance_start_trace`](#performance_start_trace)
2728
- [`performance_stop_trace`](#performance_stop_trace)
@@ -213,6 +214,16 @@
213214

214215
## Performance
215216

217+
### `performance_analyze_file`
218+
219+
**Description:** Analyzes a performance trace file from the local filesystem. This can be used to analyze traces exported from Chrome DevTools, Lighthouse, or other tools that generate Chrome trace format files.
220+
221+
**Parameters:**
222+
223+
- **filePath** (string) **(required)**: The absolute path to the trace file to analyze. Supports JSON trace files in Chrome trace format (e.g., trace.json, lighthouse-0.trace.json).
224+
225+
---
226+
216227
### `performance_analyze_insight`
217228

218229
**Description:** Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.

src/tools/performance.ts

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

7+
import fs from 'node:fs/promises';
8+
79
import {logger} from '../logger.js';
810
import {zod} from '../third_party/index.js';
911
import type {Page} from '../third_party/index.js';
@@ -161,6 +163,50 @@ export const analyzeInsight = defineTool({
161163
},
162164
});
163165

166+
export const analyzeFile = defineTool({
167+
name: 'performance_analyze_file',
168+
description:
169+
'Analyzes a performance trace file from the local filesystem. This can be used to analyze traces exported from Chrome DevTools, Lighthouse, or other tools that generate Chrome trace format files.',
170+
annotations: {
171+
category: ToolCategory.PERFORMANCE,
172+
readOnlyHint: true,
173+
},
174+
schema: {
175+
filePath: zod
176+
.string()
177+
.describe(
178+
'The absolute path to the trace file to analyze. Supports JSON trace files in Chrome trace format (e.g., trace.json, lighthouse-0.trace.json).',
179+
),
180+
},
181+
handler: async (request, response, context) => {
182+
const {filePath} = request.params;
183+
184+
try {
185+
const buffer = await fs.readFile(filePath);
186+
const result = await parseRawTraceBuffer(new Uint8Array(buffer));
187+
188+
if (traceResultIsSuccess(result)) {
189+
context.storeTraceRecording(result);
190+
const traceSummaryText = getTraceSummary(result);
191+
response.appendResponseLine(
192+
`Successfully analyzed trace file: ${filePath}`,
193+
);
194+
response.appendResponseLine(traceSummaryText);
195+
} else {
196+
response.appendResponseLine(
197+
'There was an error parsing the trace file:',
198+
);
199+
response.appendResponseLine(result.error);
200+
}
201+
} catch (e) {
202+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
203+
logger(`Error reading trace file: ${errorText}`);
204+
response.appendResponseLine('An error occurred reading the trace file:');
205+
response.appendResponseLine(errorText);
206+
}
207+
},
208+
});
209+
164210
async function stopTracingAndAppendOutput(
165211
page: Page,
166212
response: Response,

tests/tools/performance.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
*/
66

77
import assert from 'node:assert';
8+
import fs from 'node:fs';
9+
import os from 'node:os';
10+
import path from 'node:path';
811
import {describe, it, afterEach} from 'node:test';
912

1013
import sinon from 'sinon';
1114

1215
import {
16+
analyzeFile,
1317
analyzeInsight,
1418
startTrace,
1519
stopTrace,
@@ -276,4 +280,79 @@ describe('performance', () => {
276280
});
277281
});
278282
});
283+
284+
describe('performance_analyze_file', () => {
285+
it('analyzes a trace file from the filesystem', async () => {
286+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
287+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-'));
288+
const tempFilePath = path.join(tempDir, 'trace.json');
289+
fs.writeFileSync(tempFilePath, rawData);
290+
291+
try {
292+
await withMcpContext(async (response, context) => {
293+
await analyzeFile.handler(
294+
{params: {filePath: tempFilePath}},
295+
response,
296+
context,
297+
);
298+
299+
const output = response.responseLines.join('\n');
300+
assert.ok(
301+
output.includes(
302+
`Successfully analyzed trace file: ${tempFilePath}`,
303+
),
304+
);
305+
assert.ok(
306+
output.includes('## Summary of Performance trace findings'),
307+
);
308+
assert.ok(output.includes('URL: https://web.dev/'));
309+
assert.strictEqual(context.recordedTraces().length, 1);
310+
});
311+
} finally {
312+
fs.rmSync(tempDir, {recursive: true});
313+
}
314+
});
315+
316+
it('returns an error if the file does not exist', async () => {
317+
await withMcpContext(async (response, context) => {
318+
await analyzeFile.handler(
319+
{params: {filePath: '/nonexistent/path/trace.json'}},
320+
response,
321+
context,
322+
);
323+
324+
assert.ok(
325+
response.responseLines
326+
.join('\n')
327+
.includes('An error occurred reading the trace file:'),
328+
);
329+
assert.strictEqual(context.recordedTraces().length, 0);
330+
});
331+
});
332+
333+
it('returns an error if the file is not valid JSON', async () => {
334+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-'));
335+
const tempFilePath = path.join(tempDir, 'invalid.json');
336+
fs.writeFileSync(tempFilePath, 'not valid json');
337+
338+
try {
339+
await withMcpContext(async (response, context) => {
340+
await analyzeFile.handler(
341+
{params: {filePath: tempFilePath}},
342+
response,
343+
context,
344+
);
345+
346+
assert.ok(
347+
response.responseLines
348+
.join('\n')
349+
.includes('There was an error parsing the trace file:'),
350+
);
351+
assert.strictEqual(context.recordedTraces().length, 0);
352+
});
353+
} finally {
354+
fs.rmSync(tempDir, {recursive: true});
355+
}
356+
});
357+
});
279358
});

0 commit comments

Comments
 (0)