- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
501 lines
14 KiB
JavaScript
501 lines
14 KiB
JavaScript
/**
|
|
* 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<string>} 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;
|