@bschauer/webtools-mcp-server
Version:
MCP server providing web analysis tools including screenshot, debug, performance, security, accessibility, SEO, and asset optimization capabilities
357 lines (300 loc) • 12.7 kB
JavaScript
/**
* JavaScript execution analysis module
*/
/**
* Analyze JavaScript execution in the trace data
* @param {Array} events - All trace events
* @returns {Object|null} JavaScript execution analysis or null if no JS execution found
*/
export function analyzeJavaScriptExecution(events) {
// Find JavaScript execution events
const jsEvents = events.filter((event) => event.name === "V8.Execute" || event.name === "FunctionCall" || event.name === "EvaluateScript");
if (jsEvents.length === 0) {
return null;
}
// Find layout events for correlation
const layoutEvents = events.filter((event) => event.name === "Layout" || event.name === "UpdateLayoutTree");
// Calculate total JavaScript execution time
const totalJsTime = jsEvents.reduce((sum, event) => sum + (event.dur || 0), 0);
// Correlate JS execution with layout events
const jsLayoutCorrelation = correlateJsWithLayout(jsEvents, layoutEvents, events);
// Analyze the call stack to find functions that trigger layouts
const callStackAnalysis = analyzeCallStacksForLayout(events);
// Extract code snippets that cause layout thrashing
const layoutThrashingCodeSnippets = extractLayoutThrashingCodeSnippets(events);
// Analyze script evaluation times
const scriptEvaluations = analyzeScriptEvaluations(jsEvents);
// Generate recommendations
const recommendations = generateJsExecutionRecommendations(jsEvents, jsLayoutCorrelation, callStackAnalysis, scriptEvaluations);
return {
type: "javascript_execution",
description: `Total JavaScript execution time: ${(totalJsTime / 1000).toFixed(2)}ms with ${jsLayoutCorrelation.correlatedEvents.length} layout-triggering operations`,
details: {
totalExecutionTime: totalJsTime / 1000,
scriptEvaluation: scriptEvaluations,
jsLayoutCorrelation: jsLayoutCorrelation,
callStackAnalysis: callStackAnalysis,
layoutThrashingCodeSnippets: layoutThrashingCodeSnippets,
recommendations: recommendations,
},
};
}
/**
* Correlate JavaScript execution with layout events
* @param {Array} jsEvents - JavaScript execution events
* @param {Array} layoutEvents - Layout events
* @param {Array} allEvents - All trace events
* @returns {Object} Correlation analysis
*/
function correlateJsWithLayout(jsEvents, layoutEvents, allEvents) {
const correlatedEvents = [];
// For each layout event, find the JS event that might have triggered it
for (const layout of layoutEvents) {
// Look for JS events that completed shortly before this layout
const THRESHOLD_US = 100 * 1000; // 100ms in microseconds
const precedingJsEvents = jsEvents.filter((js) => js.ts + js.dur <= layout.ts && js.ts + js.dur > layout.ts - THRESHOLD_US);
if (precedingJsEvents.length > 0) {
// Sort by proximity to the layout event
precedingJsEvents.sort((a, b) => layout.ts - (a.ts + a.dur) - (layout.ts - (b.ts + b.dur)));
const jsEvent = precedingJsEvents[0]; // Closest JS event
correlatedEvents.push({
layout: {
time: layout.ts,
duration: layout.dur / 1000,
type: layout.name,
},
javascript: {
time: jsEvent.ts,
duration: jsEvent.dur / 1000,
functionName: jsEvent.args?.data?.functionName || "anonymous",
url: jsEvent.args?.data?.url || jsEvent.args?.data?.fileName || "unknown",
lineNumber: jsEvent.args?.data?.lineNumber,
columnNumber: jsEvent.args?.data?.columnNumber,
},
timeBetween: (layout.ts - (jsEvent.ts + jsEvent.dur)) / 1000,
});
}
}
// Group by JavaScript function
const byFunction = {};
for (const corr of correlatedEvents) {
const key = `${corr.javascript.url}:${corr.javascript.functionName}`;
if (!byFunction[key]) {
byFunction[key] = {
url: corr.javascript.url,
functionName: corr.javascript.functionName,
layoutCount: 0,
totalLayoutDuration: 0,
correlations: [],
};
}
byFunction[key].layoutCount++;
byFunction[key].totalLayoutDuration += corr.layout.duration;
byFunction[key].correlations.push(corr);
}
// Convert to array and sort by layout count
const functionImpact = Object.values(byFunction);
functionImpact.sort((a, b) => b.layoutCount - a.layoutCount);
return {
correlatedEvents,
functionImpact: functionImpact.slice(0, 10).map((f) => ({
...f,
totalLayoutDuration: f.totalLayoutDuration,
averageLayoutDuration: f.totalLayoutDuration / f.layoutCount,
correlations: f.correlations.slice(0, 5), // Limit to 5 correlations per function
})),
};
}
/**
* Analyze call stacks to find functions that trigger layouts
* @param {Array} events - All trace events
* @returns {Object} Call stack analysis
*/
function analyzeCallStacksForLayout(events) {
// Find layout events with stack traces
const layoutsWithStacks = events.filter((e) => e.name === "Layout" && e.args?.data?.beginData?.stackTrace && e.args.data.beginData.stackTrace.length > 0);
if (layoutsWithStacks.length === 0) {
return { stacksAnalyzed: false };
}
// Analyze the stack traces
const functionCounts = {};
for (const layout of layoutsWithStacks) {
const stackTrace = layout.args.data.beginData.stackTrace;
// Process each frame in the stack trace
for (const frame of stackTrace) {
const key = `${frame.url}:${frame.functionName}:${frame.lineNumber}`;
if (!functionCounts[key]) {
functionCounts[key] = {
url: frame.url,
functionName: frame.functionName || "anonymous",
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
count: 0,
totalDuration: 0,
};
}
functionCounts[key].count++;
functionCounts[key].totalDuration += layout.dur || 0;
}
}
// Convert to array and sort by count
const functions = Object.values(functionCounts);
functions.sort((a, b) => b.count - a.count);
return {
stacksAnalyzed: true,
layoutsWithStackTraces: layoutsWithStacks.length,
topFunctions: functions.slice(0, 15).map((f) => ({
...f,
totalDuration: f.totalDuration / 1000,
averageDuration: f.totalDuration / f.count / 1000,
})),
};
}
/**
* Extract code snippets that cause layout thrashing
* @param {Array} events - All trace events
* @returns {Array} Code snippets that cause layout thrashing
*/
function extractLayoutThrashingCodeSnippets(events) {
// This is a simplified version since we can't actually extract code
// In a real implementation, we would need source maps and the actual source code
// Find the worst layout thrashing offenders
const layoutEvents = events.filter((e) => e.name === "Layout" && e.args?.data?.beginData?.stackTrace);
// Group by the script URL and function name if available
const offenderMap = {};
for (const layout of layoutEvents) {
const stackTrace = layout.args?.data?.beginData?.stackTrace || [];
if (stackTrace.length > 0) {
// Use the top of the stack trace as the offender
const topFrame = stackTrace[0];
const key = `${topFrame.url}:${topFrame.functionName}:${topFrame.lineNumber}`;
if (!offenderMap[key]) {
offenderMap[key] = {
url: topFrame.url,
functionName: topFrame.functionName || "anonymous",
lineNumber: topFrame.lineNumber,
columnNumber: topFrame.columnNumber,
count: 0,
totalDuration: 0,
};
}
offenderMap[key].count++;
offenderMap[key].totalDuration += layout.dur || 0;
}
}
// Convert to array and sort by count
const offenders = Object.values(offenderMap);
offenders.sort((a, b) => b.count - a.count);
// Create "pseudo-snippets" based on the information we have
return offenders.slice(0, 10).map((offender) => ({
url: offender.url,
functionName: offender.functionName,
lineNumber: offender.lineNumber,
columnNumber: offender.columnNumber,
layoutCount: offender.count,
totalLayoutDuration: offender.totalDuration / 1000,
// This would be the actual code in a real implementation
pseudoCode:
`/* At ${offender.url}:${offender.lineNumber}:${offender.columnNumber} */\n` +
`function ${offender.functionName}() {\n` +
` // This function triggered layout ${offender.count} times\n` +
` // Total layout time: ${(offender.totalDuration / 1000).toFixed(2)}ms\n` +
` // Potential layout thrashing detected\n` +
`}`,
}));
}
/**
* Analyze script evaluation times
* @param {Array} jsEvents - JavaScript execution events
* @returns {Array} Script evaluation analysis
*/
function analyzeScriptEvaluations(jsEvents) {
// Find script evaluation events
const scriptEvents = jsEvents.filter((e) => e.name === "EvaluateScript");
// Group by script URL
const scriptsByUrl = {};
for (const script of scriptEvents) {
const url = script.args?.data?.url || script.args?.data?.fileName || "unknown";
if (!scriptsByUrl[url]) {
scriptsByUrl[url] = {
url,
count: 0,
totalDuration: 0,
events: [],
};
}
scriptsByUrl[url].count++;
scriptsByUrl[url].totalDuration += script.dur || 0;
scriptsByUrl[url].events.push({
time: script.ts,
duration: script.dur / 1000,
});
}
// Convert to array and sort by total duration
const scripts = Object.values(scriptsByUrl);
scripts.sort((a, b) => b.totalDuration - a.totalDuration);
return scripts.slice(0, 10).map((script) => ({
url: script.url,
count: script.count,
totalDuration: script.totalDuration / 1000,
averageDuration: script.totalDuration / script.count / 1000,
}));
}
/**
* Generate recommendations for JavaScript execution
* @param {Array} jsEvents - JavaScript execution events
* @param {Object} jsLayoutCorrelation - JavaScript-layout correlation
* @param {Object} callStackAnalysis - Call stack analysis
* @param {Array} scriptEvaluations - Script evaluation analysis
* @returns {Array} Recommendations
*/
function generateJsExecutionRecommendations(jsEvents, jsLayoutCorrelation, callStackAnalysis, scriptEvaluations) {
const recommendations = [];
// Check for JavaScript-heavy layout operations
if (jsLayoutCorrelation.correlatedEvents.length > 10) {
recommendations.push({
type: "js_layout_optimization",
description: `${jsLayoutCorrelation.correlatedEvents.length} layout operations triggered by JavaScript`,
recommendation: "Batch DOM reads and writes, and use requestAnimationFrame for visual updates to avoid layout thrashing",
});
// If we have specific functions that trigger layouts, add more detailed recommendations
if (jsLayoutCorrelation.functionImpact.length > 0) {
const topOffender = jsLayoutCorrelation.functionImpact[0];
recommendations.push({
type: "specific_js_layout_optimization",
description: `Function ${topOffender.functionName} in ${topOffender.url} triggered ${topOffender.layoutCount} layout operations`,
recommendation: "Review this function and batch DOM reads and writes to avoid layout thrashing",
});
}
}
// Check for large script evaluations
if (scriptEvaluations.length > 0) {
const largeScripts = scriptEvaluations.filter((s) => s.totalDuration > 100); // 100ms
if (largeScripts.length > 0) {
recommendations.push({
type: "script_optimization",
description: `${largeScripts.length} scripts take more than 100ms to evaluate`,
recommendation: "Consider code splitting, lazy loading, or optimizing these scripts to improve page load performance",
});
}
}
// Check for excessive JavaScript execution
const totalJsTime = jsEvents.reduce((sum, event) => sum + (event.dur || 0), 0) / 1000;
if (totalJsTime > 1000) {
// 1 second
recommendations.push({
type: "js_execution_reduction",
description: `High JavaScript execution time (${totalJsTime.toFixed(2)}ms)`,
recommendation: "Reduce JavaScript execution by deferring non-critical operations, using web workers for heavy computation, and optimizing hot functions",
});
}
// Check for deep call stacks
if (callStackAnalysis.stacksAnalyzed && callStackAnalysis.topFunctions.some((f) => f.count > 20)) {
recommendations.push({
type: "call_stack_optimization",
description: "Deep call stacks detected that trigger layout operations",
recommendation: "Flatten call hierarchies and avoid nested functions that trigger layout operations",
});
}
return recommendations;
}