- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
756 lines
27 KiB
JavaScript
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()
|
|
};
|
|
}
|
|
} |