Compare commits

...

6 Commits

Author SHA1 Message Date
Jorge Cabiedes
d3cdd82fe7 format 2025-05-06 08:44:20 -07:00
Jorge Cabiedes
ba3c9e0ead Gracefully handle failures when programmatically clicking DOM elements 2025-05-06 08:40:18 -07:00
Jorge Cabiedes
ece6103657 Address comments 2025-05-05 10:20:06 -07:00
Jorge Cabiedes Acosta
fa03b19203 formatting 2025-05-02 16:01:12 -07:00
Jorge Cabiedes Acosta
7cce86202a [mcp] Revert left over changes from merge hell 2025-05-02 16:00:20 -07:00
Jorge Cabiedes Acosta
ad63721bd2 [mcp] Add proper web-vitals metric collection 2025-05-02 15:50:00 -07:00
2 changed files with 129 additions and 63 deletions

View File

@@ -22,6 +22,12 @@ import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
function calculateMean(values: number[]): string {
return values.length > 0
? values.reduce((acc, curr) => acc + curr, 0) / values.length + 'ms'
: 'could not collect';
}
const server = new McpServer({
name: 'React',
version: '0.0.0',
@@ -326,17 +332,16 @@ server.tool(
# React Component Performance Results
## Mean Render Time
${results.renderTime / iterations}ms
${calculateMean(results.renderTime)}
## Mean Web Vitals
- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms
- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms
- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms
- First Input Delay (FID): ${results.webVitals.fid / iterations}ms
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
## Mean React Profiler
- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms
- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
`;
return {

View File

@@ -8,25 +8,51 @@ import * as babelPresetEnv from '@babel/preset-env';
import * as babelPresetReact from '@babel/preset-react';
type PerformanceResults = {
renderTime: number;
renderTime: number[];
webVitals: {
cls: number;
lcp: number;
inp: number;
fid: number;
ttfb: number;
cls: number[];
lcp: number[];
inp: number[];
fid: number[];
ttfb: number[];
};
reactProfiler: {
id: number;
phase: number;
actualDuration: number;
baseDuration: number;
startTime: number;
commitTime: number;
id: number[];
phase: number[];
actualDuration: number[];
baseDuration: number[];
startTime: number[];
commitTime: number[];
};
error: Error | null;
};
type EvaluationResults = {
renderTime: number | null;
webVitals: {
cls: number | null;
lcp: number | null;
inp: number | null;
fid: number | null;
ttfb: number | null;
};
reactProfiler: {
id: number | null;
phase: number | null;
actualDuration: number | null;
baseDuration: number | null;
startTime: number | null;
commitTime: number | null;
};
error: Error | null;
};
function delay(time: number) {
return new Promise(function (resolve) {
setTimeout(resolve, time);
});
}
export async function measurePerformance(
code: string,
iterations: number,
@@ -72,21 +98,21 @@ export async function measurePerformance(
const html = buildHtml(transpiled);
let performanceResults: PerformanceResults = {
renderTime: 0,
renderTime: [],
webVitals: {
cls: 0,
lcp: 0,
inp: 0,
fid: 0,
ttfb: 0,
cls: [],
lcp: [],
inp: [],
fid: [],
ttfb: [],
},
reactProfiler: {
id: 0,
phase: 0,
actualDuration: 0,
baseDuration: 0,
startTime: 0,
commitTime: 0,
id: [],
phase: [],
actualDuration: [],
baseDuration: [],
startTime: [],
commitTime: [],
},
error: null,
};
@@ -96,38 +122,73 @@ export async function measurePerformance(
await page.waitForFunction(
'window.__RESULT__ !== undefined && (window.__RESULT__.renderTime !== null || window.__RESULT__.error !== null)',
);
// ui chaos monkey
await page.waitForFunction(`window.__RESULT__ !== undefined && (function() {
for (const el of [...document.querySelectorAll('a'), ...document.querySelectorAll('button')]) {
console.log(el);
el.click();
const selectors = await page.evaluate(() => {
window.__INTERACTABLE_SELECTORS__ = [];
const elements = Array.from(document.querySelectorAll('a')).concat(
Array.from(document.querySelectorAll('button')),
);
for (const el of elements) {
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
}
return true;
})() `);
const evaluationResult: PerformanceResults = await page.evaluate(() => {
return window.__INTERACTABLE_SELECTORS__;
});
await Promise.all(
selectors.map(async (selector: string) => {
try {
await page.click(selector);
} catch (e) {
console.log(`warning: Could not click ${selector}: ${e.message}`);
}
}),
);
await delay(500);
// Visit a new page for 1s to background the current page so that WebVitals can finish being calculated
const tempPage = await browser.newPage();
await tempPage.evaluate(() => {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 1000);
});
});
await tempPage.close();
const evaluationResult: EvaluationResults = await page.evaluate(() => {
return (window as any).__RESULT__;
});
// TODO: investigate why webvital metrics are not populating correctly
performanceResults.renderTime += evaluationResult.renderTime;
performanceResults.webVitals.cls += evaluationResult.webVitals.cls || 0;
performanceResults.webVitals.lcp += evaluationResult.webVitals.lcp || 0;
performanceResults.webVitals.inp += evaluationResult.webVitals.inp || 0;
performanceResults.webVitals.fid += evaluationResult.webVitals.fid || 0;
performanceResults.webVitals.ttfb += evaluationResult.webVitals.ttfb || 0;
if (evaluationResult.renderTime !== null) {
performanceResults.renderTime.push(evaluationResult.renderTime);
}
performanceResults.reactProfiler.id +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.phase +=
evaluationResult.reactProfiler.phase || 0;
performanceResults.reactProfiler.actualDuration +=
evaluationResult.reactProfiler.actualDuration || 0;
performanceResults.reactProfiler.baseDuration +=
evaluationResult.reactProfiler.baseDuration || 0;
performanceResults.reactProfiler.startTime +=
evaluationResult.reactProfiler.startTime || 0;
performanceResults.reactProfiler.commitTime +=
evaluationResult.reactProfiler.commitTime || 0;
const webVitalMetrics = ['cls', 'lcp', 'inp', 'fid', 'ttfb'] as const;
for (const metric of webVitalMetrics) {
if (evaluationResult.webVitals[metric] !== null) {
performanceResults.webVitals[metric].push(
evaluationResult.webVitals[metric],
);
}
}
const profilerMetrics = [
'id',
'phase',
'actualDuration',
'baseDuration',
'startTime',
'commitTime',
] as const;
for (const metric of profilerMetrics) {
if (evaluationResult.reactProfiler[metric] !== null) {
performanceResults.reactProfiler[metric].push(
evaluationResult.reactProfiler[metric],
);
}
}
performanceResults.error = evaluationResult.error;
}
@@ -159,14 +220,14 @@ function buildHtml(transpiled: string) {
renderTime: null,
webVitals: {},
reactProfiler: {},
error: null
error: null,
};
webVitals.onCLS((metric) => { window.__RESULT__.webVitals.cls = metric; });
webVitals.onLCP((metric) => { window.__RESULT__.webVitals.lcp = metric; });
webVitals.onINP((metric) => { window.__RESULT__.webVitals.inp = metric; });
webVitals.onFID((metric) => { window.__RESULT__.webVitals.fid = metric; });
webVitals.onTTFB((metric) => { window.__RESULT__.webVitals.ttfb = metric; });
webVitals.onCLS(({value}) => { window.__RESULT__.webVitals.cls = value; });
webVitals.onLCP(({value}) => { window.__RESULT__.webVitals.lcp = value; });
webVitals.onINP(({value}) => { window.__RESULT__.webVitals.inp = value; });
webVitals.onFID(({value}) => { window.__RESULT__.webVitals.fid = value; });
webVitals.onTTFB(({value}) => { window.__RESULT__.webVitals.ttfb = value; });
try {
${transpiled}