UNPKG

@code-zetta/ng-profiler

Version:

A tiny in-app profiler overlay for Angular that works in both zoned and zoneless applications

1,188 lines (1,182 loc) 95.7 kB
import * as i0 from '@angular/core'; import { Injectable, InjectionToken, Inject, HostListener, Component, Input, Directive, NgModule } from '@angular/core'; import * as i3 from '@angular/common'; import { CommonModule } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; class RecommendationService { /** * Analyze performance data and generate recommendations */ analyzePerformance(stats) { const recommendations = []; // Frame performance analysis this.analyzeFramePerformance(stats, recommendations); // Component render analysis this.analyzeComponentRenders(stats, recommendations); // Memory analysis (if available) this.analyzeMemoryUsage(stats, recommendations); // Best practices analysis this.analyzeBestPractices(stats, recommendations); // Sort by priority (highest first) return recommendations.sort((a, b) => b.priority - a.priority); } /** * Analyze frame performance issues */ analyzeFramePerformance(stats, recommendations) { // Critical frame time issues if (stats.lastFrameDuration > 50) { recommendations.push({ id: 'critical-frame-time', type: 'error', title: 'Critical Frame Time Detected', description: `Last frame took ${stats.lastFrameDuration.toFixed(2)}ms, which is significantly above the 16.67ms target for 60fps.`, severity: 'critical', category: 'performance', suggestions: [ 'Optimize heavy computations in your components', 'Use OnPush change detection strategy', 'Implement virtual scrolling for large lists', 'Consider using Web Workers for heavy calculations', 'Profile with Chrome DevTools to identify bottlenecks', ], impact: 'Severe impact on user experience and perceived performance', priority: 100, }); } // High average frame time if (stats.averageFrameDuration > 20) { recommendations.push({ id: 'high-average-frame-time', type: 'warning', title: 'High Average Frame Time', description: `Average frame time is ${stats.averageFrameDuration.toFixed(2)}ms, above the recommended 16.67ms.`, severity: 'high', category: 'performance', suggestions: [ 'Review components with frequent updates', 'Implement debouncing for rapid state changes', 'Use trackBy functions in ngFor loops', 'Consider lazy loading for heavy components', 'Optimize template expressions and bindings', ], impact: 'May cause noticeable lag and poor user experience', priority: 80, }); } // Low frame count warning if (stats.frameCount < 10) { recommendations.push({ id: 'low-frame-count', type: 'info', title: 'Limited Performance Data', description: 'Only a few frames have been measured. Consider interacting with the application to gather more data.', severity: 'low', category: 'performance', suggestions: [ 'Interact with various components in your application', 'Trigger different user actions to gather more data', 'Wait for more performance data to be collected', ], impact: 'Limited data may not reflect real-world performance', priority: 20, }); } } /** * Analyze component render issues */ analyzeComponentRenders(stats, recommendations) { const highRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'high'); const mediumRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'medium'); // Critical render issues if (highRenderComponents.length > 0) { const worstComponent = highRenderComponents[0]; recommendations.push({ id: 'critical-render-count', type: 'error', title: 'Excessive Component Renders', description: `Component "${worstComponent.name}" has rendered ${worstComponent.renderCount} times, indicating potential performance issues.`, severity: 'critical', category: 'rendering', suggestions: [ 'Review change detection strategy for this component', 'Check for unnecessary template bindings', 'Implement OnPush change detection', 'Use pure pipes instead of methods in templates', 'Consider memoization for expensive calculations', 'Review parent component for unnecessary re-renders', ], impact: 'Excessive renders can cause significant performance degradation', priority: 95, }); } // Multiple high-render components if (highRenderComponents.length > 2) { recommendations.push({ id: 'multiple-high-render-components', type: 'warning', title: 'Multiple Components with High Render Counts', description: `${highRenderComponents.length} components are rendering excessively, indicating systemic performance issues.`, severity: 'high', category: 'rendering', suggestions: [ 'Review global state management patterns', 'Check for circular dependencies between components', 'Implement proper component architecture', 'Consider using a state management library (NgRx, Akita)', 'Review service injection patterns', ], impact: 'Systemic rendering issues affect overall application performance', priority: 85, }); } // Medium render issues if (mediumRenderComponents.length > 3) { recommendations.push({ id: 'multiple-medium-render-components', type: 'warning', title: 'Several Components with Moderate Render Counts', description: `${mediumRenderComponents.length} components have moderate render counts that could be optimized.`, severity: 'medium', category: 'rendering', suggestions: [ 'Review change detection triggers', 'Optimize template expressions', 'Use trackBy functions in loops', 'Consider component composition patterns', 'Review input/output binding patterns', ], impact: 'Moderate performance impact that can accumulate', priority: 60, }); } // Advanced component analysis this.analyzeComponentPatterns(stats, recommendations); this.analyzeTemplateOptimization(stats, recommendations); } /** * Analyze component patterns and architecture */ analyzeComponentPatterns(stats, recommendations) { const allComponents = stats.topComponents; // Check for potential component composition issues if (allComponents.length > 5) { recommendations.push({ id: 'component-composition', type: 'optimization', title: 'Component Composition Optimization', description: 'Many components are being tracked, suggesting potential composition improvements.', severity: 'low', category: 'rendering', suggestions: [ 'Consider using smart/dumb component pattern', 'Implement proper component hierarchy', 'Review component responsibilities', 'Consider using content projection', 'Implement proper component interfaces', ], impact: 'Improves component maintainability and performance', priority: 35, }); } // Check for potential over-engineering if (allComponents.length > 10) { recommendations.push({ id: 'component-over-engineering', type: 'warning', title: 'Potential Component Over-Engineering', description: 'Many small components detected. Consider if this level of granularity is necessary.', severity: 'medium', category: 'rendering', suggestions: [ 'Review component granularity', 'Consider combining related components', 'Implement proper component boundaries', 'Review component communication patterns', 'Consider using feature modules', ], impact: 'Reduces complexity and improves maintainability', priority: 45, }); } } /** * Analyze template optimization opportunities */ analyzeTemplateOptimization(stats, recommendations) { // High render counts often indicate template optimization opportunities const highRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'high'); if (highRenderComponents.length > 0) { recommendations.push({ id: 'template-optimization', type: 'optimization', title: 'Template Optimization Opportunities', description: 'High render counts suggest template optimization opportunities.', severity: 'medium', category: 'rendering', suggestions: [ 'Use OnPush change detection strategy', 'Implement pure pipes for expensive calculations', 'Use trackBy functions in ngFor loops', 'Avoid function calls in templates', 'Use async pipe for observables', 'Consider using signals for reactive templates', ], impact: 'Reduces template evaluation overhead and improves performance', priority: 70, }); } } /** * Analyze memory usage patterns */ analyzeMemoryUsage(stats, recommendations) { // Enhanced memory analysis with more specific recommendations // High frame count indicates potential memory pressure if (stats.frameCount > 100) { recommendations.push({ id: 'memory-best-practices', type: 'optimization', title: 'Memory Optimization Opportunities', description: 'With significant application usage, consider memory optimization strategies.', severity: 'medium', category: 'memory', suggestions: [ 'Implement proper component destruction', 'Unsubscribe from observables in ngOnDestroy', 'Use weak references where appropriate', 'Consider using OnPush change detection', 'Implement proper cleanup for timers and intervals', 'Review for memory leaks in event listeners', ], impact: 'Prevents memory leaks and improves long-term performance', priority: 50, }); } // Very high frame count suggests potential memory issues if (stats.frameCount > 500) { recommendations.push({ id: 'memory-pressure-detected', type: 'warning', title: 'Potential Memory Pressure Detected', description: 'High frame count suggests potential memory pressure or performance degradation over time.', severity: 'high', category: 'memory', suggestions: [ 'Monitor memory usage with Chrome DevTools', 'Check for memory leaks in long-running operations', 'Implement memory profiling in development', 'Consider implementing memory monitoring', 'Review component lifecycle management', 'Use Angular DevTools for memory analysis', ], impact: 'Memory pressure can cause application crashes and poor performance', priority: 75, }); } // Analyze component render patterns for memory implications const highRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'high'); if (highRenderComponents.length > 0) { recommendations.push({ id: 'memory-render-correlation', type: 'info', title: 'Memory-Render Correlation', description: 'High render counts may indicate memory allocation patterns that could be optimized.', severity: 'medium', category: 'memory', suggestions: [ 'Review object creation in frequently rendered components', 'Consider object pooling for frequently created objects', 'Implement memoization for expensive calculations', 'Use immutable data structures where possible', 'Review for unnecessary object allocations in templates', ], impact: 'Reduces garbage collection pressure and improves performance', priority: 45, }); } } /** * Analyze best practices and advanced optimizations */ analyzeBestPractices(stats, recommendations) { // General best practices based on data patterns if (stats.frameCount > 50 && stats.averageFrameDuration > 10) { recommendations.push({ id: 'performance-optimization', type: 'optimization', title: 'Performance Optimization Opportunities', description: 'Application shows opportunities for performance improvements.', severity: 'medium', category: 'best-practice', suggestions: [ 'Use Angular DevTools for detailed analysis', 'Implement code splitting and lazy loading', 'Optimize bundle size with tree shaking', 'Use Angular Universal for SSR if applicable', 'Consider using Angular PWA features', 'Implement proper caching strategies', ], impact: 'Improves overall application performance and user experience', priority: 70, }); } // Zone.js specific recommendations if (stats.frameCount > 20) { recommendations.push({ id: 'zone-optimization', type: 'optimization', title: 'Zone.js Optimization', description: 'Consider optimizing Zone.js usage for better performance.', severity: 'low', category: 'best-practice', suggestions: [ 'Use NgZone.runOutsideAngular for heavy computations', 'Consider zoneless change detection for specific components', 'Optimize async operations to minimize change detection', 'Use trackBy functions in ngFor loops', 'Implement proper error boundaries', ], impact: 'Reduces unnecessary change detection cycles', priority: 40, }); } // Network performance recommendations this.analyzeNetworkPerformance(stats, recommendations); // Bundle optimization recommendations this.analyzeBundleOptimization(stats, recommendations); // State management recommendations this.analyzeStateManagement(stats, recommendations); } /** * Analyze network performance patterns */ analyzeNetworkPerformance(stats, recommendations) { // High frame times might indicate network-related issues if (stats.averageFrameDuration > 25) { recommendations.push({ id: 'network-performance', type: 'warning', title: 'Potential Network Performance Issues', description: 'High frame times may indicate network-related performance bottlenecks.', severity: 'medium', category: 'best-practice', suggestions: [ 'Implement request caching strategies', 'Use HTTP interceptors for request optimization', 'Consider implementing request deduplication', 'Optimize API response payloads', 'Implement progressive loading for large datasets', 'Use service workers for offline capabilities', ], impact: 'Improves data loading performance and user experience', priority: 65, }); } } /** * Analyze bundle optimization opportunities */ analyzeBundleOptimization(stats, recommendations) { if (stats.frameCount > 30) { recommendations.push({ id: 'bundle-optimization', type: 'optimization', title: 'Bundle Optimization Opportunities', description: 'Consider optimizing your application bundle for better performance.', severity: 'medium', category: 'best-practice', suggestions: [ 'Implement dynamic imports for code splitting', 'Use Angular CLI build optimization flags', 'Analyze bundle with webpack-bundle-analyzer', 'Remove unused dependencies', 'Optimize third-party library imports', 'Consider using standalone components for better tree-shaking', ], impact: 'Reduces initial load time and improves perceived performance', priority: 55, }); } } /** * Analyze state management patterns */ analyzeStateManagement(stats, recommendations) { const highRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'high'); if (highRenderComponents.length > 1) { recommendations.push({ id: 'state-management', type: 'optimization', title: 'State Management Optimization', description: 'Multiple components with high render counts suggest state management improvements.', severity: 'medium', category: 'best-practice', suggestions: [ 'Consider implementing NgRx or similar state management', 'Review service injection patterns', 'Implement proper state isolation', 'Use BehaviorSubject for reactive state', 'Consider using signals for fine-grained reactivity', 'Review component communication patterns', ], impact: 'Improves state predictability and reduces unnecessary renders', priority: 60, }); } } /** * Get recommendation by ID */ getRecommendationById(id, stats) { const recommendations = this.analyzePerformance(stats); return recommendations.find((rec) => rec.id === id) || null; } /** * Get recommendations by category */ getRecommendationsByCategory(category, stats) { const recommendations = this.analyzePerformance(stats); return recommendations.filter((rec) => rec.category === category); } /** * Get critical recommendations only */ getCriticalRecommendations(stats) { const recommendations = this.analyzePerformance(stats); return recommendations.filter((rec) => rec.severity === 'critical' || rec.severity === 'high'); } /** * Check if performance is within acceptable thresholds */ isPerformanceAcceptable(stats) { return (stats.lastFrameDuration <= 16.67 && stats.averageFrameDuration <= 16.67 && stats.topComponents.filter((comp) => comp.severity === 'high').length === 0); } /** * Get performance score (0-100) */ getPerformanceScore(stats) { let score = 100; // Deduct points for frame time issues if (stats.lastFrameDuration > 50) score -= 30; else if (stats.lastFrameDuration > 33) score -= 20; else if (stats.lastFrameDuration > 16.67) score -= 10; if (stats.averageFrameDuration > 33) score -= 25; else if (stats.averageFrameDuration > 16.67) score -= 15; // Deduct points for render issues const highRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'high'); score -= highRenderComponents.length * 10; const mediumRenderComponents = stats.topComponents.filter((comp) => comp.severity === 'medium'); score -= mediumRenderComponents.length * 5; // Ensure score doesn't go below 0 return Math.max(0, score); } /** * Get performance health status */ getPerformanceHealth(stats) { const score = this.getPerformanceScore(stats); if (score >= 90) return 'excellent'; if (score >= 75) return 'good'; if (score >= 60) return 'fair'; if (score >= 40) return 'poor'; return 'critical'; } /** * Get performance summary */ getPerformanceSummary(stats) { const score = this.getPerformanceScore(stats); const health = this.getPerformanceHealth(stats); const acceptable = this.isPerformanceAcceptable(stats); const recommendations = this.analyzePerformance(stats); const criticalIssues = recommendations.filter((rec) => rec.severity === 'critical').length; const warnings = recommendations.filter((rec) => rec.severity === 'high' || rec.severity === 'medium').length; return { score, health, acceptable, criticalIssues, warnings, }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: RecommendationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: RecommendationService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: RecommendationService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class ProfilerStore { recommendationService; renderCounts = new Map(); elementMap = new Map(); frameMeasurements = []; maxFrameHistory = 100; isPaused = false; constructor(recommendationService) { this.recommendationService = recommendationService; } statsSubject = new BehaviorSubject({ lastFrameDuration: 0, averageFrameDuration: 0, frameCount: 0, topComponents: [], lastMeasurement: Date.now(), }); stats$ = this.statsSubject.asObservable(); /** * Increment render count for a component */ incrementRenderCount(name, element) { if (this.isPaused) { return; // Don't count renders when paused } const currentCount = this.renderCounts.get(name) || 0; const newCount = currentCount + 1; this.renderCounts.set(name, newCount); if (element) { this.elementMap.set(name, element); } this.updateStats(); } /** * Add frame measurement */ addFrameMeasurement(duration) { if (this.isPaused) { return; // Don't add frame measurements when paused } const measurement = { duration, timestamp: Date.now(), }; this.frameMeasurements.push(measurement); // Keep only the last N measurements if (this.frameMeasurements.length > this.maxFrameHistory) { this.frameMeasurements.shift(); } this.updateStats(); } /** * Get severity level based on render count */ getSeverity(renderCount) { if (renderCount <= 5) return 'low'; if (renderCount <= 20) return 'medium'; return 'high'; } /** * Update statistics */ updateStats() { const currentStats = this.statsSubject.value; // Calculate frame statistics const lastFrame = this.frameMeasurements[this.frameMeasurements.length - 1]; const lastFrameDuration = lastFrame ? lastFrame.duration : 0; const averageFrameDuration = this.frameMeasurements.length > 0 ? this.frameMeasurements.reduce((sum, m) => sum + m.duration, 0) / this.frameMeasurements.length : 0; // Get top components by render count const topComponents = Array.from(this.renderCounts.entries()) .map(([name, count]) => ({ name, renderCount: count, severity: this.getSeverity(count), element: this.elementMap.get(name), })) .sort((a, b) => b.renderCount - a.renderCount) .slice(0, 10); const newStats = { lastFrameDuration, averageFrameDuration, frameCount: this.frameMeasurements.length, topComponents, lastMeasurement: Date.now(), }; // Add recommendations after creating the stats object newStats.recommendations = this.recommendationService.analyzePerformance(newStats); this.statsSubject.next(newStats); } /** * Reset all statistics */ reset() { this.renderCounts.clear(); this.elementMap.clear(); this.frameMeasurements = []; this.updateStats(); } /** * Export current statistics as JSON */ exportStats() { const stats = this.statsSubject.value; // Create a clean version of topComponents without circular references const cleanTopComponents = stats.topComponents.map((component) => ({ name: component.name, renderCount: component.renderCount, severity: component.severity, // Remove the element property to avoid circular references })); // Get performance summary and analysis const performanceSummary = this.recommendationService.getPerformanceSummary(stats); const criticalRecommendations = this.recommendationService.getCriticalRecommendations(stats); const recommendationsByCategory = { performance: this.recommendationService.getRecommendationsByCategory('performance', stats), rendering: this.recommendationService.getRecommendationsByCategory('rendering', stats), memory: this.recommendationService.getRecommendationsByCategory('memory', stats), 'best-practice': this.recommendationService.getRecommendationsByCategory('best-practice', stats) }; const exportData = { // Basic stats lastFrameDuration: stats.lastFrameDuration, averageFrameDuration: stats.averageFrameDuration, frameCount: stats.frameCount, topComponents: cleanTopComponents, lastMeasurement: stats.lastMeasurement, renderCounts: Object.fromEntries(this.renderCounts), frameMeasurements: this.frameMeasurements, // Enhanced performance analysis performanceSummary: { score: performanceSummary.score, health: performanceSummary.health, acceptable: performanceSummary.acceptable, criticalIssues: performanceSummary.criticalIssues, warnings: performanceSummary.warnings, isPerformanceAcceptable: this.recommendationService.isPerformanceAcceptable(stats) }, // Comprehensive recommendations recommendations: stats.recommendations, criticalRecommendations: criticalRecommendations, recommendationsByCategory: recommendationsByCategory, // Performance thresholds and standards performanceThresholds: { targetFrameTime: 16.67, // 60fps acceptableFrameTime: 33.33, // 30fps criticalFrameTime: 50, excellentScore: 90, goodScore: 75, fairScore: 60, poorScore: 40 }, // Export metadata exportTimestamp: new Date().toISOString(), profilerVersion: '1.0.0', exportFormat: 'enhanced-json' }; return JSON.stringify(exportData, null, 2); } /** * Get current render count for a component */ getRenderCount(name) { return this.renderCounts.get(name) || 0; } /** * Get all render counts */ getAllRenderCounts() { return new Map(this.renderCounts); } /** * Pause render counting */ pauseRenderCounting() { this.isPaused = true; } /** * Resume render counting */ resumeRenderCounting() { this.isPaused = false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: ProfilerStore, deps: [{ token: RecommendationService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: ProfilerStore, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: ProfilerStore, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: RecommendationService }] }); const NG_PROFILER_CONFIG = new InjectionToken('NgProfilerConfig'); class NgProfilerService { appRef; profilerStore; config; isEnabled = false; isZoned = false; originalTick; frameStartTime = 0; longTaskObserver = null; animationFrameId = null; constructor(appRef, profilerStore, config) { this.appRef = appRef; this.profilerStore = profilerStore; this.config = config; this.detectZone(); this.setupHotkey(); } /** * Initialize the profiler */ initialize() { console.log('NgProfiler: Initializing profiler...'); console.log('NgProfiler: shouldEnable() =', this.shouldEnable()); console.log('NgProfiler: isProduction() =', this.isProduction()); console.log('NgProfiler: isZoned =', this.isZoned); if (!this.shouldEnable()) { console.log('NgProfiler: Profiler disabled'); return; } console.log('NgProfiler: Profiler enabled'); this.isEnabled = true; if (this.isZoned) { console.log('NgProfiler: Setting up zoned measurement'); this.patchApplicationRef(); } else { console.log('NgProfiler: Setting up zoneless measurement'); this.setupZonelessMeasurement(); } } /** * Check if profiler should be enabled */ shouldEnable() { if (this.config.enableInProduction) { return true; } // Disable in production unless forced return !this.isProduction(); } /** * Check if running in production */ isProduction() { return (typeof window !== 'undefined' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1' && window.location.protocol !== 'file:'); } /** * Detect if Zone.js is present */ detectZone() { this.isZoned = typeof window.Zone !== 'undefined' && window.Zone.current; } /** * Patch ApplicationRef.tick() for zoned applications */ patchApplicationRef() { if (this.originalTick) { return; // Already patched } this.originalTick = this.appRef.tick; this.appRef.tick = () => { const startTime = performance.now(); try { const result = this.originalTick.call(this.appRef); const endTime = performance.now(); const duration = endTime - startTime; if (this.isEnabled) { this.profilerStore.addFrameMeasurement(duration); } return result; } catch (error) { const endTime = performance.now(); const duration = endTime - startTime; if (this.isEnabled) { this.profilerStore.addFrameMeasurement(duration); } throw error; } }; } /** * Setup measurement for zoneless applications */ setupZonelessMeasurement() { // Use requestAnimationFrame to measure frame timing const measureFrame = () => { const currentTime = performance.now(); if (this.frameStartTime > 0 && this.isEnabled) { const frameDuration = currentTime - this.frameStartTime; this.profilerStore.addFrameMeasurement(frameDuration); } this.frameStartTime = currentTime; this.animationFrameId = requestAnimationFrame(measureFrame); }; this.animationFrameId = requestAnimationFrame(measureFrame); // Setup PerformanceObserver for long tasks if ('PerformanceObserver' in window) { try { this.longTaskObserver = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.entryType === 'longtask' && this.isEnabled) { const duration = entry.duration; this.profilerStore.addFrameMeasurement(duration); } } }); this.longTaskObserver.observe({ entryTypes: ['longtask'] }); } catch (error) { console.warn('NgProfiler: PerformanceObserver not supported'); } } } /** * Setup hotkey listener */ setupHotkey() { const hotkey = this.config.hotkey || 'Ctrl+Shift+P'; document.addEventListener('keydown', (event) => { const isCtrl = event.ctrlKey || event.metaKey; const isShift = event.shiftKey; const isP = event.key === 'p' || event.key === 'P'; if (isCtrl && isShift && isP) { event.preventDefault(); console.log('NgProfiler: Hotkey detected, toggling overlay'); this.toggleOverlay(); } }); } /** * Toggle overlay visibility */ toggleOverlay() { // This will be handled by the overlay component console.log('NgProfiler: Dispatching toggle event'); const event = new CustomEvent('ng-profiler-toggle'); document.dispatchEvent(event); } /** * Cleanup resources */ destroy() { if (this.originalTick && this.appRef.tick !== this.originalTick) { this.appRef.tick = this.originalTick; } if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); } if (this.longTaskObserver) { this.longTaskObserver.disconnect(); } } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Check if profiler is enabled */ isProfilerEnabled() { return this.isEnabled; } /** * Check if running in zoned mode */ isZonedMode() { return this.isZoned; } /** * Pause profiling temporarily */ pauseProfiling() { this.isEnabled = false; // Also stop any ongoing measurements if (this.animationFrameId) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } if (this.longTaskObserver) { this.longTaskObserver.disconnect(); } } /** * Resume profiling */ resumeProfiling() { if (this.shouldEnable()) { this.isEnabled = true; // Restart measurements if they were stopped if (!this.isZoned && !this.animationFrameId) { this.setupZonelessMeasurement(); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: NgProfilerService, deps: [{ token: i0.ApplicationRef }, { token: ProfilerStore }, { token: NG_PROFILER_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: NgProfilerService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImport: i0, type: NgProfilerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: i0.ApplicationRef }, { type: ProfilerStore }, { type: undefined, decorators: [{ type: Inject, args: [NG_PROFILER_CONFIG] }] }] }); class NgProfilerOverlayComponent { profilerStore; profilerService; isVisible = true; // Show by default in development stats = { lastFrameDuration: 0, averageFrameDuration: 0, frameCount: 0, topComponents: [], lastMeasurement: Date.now(), }; subscription = null; position = 'top-right'; constructor(profilerStore, profilerService) { this.profilerStore = profilerStore; this.profilerService = profilerService; this.position = this.profilerService.getConfig().position || 'top-right'; } ngOnInit() { console.log('NgProfiler: Overlay component initialized, isVisible:', this.isVisible); this.subscription = this.profilerStore.stats$.subscribe((stats) => { this.stats = stats; }); // Listen for toggle events document.addEventListener('ng-profiler-toggle', () => { console.log('NgProfiler: Toggle event received, toggling visibility'); this.toggleVisibility(); }); } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } onEscapeKey() { if (this.isVisible) { this.isVisible = false; } } toggleVisibility() { this.isVisible = !this.isVisible; console.log('NgProfiler: Overlay visibility toggled to:', this.isVisible); // Force change detection and DOM update setTimeout(() => { const overlay = document.querySelector('.ng-profiler-overlay'); if (overlay) { // Manually update classes to ensure they're correct if (this.isVisible) { overlay.classList.add('visible'); } else { overlay.classList.remove('visible'); } console.log('NgProfiler: Overlay classes after manual update:', overlay.className); console.log('NgProfiler: Overlay computed style visibility:', getComputedStyle(overlay).visibility); console.log('NgProfiler: Overlay computed style opacity:', getComputedStyle(overlay).opacity); console.log('NgProfiler: Overlay computed style transform:', getComputedStyle(overlay).transform); } }, 50); } resetStats() { this.profilerStore.reset(); } exportStats() { console.log('NgProfiler: Export button clicked'); try { // Take a snapshot of stats BEFORE starting the export process const statsSnapshot = this.profilerStore.exportStats(); console.log('NgProfiler: Stats snapshot taken before JSON export'); // Temporarily pause profiling to avoid counting export renders this.profilerService.pauseProfiling(); this.profilerStore.pauseRenderCounting(); console.log('NgProfiler: Profiling paused during JSON export'); // Use the snapshot data instead of calling exportStats again const blob = new Blob([statsSnapshot], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ng-profiler-stats-${new Date() .toISOString() .slice(0, 19) .replace(/:/g, '-')}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('NgProfiler: Export completed successfully'); } catch (error) { console.error('NgProfiler: Export failed:', error); } finally { // Always resume profiling, even if export failed this.profilerService.resumeProfiling(); this.profilerStore.resumeRenderCounting(); console.log('NgProfiler: Profiling resumed after JSON export'); } } async exportPDF() { try { // Take a snapshot of stats BEFORE starting ANY export process const statsSnapshot = this.profilerStore.exportStats(); const statsData = JSON.parse(statsSnapshot); // Completely disable the profiler during export this.profilerService.pauseProfiling(); this.profilerStore.pauseRenderCounting(); // Wait for any pending operations to complete await new Promise((resolve) => setTimeout(resolve, 200)); // Force garbage collection if available if (window.gc) { window.gc(); } // Dynamically import jsPDF only (no html2canvas to avoid DOM manipulation) // Use a more robust dynamic import approach let jsPDF; try { // Try multiple import strategies try { // Strategy 1: Direct dynamic import jsPDF = await import('jspdf').then((m) => m.default); } catch (e1) { try { // Strategy 2: Function constructor approach const importJspdf = new Function('return import("jspdf")'); jsPDF = await importJspdf().then((m) => m.default); } catch (e2) { try { // Strategy 3: Try to access from window object jsPDF = window.jsPDF; if (!jsPDF) { throw new Error('jsPDF not found in window object'); } } catch (e3) { throw new Error('All import strategies failed'); } } } } catch (importError) { console.error('NgProfiler: jspdf import failed:', importError); alert('PDF export requires jspdf and html2canvas packages. Please install them with: npm install jspdf html2canvas'); return; } // Create PDF directly from data (no DOM manipulation) const pdf = new jsPDF('p', 'mm', 'a4'); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const margin = 20; const contentWidth = pageWidth - 2 * margin; let yPosition = margin; // Helper function to check if we need a new page const checkNewPage = (requiredSpace) => { if (yPosition + requiredSpace > pageHeight - margin) { pdf.addPage(); yPosition = margin; return true; } return false; }; // Helper function to add text with word wrapping const addWrappedText = (text, fontSize, y, isBold = false) => { pdf.setFontSize(fontSize); if (isBold) pdf.setFont('helvetica', 'bold'); else pdf.setFont('helvetica', 'normal'); const lines = pdf.splitTextToSize(text, contentWidth); // Check if we need a new page for this text const textHeight = lines.length * fontSize * 0.4; checkNewPage(textHeight); pdf.text(lines, margin, yPosition); yPosition += textHeight; return yPosition; }; // Helper function to add a section const addSection = (title) => { checkNewPage(50); // Space for title + spacing yPosition = addWrappedText(title, 16, yPosition, true); yPosition += 20; // Increased spacing after section title return yPosition; }; // Title yPosition = addWrappedText('Angular Profiler Report', 24, yPosition, true); yPosition = addWrappedText(`Generated on ${new Date().toLocaleString()}`, 12, yPosition); yPosition += 20; // Performance Score and Health if (statsData.performanceSummary) { yPosition = addSection('Performance Health Score'); const score = statsData.performanceSummary.score; const health = statsData.performanceSummary.health; const healthColor = health === 'excellent' ? [40, 167, 69] : health === 'good' ? [0, 122, 204] : health === 'fair' ? [255, 193, 7] : health === 'poor' ? [255, 152, 0] : [220, 53, 69]; // Score circle const centerX = pageWidth / 2; const centerY = yPosition + 20; const radius = 30; pdf.setFillColor(healthColor[0], healthColor[1], healthColor[2]); pdf.circle(centerX, centerY, radius, 'F'); pdf.setFontSize(18); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(255, 255, 255); pdf.text(score.toString(), centerX - 10, centerY + 6); // Health label - Fixed positioning and visibility pdf.setFontSize(16); pdf.setFont('helvetica', 'bold'); pdf.setTextColor(healthColor[0], healthColor[1], healthColor[2]); pdf.text(health.toUpperCase(), centerX - 30, centerY + 40); // Status indicators - Fixed layout and values const statusY = yPosition + 70; const statusItems = [ { label: 'Performance Acceptable', value: statsData.performanceSummary.acceptable ? 'YES' : 'NO', color: statsData.performanceSummary.acceptable ? [40, 167, 69] : [220, 53, 69], }, { label: 'Critical Issues', value: statsData.performanceSummary.criticalIssues.toString(), color: [220, 53, 69], }, { label: 'Warnings', value: statsData.performanceSummary.warnings.toString(), color: [255, 193, 7], }, ]; statusItems.forEach((item, index) => { const x = margin + index * (contentWidth / 3); // Draw background box for better visibility - Increased height for text pdf.setFillColor(248, 249, 250);