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
This commit is contained in:
756
resources/js/modules/api-manager/PerformanceManager.js
Normal file
756
resources/js/modules/api-manager/PerformanceManager.js
Normal file
@@ -0,0 +1,756 @@
|
||||
// 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user