Files
michaelschiemer/resources/js/modules/api-manager/PerformanceManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

756 lines
27 KiB
JavaScript

// modules/api-manager/PerformanceManager.js
import { Logger } from '../../core/logger.js';
/**
* Performance APIs Manager - Timing, Metrics, Observers, Optimization
*/
export class PerformanceManager {
constructor(config = {}) {
this.config = config;
this.marks = new Map();
this.measures = new Map();
this.observers = new Map();
this.metrics = new Map();
this.thresholds = {
fcp: 2000, // First Contentful Paint
lcp: 2500, // Largest Contentful Paint
fid: 100, // First Input Delay
cls: 0.1, // Cumulative Layout Shift
...config.thresholds
};
// Check API support
this.support = {
performance: 'performance' in window,
timing: 'timing' in (window.performance || {}),
navigation: 'navigation' in (window.performance || {}),
observer: 'PerformanceObserver' in window,
memory: 'memory' in (window.performance || {}),
userTiming: 'mark' in (window.performance || {}),
resourceTiming: 'getEntriesByType' in (window.performance || {}),
paintTiming: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('paint'),
layoutInstability: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('layout-shift'),
longTask: 'PerformanceObserver' in window && PerformanceObserver.supportedEntryTypes?.includes('longtask')
};
Logger.info('[PerformanceManager] Initialized with support:', this.support);
// Auto-start core metrics collection
this.startCoreMetrics();
}
/**
* User Timing API - Marks and Measures
*/
timing = {
// Create performance mark
mark: (name, options = {}) => {
if (!this.support.userTiming) {
Logger.warn('[PerformanceManager] User Timing not supported');
return null;
}
try {
performance.mark(name, options);
this.marks.set(name, {
name,
timestamp: performance.now(),
options
});
Logger.info(`[PerformanceManager] Mark created: ${name}`);
return this.marks.get(name);
} catch (error) {
Logger.error('[PerformanceManager] Mark creation failed:', error);
return null;
}
},
// Create performance measure
measure: (name, startMark, endMark, options = {}) => {
if (!this.support.userTiming) {
Logger.warn('[PerformanceManager] User Timing not supported');
return null;
}
try {
performance.measure(name, startMark, endMark, options);
const entry = performance.getEntriesByName(name, 'measure')[0];
const measure = {
name,
startTime: entry.startTime,
duration: entry.duration,
startMark,
endMark,
options
};
this.measures.set(name, measure);
Logger.info(`[PerformanceManager] Measure created: ${name} (${measure.duration.toFixed(2)}ms)`);
return measure;
} catch (error) {
Logger.error('[PerformanceManager] Measure creation failed:', error);
return null;
}
},
// Clear marks and measures
clear: (name = null) => {
if (!this.support.userTiming) return;
try {
if (name) {
performance.clearMarks(name);
performance.clearMeasures(name);
this.marks.delete(name);
this.measures.delete(name);
} else {
performance.clearMarks();
performance.clearMeasures();
this.marks.clear();
this.measures.clear();
}
Logger.info(`[PerformanceManager] Cleared: ${name || 'all'}`);
} catch (error) {
Logger.error('[PerformanceManager] Clear failed:', error);
}
},
// Get all marks
getMarks: () => {
if (!this.support.userTiming) return [];
return Array.from(this.marks.values());
},
// Get all measures
getMeasures: () => {
if (!this.support.userTiming) return [];
return Array.from(this.measures.values());
}
};
/**
* Navigation Timing API
*/
navigation = {
// Get navigation timing data
get: () => {
if (!this.support.navigation) {
return this.getLegacyNavigationTiming();
}
try {
const entry = performance.getEntriesByType('navigation')[0];
if (!entry) return null;
return {
// Navigation phases
redirect: entry.redirectEnd - entry.redirectStart,
dns: entry.domainLookupEnd - entry.domainLookupStart,
connect: entry.connectEnd - entry.connectStart,
ssl: entry.connectEnd - entry.secureConnectionStart,
ttfb: entry.responseStart - entry.requestStart, // Time to First Byte
download: entry.responseEnd - entry.responseStart,
domProcessing: entry.domContentLoadedEventStart - entry.responseEnd,
domComplete: entry.domComplete - entry.domContentLoadedEventStart,
loadComplete: entry.loadEventEnd - entry.loadEventStart,
// Total times
totalTime: entry.loadEventEnd - entry.startTime,
// Enhanced metrics
navigationStart: entry.startTime,
unloadTime: entry.unloadEventEnd - entry.unloadEventStart,
redirectCount: entry.redirectCount,
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
decodedBodySize: entry.decodedBodySize,
// Connection info
connectionInfo: {
nextHopProtocol: entry.nextHopProtocol,
renderBlockingStatus: entry.renderBlockingStatus
}
};
} catch (error) {
Logger.error('[PerformanceManager] Navigation timing failed:', error);
return null;
}
},
// Get performance insights
getInsights: () => {
const timing = this.navigation.get();
if (!timing) return null;
return {
insights: {
serverResponseTime: this.getInsight('ttfb', timing.ttfb, 200, 500),
domProcessing: this.getInsight('domProcessing', timing.domProcessing, 500, 1000),
totalLoadTime: this.getInsight('totalTime', timing.totalTime, 2000, 4000),
transferEfficiency: this.getTransferEfficiency(timing)
},
recommendations: this.getNavigationRecommendations(timing)
};
}
};
/**
* Resource Timing API
*/
resources = {
// Get resource timing data
get: (type = null) => {
if (!this.support.resourceTiming) {
Logger.warn('[PerformanceManager] Resource Timing not supported');
return [];
}
try {
const entries = performance.getEntriesByType('resource');
const resources = entries.map(entry => ({
name: entry.name,
type: this.getResourceType(entry),
startTime: entry.startTime,
duration: entry.duration,
size: {
transfer: entry.transferSize,
encoded: entry.encodedBodySize,
decoded: entry.decodedBodySize
},
timing: {
redirect: entry.redirectEnd - entry.redirectStart,
dns: entry.domainLookupEnd - entry.domainLookupStart,
connect: entry.connectEnd - entry.connectStart,
ssl: entry.connectEnd - entry.secureConnectionStart,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart
},
protocol: entry.nextHopProtocol,
cached: entry.transferSize === 0 && entry.decodedBodySize > 0
}));
return type ? resources.filter(r => r.type === type) : resources;
} catch (error) {
Logger.error('[PerformanceManager] Resource timing failed:', error);
return [];
}
},
// Get resource performance summary
getSummary: () => {
const resources = this.resources.get();
const summary = {
total: resources.length,
types: {},
totalSize: 0,
totalDuration: 0,
cached: 0,
slowResources: []
};
resources.forEach(resource => {
const type = resource.type;
if (!summary.types[type]) {
summary.types[type] = { count: 0, size: 0, duration: 0 };
}
summary.types[type].count++;
summary.types[type].size += resource.size.transfer;
summary.types[type].duration += resource.duration;
summary.totalSize += resource.size.transfer;
summary.totalDuration += resource.duration;
if (resource.cached) summary.cached++;
if (resource.duration > 1000) summary.slowResources.push(resource);
});
return summary;
}
};
/**
* Core Web Vitals monitoring
*/
vitals = {
// Start monitoring Core Web Vitals
start: (callback = null) => {
const vitalsData = {};
// First Contentful Paint
this.observePaint((entries) => {
entries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
vitalsData.fcp = entry.startTime;
this.checkThreshold('fcp', entry.startTime);
if (callback) callback('fcp', entry.startTime);
}
});
});
// Largest Contentful Paint
this.observeLCP((entries) => {
entries.forEach(entry => {
vitalsData.lcp = entry.startTime;
this.checkThreshold('lcp', entry.startTime);
if (callback) callback('lcp', entry.startTime);
});
});
// First Input Delay
this.observeFID((entries) => {
entries.forEach(entry => {
vitalsData.fid = entry.processingStart - entry.startTime;
this.checkThreshold('fid', vitalsData.fid);
if (callback) callback('fid', vitalsData.fid);
});
});
// Cumulative Layout Shift
this.observeCLS((entries) => {
let cls = 0;
entries.forEach(entry => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
vitalsData.cls = cls;
this.checkThreshold('cls', cls);
if (callback) callback('cls', cls);
});
return vitalsData;
},
// Get current vitals
get: () => {
return {
fcp: this.getMetric('fcp'),
lcp: this.getMetric('lcp'),
fid: this.getMetric('fid'),
cls: this.getMetric('cls'),
ratings: {
fcp: this.getRating('fcp', this.getMetric('fcp')),
lcp: this.getRating('lcp', this.getMetric('lcp')),
fid: this.getRating('fid', this.getMetric('fid')),
cls: this.getRating('cls', this.getMetric('cls'))
}
};
}
};
/**
* Memory monitoring
*/
memory = {
// Get memory usage
get: () => {
if (!this.support.memory) {
Logger.warn('[PerformanceManager] Memory API not supported');
return null;
}
try {
const memory = performance.memory;
return {
used: memory.usedJSHeapSize,
total: memory.totalJSHeapSize,
limit: memory.jsHeapSizeLimit,
percentage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100,
formatted: {
used: this.formatBytes(memory.usedJSHeapSize),
total: this.formatBytes(memory.totalJSHeapSize),
limit: this.formatBytes(memory.jsHeapSizeLimit)
}
};
} catch (error) {
Logger.error('[PerformanceManager] Memory get failed:', error);
return null;
}
},
// Monitor memory usage
monitor: (callback, interval = 5000) => {
if (!this.support.memory) return null;
const intervalId = setInterval(() => {
const memoryData = this.memory.get();
if (memoryData) {
callback(memoryData);
// Warn if memory usage is high
if (memoryData.percentage > 80) {
Logger.warn('[PerformanceManager] High memory usage detected:', memoryData.percentage.toFixed(1) + '%');
}
}
}, interval);
return {
stop: () => clearInterval(intervalId)
};
}
};
/**
* Long Task monitoring
*/
longTasks = {
// Start monitoring long tasks
start: (callback = null) => {
if (!this.support.longTask) {
Logger.warn('[PerformanceManager] Long Task API not supported');
return null;
}
try {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
const taskInfo = {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name,
attribution: entry.attribution || []
};
Logger.warn('[PerformanceManager] Long task detected:', taskInfo);
if (callback) callback(taskInfo);
});
});
observer.observe({ entryTypes: ['longtask'] });
const observerId = this.generateId('longtask');
this.observers.set(observerId, observer);
return {
id: observerId,
stop: () => {
observer.disconnect();
this.observers.delete(observerId);
}
};
} catch (error) {
Logger.error('[PerformanceManager] Long task monitoring failed:', error);
return null;
}
}
};
/**
* Performance optimization utilities
*/
optimize = {
// Defer non-critical JavaScript
deferScript: (src, callback = null) => {
const script = document.createElement('script');
script.src = src;
script.defer = true;
if (callback) script.onload = callback;
document.head.appendChild(script);
return script;
},
// Preload critical resources
preload: (href, as, crossorigin = false) => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = href;
link.as = as;
if (crossorigin) link.crossOrigin = 'anonymous';
document.head.appendChild(link);
return link;
},
// Prefetch future resources
prefetch: (href) => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = href;
document.head.appendChild(link);
return link;
},
// Optimize images with lazy loading
lazyImages: (selector = 'img[data-src]') => {
if ('IntersectionObserver' in window) {
const images = document.querySelectorAll(selector);
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
return imageObserver;
} else {
// Fallback for older browsers
const images = document.querySelectorAll(selector);
images.forEach(img => {
img.src = img.dataset.src;
img.classList.remove('lazy');
});
}
},
// Bundle size analyzer
analyzeBundles: () => {
const scripts = Array.from(document.querySelectorAll('script[src]'));
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
const analysis = {
scripts: scripts.map(script => ({
src: script.src,
async: script.async,
defer: script.defer
})),
styles: styles.map(style => ({
href: style.href,
media: style.media
})),
recommendations: []
};
// Add recommendations
if (scripts.length > 10) {
analysis.recommendations.push('Consider bundling JavaScript files');
}
if (styles.length > 5) {
analysis.recommendations.push('Consider bundling CSS files');
}
return analysis;
}
};
// Helper methods
startCoreMetrics() {
// Auto-start vitals monitoring
this.vitals.start((metric, value) => {
this.setMetric(metric, value);
});
// Start memory monitoring if supported
if (this.support.memory) {
this.memory.monitor((memoryData) => {
this.setMetric('memory', memoryData);
});
}
}
observePaint(callback) {
if (!this.support.paintTiming) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['paint'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] Paint observer failed:', error);
}
}
observeLCP(callback) {
if (!this.support.observer) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] LCP observer failed:', error);
}
}
observeFID(callback) {
if (!this.support.observer) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['first-input'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] FID observer failed:', error);
}
}
observeCLS(callback) {
if (!this.support.layoutInstability) return;
try {
const observer = new PerformanceObserver((list) => {
callback(list.getEntries());
});
observer.observe({ entryTypes: ['layout-shift'] });
return observer;
} catch (error) {
Logger.error('[PerformanceManager] CLS observer failed:', error);
}
}
getLegacyNavigationTiming() {
if (!this.support.timing) return null;
const timing = performance.timing;
const navigationStart = timing.navigationStart;
return {
redirect: timing.redirectEnd - timing.redirectStart,
dns: timing.domainLookupEnd - timing.domainLookupStart,
connect: timing.connectEnd - timing.connectStart,
ssl: timing.connectEnd - timing.secureConnectionStart,
ttfb: timing.responseStart - timing.requestStart,
download: timing.responseEnd - timing.responseStart,
domProcessing: timing.domContentLoadedEventStart - timing.responseEnd,
domComplete: timing.domComplete - timing.domContentLoadedEventStart,
loadComplete: timing.loadEventEnd - timing.loadEventStart,
totalTime: timing.loadEventEnd - navigationStart
};
}
getResourceType(entry) {
const url = new URL(entry.name);
const extension = url.pathname.split('.').pop().toLowerCase();
const typeMap = {
js: 'script',
css: 'stylesheet',
png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', svg: 'image', webp: 'image',
woff: 'font', woff2: 'font', ttf: 'font', eot: 'font',
json: 'fetch', xml: 'fetch'
};
return typeMap[extension] || entry.initiatorType || 'other';
}
getInsight(metric, value, good, needs) {
if (value < good) return { rating: 'good', message: `Excellent ${metric}` };
if (value < needs) return { rating: 'needs-improvement', message: `${metric} needs improvement` };
return { rating: 'poor', message: `Poor ${metric} performance` };
}
getTransferEfficiency(timing) {
const compressionRatio = timing.decodedBodySize > 0 ?
timing.encodedBodySize / timing.decodedBodySize : 1;
return {
ratio: compressionRatio,
rating: compressionRatio < 0.7 ? 'good' : compressionRatio < 0.9 ? 'fair' : 'poor'
};
}
getNavigationRecommendations(timing) {
const recommendations = [];
if (timing.ttfb > 500) {
recommendations.push('Server response time is slow. Consider optimizing backend performance.');
}
if (timing.dns > 100) {
recommendations.push('DNS lookup time is high. Consider using a faster DNS provider.');
}
if (timing.connect > 1000) {
recommendations.push('Connection time is slow. Check network latency.');
}
if (timing.domProcessing > 1000) {
recommendations.push('DOM processing is slow. Consider optimizing JavaScript execution.');
}
return recommendations;
}
checkThreshold(metric, value) {
const threshold = this.thresholds[metric];
if (threshold && value > threshold) {
Logger.warn(`[PerformanceManager] ${metric.toUpperCase()} threshold exceeded: ${value}ms (threshold: ${threshold}ms)`);
}
}
getRating(metric, value) {
const thresholds = {
fcp: { good: 1800, poor: 3000 },
lcp: { good: 2500, poor: 4000 },
fid: { good: 100, poor: 300 },
cls: { good: 0.1, poor: 0.25 }
};
const threshold = thresholds[metric];
if (!threshold || value === null || value === undefined) return 'unknown';
if (value <= threshold.good) return 'good';
if (value <= threshold.poor) return 'needs-improvement';
return 'poor';
}
setMetric(key, value) {
this.metrics.set(key, {
value,
timestamp: Date.now()
});
}
getMetric(key) {
const metric = this.metrics.get(key);
return metric ? metric.value : null;
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
generateId(prefix = 'perf') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Stop all observers and clear data
*/
cleanup() {
this.observers.forEach(observer => {
observer.disconnect();
});
this.observers.clear();
this.metrics.clear();
this.marks.clear();
this.measures.clear();
Logger.info('[PerformanceManager] Cleanup completed');
}
/**
* Get comprehensive performance report
*/
getReport() {
return {
support: this.support,
navigation: this.navigation.get(),
resources: this.resources.getSummary(),
vitals: this.vitals.get(),
memory: this.memory.get(),
marks: this.timing.getMarks(),
measures: this.timing.getMeasures(),
activeObservers: this.observers.size,
timestamp: Date.now()
};
}
}