feat: enhanced fe performance monitor
This commit is contained in:
@@ -93,6 +93,262 @@ export class FrontendPerformanceMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Performance Monitor with Memory Management
|
||||
export class EnhancedPerformanceMonitor extends FrontendPerformanceMonitor {
|
||||
private memoryWarningThreshold: number = 100000000; // 100MB
|
||||
private memoryAlertCallback?: (usage: MemoryInfo) => void;
|
||||
private frameTimeHistory: number[] = [];
|
||||
private maxMetricsHistory: number = 100; // Keep only the last 100 readings
|
||||
private isThrottled: boolean = false;
|
||||
private rafHandle: number | null = null;
|
||||
private memoryCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(options?: {
|
||||
memoryWarningThreshold?: number,
|
||||
maxMetricsHistory?: number,
|
||||
memoryAlertCallback?: (usage: MemoryInfo) => void
|
||||
}) {
|
||||
super();
|
||||
|
||||
if (options) {
|
||||
if (options.memoryWarningThreshold) {
|
||||
this.memoryWarningThreshold = options.memoryWarningThreshold;
|
||||
}
|
||||
|
||||
if (options.maxMetricsHistory) {
|
||||
this.maxMetricsHistory = options.maxMetricsHistory;
|
||||
}
|
||||
|
||||
if (options.memoryAlertCallback) {
|
||||
this.memoryAlertCallback = options.memoryAlertCallback;
|
||||
}
|
||||
}
|
||||
|
||||
// Override the parent's monitoring with our enhanced version
|
||||
this.startEnhancedMonitoring();
|
||||
}
|
||||
|
||||
private startEnhancedMonitoring(): void {
|
||||
// Stop any existing monitoring from parent class
|
||||
if (this.rafHandle) {
|
||||
cancelAnimationFrame(this.rafHandle);
|
||||
}
|
||||
|
||||
if (this.memoryCheckInterval) {
|
||||
clearInterval(this.memoryCheckInterval);
|
||||
}
|
||||
|
||||
// Enhanced FPS monitoring with frame time tracking
|
||||
let lastFrameTime = performance.now();
|
||||
let frameCount = 0;
|
||||
let frameTimes: number[] = [];
|
||||
|
||||
const measureFPS = () => {
|
||||
const now = performance.now();
|
||||
const frameTime = now - lastFrameTime;
|
||||
lastFrameTime = now;
|
||||
|
||||
// Track individual frame times for jank detection
|
||||
frameTimes.push(frameTime);
|
||||
if (frameTimes.length > 60) { // Keep only last 60 frame times
|
||||
frameTimes.shift();
|
||||
}
|
||||
|
||||
frameCount++;
|
||||
this.frameTimeHistory.push(frameTime);
|
||||
|
||||
// Calculate FPS every second
|
||||
if (this.frameTimeHistory.length >= 60) {
|
||||
const totalTime = this.frameTimeHistory.reduce((sum, time) => sum + time, 0);
|
||||
const fps = Math.round((this.frameTimeHistory.length * 1000) / totalTime);
|
||||
|
||||
// Get metrics from parent class
|
||||
const metrics = this.getMetrics();
|
||||
metrics.fps.push(fps);
|
||||
|
||||
// Limit metrics history
|
||||
if (metrics.fps.length > this.maxMetricsHistory) {
|
||||
metrics.fps.shift();
|
||||
}
|
||||
|
||||
// Detect jank (long frames)
|
||||
const jankThreshold = 16.7 * 2; // 2x normal frame time at 60fps
|
||||
const jankFrames = frameTimes.filter(time => time > jankThreshold);
|
||||
|
||||
if (jankFrames.length > 10) { // If more than 10 out of 60 frames are janky
|
||||
this.detectPerformanceIssue('jank', {
|
||||
jankFrames: jankFrames.length,
|
||||
averageJankTime: jankFrames.reduce((sum, time) => sum + time, 0) / jankFrames.length
|
||||
});
|
||||
}
|
||||
|
||||
// Reset for next measurement
|
||||
this.frameTimeHistory = [];
|
||||
frameTimes = [];
|
||||
}
|
||||
|
||||
this.rafHandle = requestAnimationFrame(measureFPS);
|
||||
};
|
||||
|
||||
this.rafHandle = requestAnimationFrame(measureFPS);
|
||||
|
||||
// Enhanced memory monitoring
|
||||
if (window.performance && (performance as any).memory) {
|
||||
this.memoryCheckInterval = setInterval(() => {
|
||||
const memory = (performance as any).memory;
|
||||
const memoryInfo = {
|
||||
usedJSHeapSize: memory.usedJSHeapSize,
|
||||
totalJSHeapSize: memory.totalJSHeapSize,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Get metrics from parent class
|
||||
const metrics = this.getMetrics();
|
||||
metrics.memoryUsage.push(memoryInfo);
|
||||
|
||||
// Limit metrics history
|
||||
if (metrics.memoryUsage.length > this.maxMetricsHistory) {
|
||||
metrics.memoryUsage.shift();
|
||||
}
|
||||
|
||||
// Check for memory warnings
|
||||
if (memoryInfo.usedJSHeapSize > this.memoryWarningThreshold) {
|
||||
this.detectPerformanceIssue('memory', memoryInfo);
|
||||
|
||||
if (this.memoryAlertCallback) {
|
||||
this.memoryAlertCallback(memoryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for memory leaks (steady increase)
|
||||
if (metrics.memoryUsage.length >= 10) {
|
||||
const recentMemory = metrics.memoryUsage.slice(-10);
|
||||
let increasingCount = 0;
|
||||
|
||||
for (let i = 1; i < recentMemory.length; i++) {
|
||||
if (recentMemory[i].usedJSHeapSize > recentMemory[i-1].usedJSHeapSize) {
|
||||
increasingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If memory increased in 8 out of 9 consecutive readings
|
||||
if (increasingCount >= 8) {
|
||||
this.detectPerformanceIssue('memoryLeak', {
|
||||
startMemory: recentMemory[0].usedJSHeapSize,
|
||||
currentMemory: recentMemory[recentMemory.length - 1].usedJSHeapSize,
|
||||
increaseRate: (recentMemory[recentMemory.length - 1].usedJSHeapSize - recentMemory[0].usedJSHeapSize) /
|
||||
(recentMemory[recentMemory.length - 1].timestamp - recentMemory[0].timestamp) * 1000 // bytes per second
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Method to detect various performance issues
|
||||
private detectPerformanceIssue(type: 'jank' | 'memory' | 'memoryLeak', data: any): void {
|
||||
console.warn(`Performance issue detected: ${type}`, data);
|
||||
|
||||
if (type === 'memory' || type === 'memoryLeak') {
|
||||
// Auto-throttle rendering if memory issues detected
|
||||
if (!this.isThrottled) {
|
||||
this.throttleRendering();
|
||||
}
|
||||
|
||||
// Suggest garbage collection
|
||||
this.suggestGarbageCollection();
|
||||
}
|
||||
}
|
||||
|
||||
// Get access to the metrics from parent class
|
||||
private getMetrics(): any {
|
||||
return (this as any).metrics;
|
||||
}
|
||||
|
||||
// Throttle rendering to reduce memory pressure
|
||||
private throttleRendering(): void {
|
||||
this.isThrottled = true;
|
||||
console.info('Throttling rendering due to memory pressure');
|
||||
// Application code would implement throttling behavior
|
||||
}
|
||||
|
||||
// Un-throttle rendering when memory pressure is reduced
|
||||
public unthrottleRendering(): void {
|
||||
if (this.isThrottled) {
|
||||
this.isThrottled = false;
|
||||
console.info('Resuming normal rendering');
|
||||
}
|
||||
}
|
||||
|
||||
// Suggest garbage collection to the browser
|
||||
private suggestGarbageCollection(): void {
|
||||
if (window.gc) {
|
||||
try {
|
||||
window.gc();
|
||||
} catch (e) {
|
||||
// gc() might not be available without special flags
|
||||
}
|
||||
}
|
||||
|
||||
// Alternative approach to encourage garbage collection
|
||||
const largeArray = new Array(1000000).fill(0);
|
||||
largeArray.length = 0;
|
||||
}
|
||||
|
||||
// Enhanced performance report with more detailed metrics
|
||||
public getEnhancedPerformanceReport(): EnhancedPerformanceReport {
|
||||
const baseReport = super.getPerformanceReport();
|
||||
const metrics = this.getMetrics();
|
||||
|
||||
// Calculate 95th percentile render time
|
||||
const sortedRenderTimes = [...metrics.renderTime].sort((a, b) => a - b);
|
||||
const idx95 = Math.floor(sortedRenderTimes.length * 0.95);
|
||||
const renderTime95Percentile = sortedRenderTimes[idx95] || 0;
|
||||
|
||||
// Calculate memory growth rate
|
||||
let memoryGrowthRate = 0;
|
||||
if (metrics.memoryUsage.length >= 2) {
|
||||
const first = metrics.memoryUsage[0];
|
||||
const last = metrics.memoryUsage[metrics.memoryUsage.length - 1];
|
||||
const timeDiffInSeconds = (last.timestamp - first.timestamp) / 1000;
|
||||
memoryGrowthRate = timeDiffInSeconds > 0
|
||||
? (last.usedJSHeapSize - first.usedJSHeapSize) / timeDiffInSeconds
|
||||
: 0;
|
||||
}
|
||||
|
||||
return {
|
||||
...baseReport,
|
||||
renderTime95Percentile,
|
||||
memoryGrowthRate,
|
||||
isThrottled: this.isThrottled,
|
||||
heapUsagePercentage: baseReport.lastMemoryUsage
|
||||
? (baseReport.lastMemoryUsage.usedJSHeapSize / baseReport.lastMemoryUsage.totalJSHeapSize) * 100
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Clean up resources when no longer needed
|
||||
public dispose(): void {
|
||||
if (this.rafHandle) {
|
||||
cancelAnimationFrame(this.rafHandle);
|
||||
this.rafHandle = null;
|
||||
}
|
||||
|
||||
if (this.memoryCheckInterval) {
|
||||
clearInterval(this.memoryCheckInterval);
|
||||
this.memoryCheckInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extended types
|
||||
interface EnhancedPerformanceReport extends PerformanceReport {
|
||||
renderTime95Percentile: number;
|
||||
memoryGrowthRate: number; // bytes per second
|
||||
isThrottled: boolean;
|
||||
heapUsagePercentage: number;
|
||||
}
|
||||
|
||||
// Backend Performance Monitoring
|
||||
export class BackendPerformanceMonitor {
|
||||
private metrics: {
|
||||
|
||||
Reference in New Issue
Block a user