UNPKG

js-memory-leak-detector

Version:

A comprehensive memory leak detector for web applications with Redux Toolkit support

204 lines 7.91 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MemoryLeakDetector = void 0; const event_listener_tracker_1 = require("./event-listener-tracker"); const timer_tracker_1 = require("./timer-tracker"); const dom_tracker_1 = require("./dom-tracker"); const redux_tracker_1 = require("./redux-tracker"); class MemoryLeakDetector { constructor(config = {}) { this.snapshots = []; this.isRunning = false; this.config = { enableEventListenerTracking: config.enableEventListenerTracking ?? true, enableTimerTracking: config.enableTimerTracking ?? true, enableDOMTracking: config.enableDOMTracking ?? true, enableReduxTracking: config.enableReduxTracking ?? true, enablePerformanceObserver: config.enablePerformanceObserver ?? true, reportInterval: config.reportInterval ?? 30000, // 30 seconds memoryThreshold: config.memoryThreshold ?? 300, // 300MB onReport: config.onReport ?? (() => { }), onLeak: config.onLeak ?? (() => { }) }; this.init(); } init() { if (this.config.enableEventListenerTracking) { this.eventTracker = new event_listener_tracker_1.EventListenerTracker(); } if (this.config.enableTimerTracking) { this.timerTracker = new timer_tracker_1.TimerTracker(); } if (this.config.enableDOMTracking) { this.domTracker = new dom_tracker_1.DOMTracker(); } if (this.config.enableReduxTracking) { this.reduxTracker = new redux_tracker_1.ReduxTracker(); } } start() { if (this.isRunning) return; this.isRunning = true; // Take initial snapshot this.takeSnapshot(); // Set up periodic reporting this.reportInterval = globalThis.setInterval(() => { this.generateReport(); }, this.config.reportInterval); console.log('🔍 Memory Leak Detector started'); } stop() { if (!this.isRunning) return; this.isRunning = false; if (this.reportInterval) { globalThis.clearInterval(this.reportInterval); this.reportInterval = undefined; } console.log('🛑 Memory Leak Detector stopped'); } getMemoryInfo() { // Try different ways to get memory info if (performance.memory) { return performance.memory; } // Fallback for environments without performance.memory return { heapUsed: 0, heapTotal: 0, external: 0, arrayBuffers: 0 }; } takeSnapshot() { const memory = this.getMemoryInfo(); const snapshot = { timestamp: Date.now(), memory: { heapUsed: memory.usedJSHeapSize || 0, heapTotal: memory.totalJSHeapSize || 0, external: memory.external || 0, arrayBuffers: memory.arrayBuffers || 0 }, counts: { eventListeners: this.eventTracker?.getActiveListeners() || 0, timers: this.timerTracker?.getActiveTimers() || 0, domNodes: this.domTracker?.getDOMNodeCount() || 0, detachedNodes: this.domTracker?.getDetachedNodeCount() || 0 } }; this.snapshots.push(snapshot); // Keep only last 100 snapshots if (this.snapshots.length > 100) { this.snapshots = this.snapshots.slice(-100); } return snapshot; } detectLeaks() { const suspects = []; // Collect suspects from all trackers if (this.eventTracker) { suspects.push(...this.eventTracker.detectLeaks()); } if (this.timerTracker) { suspects.push(...this.timerTracker.detectLeaks()); } if (this.domTracker) { suspects.push(...this.domTracker.detectLeaks()); } if (this.reduxTracker) { suspects.push(...this.reduxTracker.detectLeaks()); } // Analyze memory growth if (this.snapshots.length >= 2) { const latest = this.snapshots[this.snapshots.length - 1]; const previous = this.snapshots[this.snapshots.length - 2]; const memoryGrowth = latest.memory.heapUsed - previous.memory.heapUsed; const growthMB = memoryGrowth / (1024 * 1024); if (growthMB > 10) { // 10MB growth suspects.push({ type: 'closure', severity: growthMB > 50 ? 'critical' : 'high', description: `Memory increased by ${growthMB.toFixed(2)}MB in ${this.config.reportInterval / 1000}s`, count: Math.round(growthMB) }); } } return suspects; } generateRecommendations(suspects) { const recommendations = []; suspects.forEach(suspect => { switch (suspect.type) { case 'event-listener': recommendations.push('Remove event listeners when components unmount or are no longer needed'); break; case 'timer': recommendations.push('Clear intervals and timeouts when they are no longer needed'); break; case 'dom-reference': recommendations.push('Avoid keeping references to DOM elements after they are removed'); break; case 'detached-dom': recommendations.push('Check for detached DOM nodes that should be garbage collected'); break; case 'closure': recommendations.push('Review closures that might be holding references to large objects'); break; case 'redux-subscription': recommendations.push('Ensure Redux store subscriptions are properly unsubscribed when components unmount'); break; case 'redux-selector': recommendations.push('Check for infinite re-renders caused by improperly memoized selectors'); break; } }); return [...new Set(recommendations)]; // Remove duplicates } generateReport() { const snapshot = this.takeSnapshot(); const suspects = this.detectLeaks(); const recommendations = this.generateRecommendations(suspects); const report = { timestamp: snapshot.timestamp, heapUsed: snapshot.memory.heapUsed, heapTotal: snapshot.memory.heapTotal, external: snapshot.memory.external, arrayBuffers: snapshot.memory.arrayBuffers, leakSuspects: suspects, recommendations }; // Notify about individual leaks suspects.forEach(suspect => { this.config.onLeak(suspect); }); // Send report this.config.onReport(report); return report; } getSnapshots() { return [...this.snapshots]; } // Public methods for Redux integration patchReduxStore(store) { if (this.reduxTracker) { this.reduxTracker.patchStore(store); } } trackSelectorUsage(selectorName) { if (this.reduxTracker) { this.reduxTracker.trackSelectorUsage(selectorName); } } cleanup() { this.stop(); this.eventTracker?.cleanup(); this.timerTracker?.cleanup(); this.domTracker?.cleanup(); this.reduxTracker?.cleanup(); this.snapshots = []; } } exports.MemoryLeakDetector = MemoryLeakDetector; //# sourceMappingURL=memory-leak-detector.js.map