/** * LiveComponents Performance Profiler * * Provides detailed performance profiling with flamegraph and timeline visualization. * * Features: * - Real-time performance metrics collection * - Flamegraph generation for call stack visualization * - Timeline visualization for event sequencing * - Export to Chrome DevTools format * - Integration with Performance API * * @module PerformanceProfiler */ export class PerformanceProfiler { constructor(options = {}) { this.enabled = options.enabled ?? false; this.maxSamples = options.maxSamples ?? 1000; this.samplingInterval = options.samplingInterval ?? 10; // ms this.autoStart = options.autoStart ?? false; this.samples = []; this.timeline = []; this.marks = new Map(); this.measures = new Map(); this.isRecording = false; this.recordingStartTime = null; if (this.autoStart && this.enabled) { this.start(); } } /** * Start performance profiling */ start() { if (this.isRecording) { console.warn('[PerformanceProfiler] Already recording'); return; } this.isRecording = true; this.recordingStartTime = performance.now(); this.samples = []; this.timeline = []; console.log('[PerformanceProfiler] Started recording'); } /** * Stop performance profiling * @returns {Object} Profiling results */ stop() { if (!this.isRecording) { console.warn('[PerformanceProfiler] Not recording'); return null; } this.isRecording = false; const duration = performance.now() - this.recordingStartTime; const results = { duration, samples: this.samples, timeline: this.timeline, marks: Array.from(this.marks.entries()), measures: Array.from(this.measures.entries()), summary: this.generateSummary() }; console.log('[PerformanceProfiler] Stopped recording', results); return results; } /** * Mark a specific point in time * @param {string} name - Mark name * @param {Object} metadata - Additional metadata */ mark(name, metadata = {}) { if (!this.isRecording && !this.enabled) return; const timestamp = performance.now(); const mark = { name, timestamp, relativeTime: timestamp - this.recordingStartTime, metadata }; this.marks.set(name, mark); // Also use native Performance API if (performance.mark) { performance.mark(name); } // Add to timeline this.timeline.push({ type: 'mark', ...mark }); } /** * Measure duration between two marks * @param {string} name - Measure name * @param {string} startMark - Start mark name * @param {string} endMark - End mark name (optional, defaults to now) */ measure(name, startMark, endMark = null) { if (!this.isRecording && !this.enabled) return; const start = this.marks.get(startMark); if (!start) { console.warn(`[PerformanceProfiler] Start mark "${startMark}" not found`); return; } const endTime = endMark ? this.marks.get(endMark)?.timestamp : performance.now(); if (!endTime) { console.warn(`[PerformanceProfiler] End mark "${endMark}" not found`); return; } const duration = endTime - start.timestamp; const measure = { name, startMark, endMark, duration, startTime: start.timestamp, endTime, relativeStartTime: start.relativeTime, relativeEndTime: endTime - this.recordingStartTime }; this.measures.set(name, measure); // Native Performance API if (performance.measure && endMark) { try { performance.measure(name, startMark, endMark); } catch (e) { // Ignore if marks don't exist in native API } } // Add to timeline this.timeline.push({ type: 'measure', ...measure }); } /** * Record a sample for flamegraph * @param {string} functionName - Function name * @param {Array} stackTrace - Call stack * @param {number} duration - Execution duration */ sample(functionName, stackTrace = [], duration = 0) { if (!this.isRecording) return; if (this.samples.length >= this.maxSamples) { console.warn('[PerformanceProfiler] Max samples reached'); return; } this.samples.push({ timestamp: performance.now(), relativeTime: performance.now() - this.recordingStartTime, functionName, stackTrace, duration }); } /** * Generate flamegraph data * @returns {Object} Flamegraph data structure */ generateFlamegraph() { if (this.samples.length === 0) { return null; } // Build call tree from samples const root = { name: '(root)', value: 0, children: [] }; for (const sample of this.samples) { let currentNode = root; const stack = [sample.functionName, ...sample.stackTrace].reverse(); for (const funcName of stack) { let child = currentNode.children.find(c => c.name === funcName); if (!child) { child = { name: funcName, value: 0, children: [] }; currentNode.children.push(child); } child.value += sample.duration || 1; currentNode = child; } } return root; } /** * Generate timeline data for visualization * @returns {Array} Timeline events */ generateTimeline() { return this.timeline.map(event => ({ ...event, category: this.categorizeEvent(event), color: this.getEventColor(event) })); } /** * Generate summary statistics * @returns {Object} Summary data */ generateSummary() { const durations = Array.from(this.measures.values()).map(m => m.duration); if (durations.length === 0) { return { totalMeasures: 0, totalSamples: this.samples.length, totalDuration: 0 }; } const totalDuration = durations.reduce((sum, d) => sum + d, 0); const avgDuration = totalDuration / durations.length; const maxDuration = Math.max(...durations); const minDuration = Math.min(...durations); // Calculate percentiles const sortedDurations = [...durations].sort((a, b) => a - b); const p50 = sortedDurations[Math.floor(sortedDurations.length * 0.5)]; const p90 = sortedDurations[Math.floor(sortedDurations.length * 0.9)]; const p99 = sortedDurations[Math.floor(sortedDurations.length * 0.99)]; return { totalMeasures: this.measures.size, totalSamples: this.samples.length, totalDuration, avgDuration, maxDuration, minDuration, percentiles: { p50, p90, p99 } }; } /** * Export to Chrome DevTools Performance format * @returns {Object} Chrome DevTools trace event format */ exportToChromeTrace() { const events = []; // Add marks for (const [name, mark] of this.marks.entries()) { events.push({ name, cat: 'mark', ph: 'R', // Instant event ts: mark.timestamp * 1000, // microseconds pid: 1, tid: 1, args: mark.metadata }); } // Add measures as duration events for (const [name, measure] of this.measures.entries()) { events.push({ name, cat: 'measure', ph: 'B', // Begin ts: measure.startTime * 1000, pid: 1, tid: 1 }); events.push({ name, cat: 'measure', ph: 'E', // End ts: measure.endTime * 1000, pid: 1, tid: 1 }); } // Add samples for (const sample of this.samples) { events.push({ name: sample.functionName, cat: 'sample', ph: 'X', // Complete event ts: sample.timestamp * 1000, dur: (sample.duration || 1) * 1000, pid: 1, tid: 1, args: { stackTrace: sample.stackTrace } }); } return { traceEvents: events.sort((a, b) => a.ts - b.ts), displayTimeUnit: 'ms', metadata: { 'clock-domain': 'PERFORMANCE_NOW' } }; } /** * Categorize timeline event * @private */ categorizeEvent(event) { if (event.type === 'mark') { if (event.name.includes('action')) return 'action'; if (event.name.includes('render')) return 'render'; if (event.name.includes('state')) return 'state'; return 'other'; } if (event.type === 'measure') { if (event.duration > 100) return 'slow'; if (event.duration > 16) return 'normal'; return 'fast'; } return 'unknown'; } /** * Get color for event visualization * @private */ getEventColor(event) { const colorMap = { action: '#4CAF50', render: '#2196F3', state: '#FF9800', slow: '#F44336', normal: '#FFC107', fast: '#8BC34A', other: '#9E9E9E', unknown: '#607D8B' }; return colorMap[this.categorizeEvent(event)] || '#607D8B'; } /** * Clear all profiling data */ clear() { this.samples = []; this.timeline = []; this.marks.clear(); this.measures.clear(); this.recordingStartTime = null; // Clear native Performance API if (performance.clearMarks) { performance.clearMarks(); } if (performance.clearMeasures) { performance.clearMeasures(); } } /** * Get current profiling status * @returns {Object} Status information */ getStatus() { return { enabled: this.enabled, isRecording: this.isRecording, samplesCount: this.samples.length, marksCount: this.marks.size, measuresCount: this.measures.size, timelineEventsCount: this.timeline.length, recordingDuration: this.isRecording ? performance.now() - this.recordingStartTime : 0 }; } } /** * LiveComponents Performance Integration * * Automatic profiling for LiveComponents actions and lifecycle events */ export class LiveComponentsProfiler extends PerformanceProfiler { constructor(component, options = {}) { super(options); this.component = component; this.actionCallDepth = 0; if (this.enabled) { this.instrumentComponent(); } } /** * Instrument component for automatic profiling * @private */ instrumentComponent() { // Hook into action calls const originalCall = this.component.call.bind(this.component); this.component.call = (actionName, params, options) => { this.actionCallDepth++; const markName = `action:${actionName}:${this.actionCallDepth}`; this.mark(`${markName}:start`, { actionName, params, depth: this.actionCallDepth }); const result = originalCall(actionName, params, options); // Handle promise results if (result instanceof Promise) { return result.finally(() => { this.mark(`${markName}:end`); this.measure(`action:${actionName}`, `${markName}:start`, `${markName}:end`); this.actionCallDepth--; }); } this.mark(`${markName}:end`); this.measure(`action:${actionName}`, `${markName}:start`, `${markName}:end`); this.actionCallDepth--; return result; }; // Hook into state updates if (this.component.state) { const originalSet = this.component.state.set.bind(this.component.state); this.component.state.set = (key, value) => { this.mark(`state:set:${key}`, { key, value }); return originalSet(key, value); }; } // Hook into renders if (this.component.render) { const originalRender = this.component.render.bind(this.component); this.component.render = () => { this.mark('render:start'); const result = originalRender(); if (result instanceof Promise) { return result.finally(() => { this.mark('render:end'); this.measure('render', 'render:start', 'render:end'); }); } this.mark('render:end'); this.measure('render', 'render:start', 'render:end'); return result; }; } } } // Export default instance export default PerformanceProfiler;