diff --git a/README.md b/README.md index 5864b073..e18d5080 100644 --- a/README.md +++ b/README.md @@ -299,7 +299,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles - **Emulation** (2 tools) - [`emulate`](docs/tool-reference.md#emulate) - [`resize_page`](docs/tool-reference.md#resize_page) -- **Performance** (3 tools) +- **Performance** (4 tools) + - [`performance_analyze_file`](docs/tool-reference.md#performance_analyze_file) - [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight) - [`performance_start_trace`](docs/tool-reference.md#performance_start_trace) - [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 0e126b8c..d4954903 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -21,7 +21,8 @@ - **[Emulation](#emulation)** (2 tools) - [`emulate`](#emulate) - [`resize_page`](#resize_page) -- **[Performance](#performance)** (3 tools) +- **[Performance](#performance)** (4 tools) + - [`performance_analyze_file`](#performance_analyze_file) - [`performance_analyze_insight`](#performance_analyze_insight) - [`performance_start_trace`](#performance_start_trace) - [`performance_stop_trace`](#performance_stop_trace) @@ -213,6 +214,16 @@ ## Performance +### `performance_analyze_file` + +**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. + +**Parameters:** + +- **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). + +--- + ### `performance_analyze_insight` **Description:** Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording. diff --git a/src/tools/performance.ts b/src/tools/performance.ts index a8b24903..9fad8337 100644 --- a/src/tools/performance.ts +++ b/src/tools/performance.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import fs from 'node:fs/promises'; + import {logger} from '../logger.js'; import {zod} from '../third_party/index.js'; import type {Page} from '../third_party/index.js'; @@ -161,6 +163,50 @@ export const analyzeInsight = defineTool({ }, }); +export const analyzeFile = defineTool({ + name: 'performance_analyze_file', + 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.', + annotations: { + category: ToolCategory.PERFORMANCE, + readOnlyHint: true, + }, + schema: { + filePath: zod + .string() + .describe( + '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).', + ), + }, + handler: async (request, response, context) => { + const {filePath} = request.params; + + try { + const buffer = await fs.readFile(filePath); + const result = await parseRawTraceBuffer(new Uint8Array(buffer)); + + if (traceResultIsSuccess(result)) { + context.storeTraceRecording(result); + const traceSummaryText = getTraceSummary(result); + response.appendResponseLine( + `Successfully analyzed trace file: ${filePath}`, + ); + response.appendResponseLine(traceSummaryText); + } else { + response.appendResponseLine( + 'There was an error parsing the trace file:', + ); + response.appendResponseLine(result.error); + } + } catch (e) { + const errorText = e instanceof Error ? e.message : JSON.stringify(e); + logger(`Error reading trace file: ${errorText}`); + response.appendResponseLine('An error occurred reading the trace file:'); + response.appendResponseLine(errorText); + } + }, +}); + async function stopTracingAndAppendOutput( page: Page, response: Response, diff --git a/tests/tools/performance.test.ts b/tests/tools/performance.test.ts index cb390b33..a251110e 100644 --- a/tests/tools/performance.test.ts +++ b/tests/tools/performance.test.ts @@ -5,11 +5,15 @@ */ import assert from 'node:assert'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import {describe, it, afterEach} from 'node:test'; import sinon from 'sinon'; import { + analyzeFile, analyzeInsight, startTrace, stopTrace, @@ -276,4 +280,79 @@ describe('performance', () => { }); }); }); + + describe('performance_analyze_file', () => { + it('analyzes a trace file from the filesystem', async () => { + const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz'); + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-')); + const tempFilePath = path.join(tempDir, 'trace.json'); + fs.writeFileSync(tempFilePath, rawData); + + try { + await withMcpContext(async (response, context) => { + await analyzeFile.handler( + {params: {filePath: tempFilePath}}, + response, + context, + ); + + const output = response.responseLines.join('\n'); + assert.ok( + output.includes( + `Successfully analyzed trace file: ${tempFilePath}`, + ), + ); + assert.ok( + output.includes('## Summary of Performance trace findings'), + ); + assert.ok(output.includes('URL: https://web.dev/')); + assert.strictEqual(context.recordedTraces().length, 1); + }); + } finally { + fs.rmSync(tempDir, {recursive: true}); + } + }); + + it('returns an error if the file does not exist', async () => { + await withMcpContext(async (response, context) => { + await analyzeFile.handler( + {params: {filePath: '/nonexistent/path/trace.json'}}, + response, + context, + ); + + assert.ok( + response.responseLines + .join('\n') + .includes('An error occurred reading the trace file:'), + ); + assert.strictEqual(context.recordedTraces().length, 0); + }); + }); + + it('returns an error if the file is not valid JSON', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-')); + const tempFilePath = path.join(tempDir, 'invalid.json'); + fs.writeFileSync(tempFilePath, 'not valid json'); + + try { + await withMcpContext(async (response, context) => { + await analyzeFile.handler( + {params: {filePath: tempFilePath}}, + response, + context, + ); + + assert.ok( + response.responseLines + .join('\n') + .includes('There was an error parsing the trace file:'), + ); + assert.strictEqual(context.recordedTraces().length, 0); + }); + } finally { + fs.rmSync(tempDir, {recursive: true}); + } + }); + }); });