UNPKG

@bschauer/webtools-mcp-server

Version:

MCP server providing web analysis tools including screenshot, debug, performance, security, accessibility, SEO, and asset optimization capabilities

490 lines (425 loc) 18.3 kB
/** * Memory and DOM growth analysis module */ /** * Analyze memory and DOM growth in the trace data * @param {Array} memoryEvents - Memory-related events * @param {Array} domEvents - DOM-related events * @param {Array} allEvents - All trace events * @returns {Object} Memory and DOM growth analysis */ export function analyzeMemoryAndDomGrowth(memoryEvents, domEvents, allEvents) { // Analyze memory usage over time const memoryUsage = analyzeMemoryUsage(memoryEvents, allEvents); // Analyze DOM size over time const domSize = analyzeDomSize(domEvents, allEvents); // Detect potential memory leaks const potentialLeaks = detectPotentialLeaks(memoryEvents, domEvents, allEvents); // Detect event listener leaks const eventListenerLeaks = detectEventListenerLeaks(allEvents); // Analyze element growth const elementGrowth = analyzeElementGrowth(domEvents, allEvents); // Generate recommendations const recommendations = generateMemoryDomRecommendations(memoryUsage, domSize, potentialLeaks, eventListenerLeaks, elementGrowth); return { memoryUsage, domSize, potentialLeaks, eventListenerLeaks, elementGrowth, recommendations, }; } /** * Analyze memory usage over time * @param {Array} memoryEvents - Memory-related events * @param {Array} allEvents - All trace events * @returns {Object} Memory usage analysis */ function analyzeMemoryUsage(memoryEvents, allEvents) { // Extract memory usage data points const memoryDataPoints = []; // Look for UpdateCounters events with memory data for (const event of allEvents) { if (event.name === "UpdateCounters" && event.args?.data?.jsHeapSizeUsed) { memoryDataPoints.push({ timestamp: event.ts, jsHeapSizeUsed: event.args.data.jsHeapSizeUsed, jsHeapSizeTotal: event.args.data.jsHeapSizeTotal || 0, jsHeapSizeLimit: event.args.data.jsHeapSizeLimit || 0, }); } } // If we don't have enough data points, return limited analysis if (memoryDataPoints.length < 2) { return { dataPoints: memoryDataPoints, trend: "unknown", growthRate: 0, peakUsage: memoryDataPoints.length > 0 ? Math.max(...memoryDataPoints.map((p) => p.jsHeapSizeUsed)) : 0, averageUsage: memoryDataPoints.length > 0 ? memoryDataPoints.reduce((sum, p) => sum + p.jsHeapSizeUsed, 0) / memoryDataPoints.length : 0, }; } // Sort data points by timestamp memoryDataPoints.sort((a, b) => a.timestamp - b.timestamp); // Calculate growth rate const firstPoint = memoryDataPoints[0]; const lastPoint = memoryDataPoints[memoryDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const memoryGrowthBytes = lastPoint.jsHeapSizeUsed - firstPoint.jsHeapSizeUsed; const growthRatePerSecond = timeSpanSeconds > 0 ? memoryGrowthBytes / timeSpanSeconds : 0; // Determine trend let trend = "stable"; if (growthRatePerSecond > 100000) { // 100KB per second trend = "rapidly increasing"; } else if (growthRatePerSecond > 10000) { // 10KB per second trend = "increasing"; } else if (growthRatePerSecond < -10000) { // -10KB per second trend = "decreasing"; } // Calculate peak and average usage const peakUsage = Math.max(...memoryDataPoints.map((p) => p.jsHeapSizeUsed)); const averageUsage = memoryDataPoints.reduce((sum, p) => sum + p.jsHeapSizeUsed, 0) / memoryDataPoints.length; return { dataPoints: memoryDataPoints.map((p) => ({ timestamp: p.timestamp, jsHeapSizeUsed: p.jsHeapSizeUsed, jsHeapSizeTotal: p.jsHeapSizeTotal, jsHeapSizeLimit: p.jsHeapSizeLimit, })), trend, growthRate: growthRatePerSecond, peakUsage, averageUsage, percentOfLimit: memoryDataPoints[0].jsHeapSizeLimit > 0 ? (peakUsage / memoryDataPoints[0].jsHeapSizeLimit) * 100 : null, }; } /** * Analyze DOM size over time * @param {Array} domEvents - DOM-related events * @param {Array} allEvents - All trace events * @returns {Object} DOM size analysis */ function analyzeDomSize(domEvents, allEvents) { // Extract DOM node count data points const domDataPoints = []; // Look for UpdateCounters events with DOM node data for (const event of allEvents) { if (event.name === "UpdateCounters" && event.args?.data?.nodes) { domDataPoints.push({ timestamp: event.ts, nodes: event.args.data.nodes, documents: event.args.data.documents || 0, jsEventListeners: event.args.data.jsEventListeners || 0, }); } } // If we don't have enough data points, return limited analysis if (domDataPoints.length < 2) { return { dataPoints: domDataPoints, trend: "unknown", growthRate: 0, peakNodes: domDataPoints.length > 0 ? Math.max(...domDataPoints.map((p) => p.nodes)) : 0, averageNodes: domDataPoints.length > 0 ? domDataPoints.reduce((sum, p) => sum + p.nodes, 0) / domDataPoints.length : 0, }; } // Sort data points by timestamp domDataPoints.sort((a, b) => a.timestamp - b.timestamp); // Calculate growth rate const firstPoint = domDataPoints[0]; const lastPoint = domDataPoints[domDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const nodeGrowth = lastPoint.nodes - firstPoint.nodes; const growthRatePerSecond = timeSpanSeconds > 0 ? nodeGrowth / timeSpanSeconds : 0; // Determine trend let trend = "stable"; if (growthRatePerSecond > 10) { // 10 nodes per second trend = "rapidly increasing"; } else if (growthRatePerSecond > 1) { // 1 node per second trend = "increasing"; } else if (growthRatePerSecond < -1) { // -1 node per second trend = "decreasing"; } // Calculate peak and average nodes const peakNodes = Math.max(...domDataPoints.map((p) => p.nodes)); const averageNodes = domDataPoints.reduce((sum, p) => sum + p.nodes, 0) / domDataPoints.length; return { dataPoints: domDataPoints.map((p) => ({ timestamp: p.timestamp, nodes: p.nodes, documents: p.documents, jsEventListeners: p.jsEventListeners, })), trend, growthRate: growthRatePerSecond, peakNodes, averageNodes, eventListenersPerNode: domDataPoints[domDataPoints.length - 1].nodes > 0 ? domDataPoints[domDataPoints.length - 1].jsEventListeners / domDataPoints[domDataPoints.length - 1].nodes : 0, }; } /** * Detect potential memory leaks * @param {Array} memoryEvents - Memory-related events * @param {Array} domEvents - DOM-related events * @param {Array} allEvents - All trace events * @returns {Array} Potential memory leaks */ function detectPotentialLeaks(memoryEvents, domEvents, allEvents) { const potentialLeaks = []; // Extract memory usage data points const memoryDataPoints = []; // Look for UpdateCounters events with memory data for (const event of allEvents) { if (event.name === "UpdateCounters" && event.args?.data?.jsHeapSizeUsed) { memoryDataPoints.push({ timestamp: event.ts, jsHeapSizeUsed: event.args.data.jsHeapSizeUsed, jsHeapSizeTotal: event.args.data.jsHeapSizeTotal || 0, jsHeapSizeLimit: event.args.data.jsHeapSizeLimit || 0, nodes: event.args.data.nodes || 0, jsEventListeners: event.args.data.jsEventListeners || 0, }); } } // Sort data points by timestamp memoryDataPoints.sort((a, b) => a.timestamp - b.timestamp); // Check for continuous memory growth if (memoryDataPoints.length >= 3) { const firstPoint = memoryDataPoints[0]; const lastPoint = memoryDataPoints[memoryDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const memoryGrowthBytes = lastPoint.jsHeapSizeUsed - firstPoint.jsHeapSizeUsed; const growthRatePerSecond = timeSpanSeconds > 0 ? memoryGrowthBytes / timeSpanSeconds : 0; // Check if memory is continuously growing if (growthRatePerSecond > 10000 && timeSpanSeconds > 5) { // 10KB per second for at least 5 seconds // Look for garbage collection events const gcEvents = allEvents.filter((e) => e.name.includes("GC")); // If we have GC events but memory is still growing, it's a stronger indicator of a leak if (gcEvents.length > 0) { potentialLeaks.push({ type: "continuous_memory_growth", severity: "high", growthRate: growthRatePerSecond, timeSpan: timeSpanSeconds, gcEvents: gcEvents.length, description: `Memory is continuously growing at ${(growthRatePerSecond / 1024).toFixed(2)}KB/s despite ${gcEvents.length} garbage collection events`, }); } else { potentialLeaks.push({ type: "continuous_memory_growth", severity: "medium", growthRate: growthRatePerSecond, timeSpan: timeSpanSeconds, description: `Memory is continuously growing at ${(growthRatePerSecond / 1024).toFixed(2)}KB/s`, }); } } } // Check for continuous DOM growth if (memoryDataPoints.length >= 3) { const nodeDataPoints = memoryDataPoints.filter((p) => p.nodes > 0); if (nodeDataPoints.length >= 3) { const firstPoint = nodeDataPoints[0]; const lastPoint = nodeDataPoints[nodeDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const nodeGrowth = lastPoint.nodes - firstPoint.nodes; const growthRatePerSecond = timeSpanSeconds > 0 ? nodeGrowth / timeSpanSeconds : 0; // Check if DOM is continuously growing if (growthRatePerSecond > 1 && timeSpanSeconds > 5 && nodeGrowth > 10) { // 1 node per second for at least 5 seconds, with at least 10 nodes added potentialLeaks.push({ type: "continuous_dom_growth", severity: "high", growthRate: growthRatePerSecond, timeSpan: timeSpanSeconds, nodeGrowth, description: `DOM is continuously growing at ${growthRatePerSecond.toFixed(2)} nodes/s (${nodeGrowth} nodes added over ${timeSpanSeconds.toFixed(1)}s)`, }); } } } // Check for event listener growth if (memoryDataPoints.length >= 3) { const listenerDataPoints = memoryDataPoints.filter((p) => p.jsEventListeners > 0); if (listenerDataPoints.length >= 3) { const firstPoint = listenerDataPoints[0]; const lastPoint = listenerDataPoints[listenerDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const listenerGrowth = lastPoint.jsEventListeners - firstPoint.jsEventListeners; const growthRatePerSecond = timeSpanSeconds > 0 ? listenerGrowth / timeSpanSeconds : 0; // Check if event listeners are continuously growing if (growthRatePerSecond > 0.5 && timeSpanSeconds > 5 && listenerGrowth > 5) { // 0.5 listeners per second for at least 5 seconds, with at least 5 listeners added potentialLeaks.push({ type: "event_listener_growth", severity: "high", growthRate: growthRatePerSecond, timeSpan: timeSpanSeconds, listenerGrowth, description: `Event listeners are continuously growing at ${growthRatePerSecond.toFixed(2)} listeners/s (${listenerGrowth} listeners added over ${timeSpanSeconds.toFixed(1)}s)`, }); } } } return potentialLeaks; } /** * Detect event listener leaks * @param {Array} allEvents - All trace events * @returns {Array} Event listener leaks */ function detectEventListenerLeaks(allEvents) { const eventListenerLeaks = []; // Extract event listener data points const listenerDataPoints = []; // Look for UpdateCounters events with event listener data for (const event of allEvents) { if (event.name === "UpdateCounters" && event.args?.data?.jsEventListeners) { listenerDataPoints.push({ timestamp: event.ts, jsEventListeners: event.args.data.jsEventListeners, nodes: event.args.data.nodes || 0, }); } } // Sort data points by timestamp listenerDataPoints.sort((a, b) => a.timestamp - b.timestamp); // Check for continuous event listener growth if (listenerDataPoints.length >= 3) { const firstPoint = listenerDataPoints[0]; const lastPoint = listenerDataPoints[listenerDataPoints.length - 1]; const timeSpanSeconds = (lastPoint.timestamp - firstPoint.timestamp) / 1000000; // Convert to seconds const listenerGrowth = lastPoint.jsEventListeners - firstPoint.jsEventListeners; const nodeGrowth = lastPoint.nodes - firstPoint.nodes; // If listeners are growing faster than nodes, it might indicate a leak if (listenerGrowth > 0 && (nodeGrowth === 0 || listenerGrowth / nodeGrowth > 1.5)) { eventListenerLeaks.push({ type: "disproportionate_listener_growth", severity: "medium", listenerGrowth, nodeGrowth, ratio: nodeGrowth > 0 ? listenerGrowth / nodeGrowth : listenerGrowth, timeSpan: timeSpanSeconds, description: `Event listeners (${listenerGrowth}) are growing faster than DOM nodes (${nodeGrowth})`, }); } } return eventListenerLeaks; } /** * Analyze element growth * @param {Array} domEvents - DOM-related events * @param {Array} allEvents - All trace events * @returns {Object} Element growth analysis */ function analyzeElementGrowth(domEvents, allEvents) { // This is a simplified version - in a real implementation, we would need to analyze the actual DOM // Look for DOM mutation events const addNodeEvents = allEvents.filter((e) => e.name === "AddElement" || e.name === "DOMNodeInserted"); const removeNodeEvents = allEvents.filter((e) => e.name === "RemoveElement" || e.name === "DOMNodeRemoved"); // Group additions by node type const additionsByType = {}; for (const event of addNodeEvents) { const nodeType = event.args?.data?.nodeType || event.args?.data?.nodeName || "unknown"; if (!additionsByType[nodeType]) { additionsByType[nodeType] = { count: 0, events: [], }; } additionsByType[nodeType].count++; additionsByType[nodeType].events.push(event); } // Find the fastest growing element types const elementTypes = Object.keys(additionsByType); elementTypes.sort((a, b) => additionsByType[b].count - additionsByType[a].count); const fastestGrowingTypes = elementTypes.slice(0, 5).map((type) => ({ type, count: additionsByType[type].count, netGrowth: additionsByType[type].count - (removeNodeEvents.filter((e) => (e.args?.data?.nodeType || e.args?.data?.nodeName) === type).length || 0), })); return { totalAdditions: addNodeEvents.length, totalRemovals: removeNodeEvents.length, netGrowth: addNodeEvents.length - removeNodeEvents.length, fastestGrowingTypes, }; } /** * Generate recommendations for memory and DOM * @param {Object} memoryUsage - Memory usage analysis * @param {Object} domSize - DOM size analysis * @param {Array} potentialLeaks - Potential memory leaks * @param {Array} eventListenerLeaks - Event listener leaks * @param {Object} elementGrowth - Element growth analysis * @returns {Array} Recommendations */ function generateMemoryDomRecommendations(memoryUsage, domSize, potentialLeaks, eventListenerLeaks, elementGrowth) { const recommendations = []; // Check for memory leaks if (potentialLeaks.length > 0) { const memoryLeaks = potentialLeaks.filter((leak) => leak.type === "continuous_memory_growth"); if (memoryLeaks.length > 0) { recommendations.push({ type: "memory_leak_prevention", description: "Potential memory leak detected", recommendation: "Check for objects that aren't being garbage collected, such as event listeners that aren't removed, or references held in closures", }); } } // Check for DOM growth if (potentialLeaks.length > 0) { const domLeaks = potentialLeaks.filter((leak) => leak.type === "continuous_dom_growth"); if (domLeaks.length > 0) { recommendations.push({ type: "dom_growth_prevention", description: "Continuous DOM growth detected", recommendation: "Ensure DOM elements are properly removed when no longer needed, and consider using DOM recycling for dynamic content", }); } } // Check for event listener leaks if (eventListenerLeaks.length > 0 || potentialLeaks.some((leak) => leak.type === "event_listener_growth")) { recommendations.push({ type: "event_listener_cleanup", description: "Potential event listener leak detected", recommendation: "Ensure all event listeners are properly removed when components are unmounted or destroyed", }); } // Check for excessive DOM size if (domSize.peakNodes > 1000) { recommendations.push({ type: "dom_size_optimization", description: `Large DOM size detected (${domSize.peakNodes} nodes)`, recommendation: "Reduce DOM size by using virtualization for long lists, lazy loading components, and removing unnecessary elements", }); } // Check for high memory usage if (memoryUsage.percentOfLimit && memoryUsage.percentOfLimit > 70) { recommendations.push({ type: "memory_usage_optimization", description: `High memory usage detected (${memoryUsage.percentOfLimit.toFixed(1)}% of limit)`, recommendation: "Optimize memory usage by reusing objects, using object pools, and avoiding large arrays or strings", }); } // Check for specific element growth if (elementGrowth.fastestGrowingTypes.length > 0) { const topGrower = elementGrowth.fastestGrowingTypes[0]; if (topGrower.netGrowth > 50) { recommendations.push({ type: "element_growth_optimization", description: `Rapid growth of ${topGrower.type} elements (${topGrower.netGrowth} net increase)`, recommendation: `Check for ${topGrower.type} elements that aren't being properly cleaned up or recycled`, }); } } return recommendations; }