feat: enhanced fe performance monitor

This commit is contained in:
amhsirak
2025-03-04 22:36:42 +05:30
parent b17397d0c8
commit 26688b4b45

View File

@@ -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: {