diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 138dc57dc14a9..2ec747eac4dfd 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -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 { diff --git a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts index 7f4d0a1efecd8..30badc833d68c 100644 --- a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts +++ b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts @@ -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; - - 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; + if (evaluationResult.renderTime !== null) { + performanceResults.renderTime.push(evaluationResult.renderTime); + } + + 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}