Add logic to prepare perf data for LLM analysis

This commit is contained in:
Jorge Cabiedes
2025-06-06 14:23:50 -07:00
parent 23934b98bc
commit 128b267a18
3 changed files with 1756 additions and 24 deletions
@@ -459,18 +459,25 @@ server.tool(
server.tool(
'get-react-performance-data',
`
This tool returns the JSON format of the performance data recorded by the start-react-performance-recording tool.
It retrieves the data that was captured from console.timeStamp calls in the browser, which includes information from
This tool retrieves the performance data recorded by the start-react-performance-recording tool,
converts it to CSV format, and saves it to a file in the artifacts directory.
It processes the data that was captured from console.timeStamp calls in the browser, which includes information from
the React Performance panel in Chrome DevTools (Components track and Scheduler track).
<requirements>
- The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
- The user should be running a Chrome browser in debug mode on port 9222. If you receive an error message, advise the user to run
the following command in the terminal:
MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
- You must have previously run the start-react-performance-recording tool to capture performance data.
- The data will be available after the user has interacted with their app while recording was active.
</requirements>
<usage>
- Use this tool after running start-react-performance-recording and having the user interact with their app.
- The returned data will be in JSON format, containing detailed performance metrics that can be analyzed.
- The tool will save the performance data as a CSV file in the artifacts directory.
- The response will include the path to the saved file.
</usage>
`,
{
@@ -478,13 +485,13 @@ server.tool(
},
async ({url}) => {
try {
const perfData = await getPerfData(url);
const result = await getPerfData(url);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(perfData, null, 2),
text: result,
},
],
};
@@ -1,5 +1,7 @@
import puppeteer from 'puppeteer';
import {hookIntoPage} from '../utils/puppeteerUtils';
import fs from 'fs/promises';
import path from 'path';
/**
* Connects to a browser and patches the console.timeStamp method to capture data
@@ -8,25 +10,52 @@ import {hookIntoPage} from '../utils/puppeteerUtils';
* @param url The URL of the page to connect to
* @returns A promise that resolves to the captured console.timeStamp data
*/
export async function beginPerfRecording(url: string): Promise<string[]> {
export async function beginPerfRecording(url: string): Promise<string> {
try {
const targetPage = await hookIntoPage(url);
await targetPage.evaluateOnNewDocument(() => {
(window as any).__CAPTURED_TIMESTAMP_DATA__ = [];
(window as any).__COMPONENT_TRACK_TIMESTAMP_DATA__ = [];
(window as any).__SCHEDULER_TRACK_TIMESTAMP_DATA__ = [];
(window as any).__MCP_RECORDING_ACTIVE__ = true;
const originalTimestamp = console.timeStamp;
// Monkey-patch console.timeStamp to capture data
console.timeStamp = function (...args) {
console.timeStamp = function (...args: any) {
if ((window as any).__MCP_RECORDING_ACTIVE__) {
console.log('[MCP] console.timestamp called with', ...args);
(window as any).__CAPTURED_TIMESTAMP_DATA__.push(args);
if ((args[4] as string)?.includes('Scheduler')) {
const timeStampData = {
name: args[0],
startTime: args[1],
endTime: args[2],
type: args[3],
track: 'Scheduler',
color: args[5],
};
(window as any).__SCHEDULER_TRACK_TIMESTAMP_DATA__.push(
timeStampData,
);
} else if ((args[3] as string)?.includes('Components')) {
const timeStampData = {
name: args[0],
startTime: args[1],
endTime: args[2],
track: 'Components',
color: args[4],
};
(window as any).__COMPONENT_TRACK_TIMESTAMP_DATA__.push(
timeStampData,
);
} else {
console.log('[MCP] Unknown track format:', args);
}
}
if (originalTimestamp) {
return originalTimestamp.apply(console, args);
return originalTimestamp.apply(console, args as any);
}
};
@@ -39,10 +68,6 @@ export async function beginPerfRecording(url: string): Promise<string[]> {
if (mcpStyle) mcpStyle.remove();
if (mcpIndicator) mcpIndicator.remove();
console.log(
'[MCP] Recording stopped, captured data:',
(window as any).__CAPTURED_TIMESTAMP_DATA__,
);
return (window as any).__CAPTURED_TIMESTAMP_DATA__;
};
@@ -107,20 +132,137 @@ export async function beginPerfRecording(url: string): Promise<string[]> {
await targetPage.reload({waitUntil: 'domcontentloaded'});
const capturedTimeStampData = await targetPage.evaluate(() => {
return (window as any).__CAPTURED_TIMESTAMP_DATA__ || [];
});
return capturedTimeStampData;
return 'Recording Successfully Started';
} catch (error) {
throw new Error(`Failed to capture console.timeStamp data: ${error}`);
}
}
export async function getPerfData(url: string): Promise<string[]> {
const page = await hookIntoPage(url);
export async function getPerfData(url: string): Promise<string> {
try {
const page = await hookIntoPage(url);
return await page.evaluate(() => {
return (window as any).__CAPTURED_TIMESTAMP_DATA__;
});
// Get both timestamp data tracks from the browser
const [componentTrackData, schedulerTrackData] = await page.evaluate(() => {
return [
(window as any).__COMPONENT_TRACK_TIMESTAMP_DATA__ || [],
(window as any).__SCHEDULER_TRACK_TIMESTAMP_DATA__ || [],
];
});
if (
(!Array.isArray(componentTrackData) || componentTrackData.length === 0) &&
(!Array.isArray(schedulerTrackData) || schedulerTrackData.length === 0)
) {
return 'No performance data was captured. Make sure to run start-react-performance-recording first.';
}
// Create a timestamp for the filenames
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const artifactsDir = path.join(__dirname, '../src/artifacts');
// Ensure the artifacts directory exists
try {
await fs.mkdir(artifactsDir, {recursive: true});
} catch (err) {
console.error(`Error creating directory: ${err}`);
}
const results = [];
// Process Component Track data
if (Array.isArray(componentTrackData) && componentTrackData.length > 0) {
const componentFilename = `react-component-track-${timestamp}.csv`;
const componentFilePath = path.join(artifactsDir, componentFilename);
// Define headers for component track
const componentHeaders = [
'name',
'startTime',
'endTime',
'track',
'color',
].join(',');
// Convert data to CSV rows
const componentRows = componentTrackData.map(item => {
return componentHeaders
.split(',')
.map(header => {
const value = item[header];
// Handle undefined values
if (value === undefined) {
return '';
}
// Handle strings with commas by wrapping in quotes
if (typeof value === 'string' && value.includes(',')) {
return `"${value}"`;
}
return value;
})
.join(',');
});
// Combine headers and rows
const componentCsvContent = [componentHeaders, ...componentRows].join(
'\n',
);
// Write to file
await fs.writeFile(componentFilePath, componentCsvContent, 'utf8');
results.push(`Component track data saved to ${componentFilename}`);
}
// Process Scheduler Track data
if (Array.isArray(schedulerTrackData) && schedulerTrackData.length > 0) {
const schedulerFilename = `react-scheduler-track-${timestamp}.csv`;
const schedulerFilePath = path.join(artifactsDir, schedulerFilename);
// Define headers for scheduler track
const schedulerHeaders = [
'name',
'startTime',
'endTime',
'type',
'track',
'color',
].join(',');
// Convert data to CSV rows
const schedulerRows = schedulerTrackData.map(item => {
return schedulerHeaders
.split(',')
.map(header => {
const value = item[header];
// Handle undefined values
if (value === undefined) {
return '';
}
// Handle strings with commas by wrapping in quotes
if (typeof value === 'string' && value.includes(',')) {
return `"${value}"`;
}
return value;
})
.join(',');
});
// Combine headers and rows
const schedulerCsvContent = [schedulerHeaders, ...schedulerRows].join(
'\n',
);
// Write to file
await fs.writeFile(schedulerFilePath, schedulerCsvContent, 'utf8');
results.push(`Scheduler track data saved to ${schedulerFilename}`);
}
return results.length > 0
? `Performance data saved in the artifacts directory:\n${results.join('\n')}`
: 'No performance data was available to save.';
} catch (error) {
throw new Error(`Failed to process performance data: ${error}`);
}
}