ObsiViewer/src/app/services/performance-profiler.service.ts

169 lines
4.7 KiB
TypeScript

import { Injectable } from '@angular/core';
interface PerformanceSample {
duration: number;
success: boolean;
timestamp: number;
}
interface BottleneckAnalysis {
slowOperations: Array<{
operation: string;
avgDuration: number;
failureRate: number;
}>;
frequentOperations: Array<{
operation: string;
callCount: number;
}>;
}
@Injectable({ providedIn: 'root' })
export class PerformanceProfilerService {
private metrics = new Map<string, PerformanceSample[]>();
private readonly maxSamples = 100;
// Mesurer une opération asynchrone
async measureAsync<T>(
operationName: string,
operation: () => Promise<T>
): Promise<T> {
const start = performance.now();
try {
const result = await operation();
const duration = performance.now() - start;
this.recordSample(operationName, duration, true);
return result;
} catch (error) {
const duration = performance.now() - start;
this.recordSample(operationName, duration, false);
throw error;
}
}
// Mesurer une opération synchrone
measureSync<T>(
operationName: string,
operation: () => T
): T {
const start = performance.now();
try {
const result = operation();
const duration = performance.now() - start;
this.recordSample(operationName, duration, true);
return result;
} catch (error) {
const duration = performance.now() - start;
this.recordSample(operationName, duration, false);
throw error;
}
}
// Analyser les goulots d'étranglement
analyzeBottlenecks(): BottleneckAnalysis {
const analysis: BottleneckAnalysis = {
slowOperations: [],
frequentOperations: []
};
for (const [operation, samples] of this.metrics.entries()) {
const avgDuration = samples.reduce((sum, s) => sum + s.duration, 0) / samples.length;
const failureRate = samples.filter(s => !s.success).length / samples.length;
// Opérations lentes (> 100ms)
if (avgDuration > 100) {
analysis.slowOperations.push({
operation,
avgDuration: Math.round(avgDuration * 100) / 100,
failureRate: Math.round(failureRate * 10000) / 100
});
}
// Opérations fréquentes (> 50 appels)
if (samples.length > 50) {
analysis.frequentOperations.push({
operation,
callCount: samples.length
});
}
}
return analysis;
}
// Obtenir les métriques brutes
getMetrics() {
const result: Record<string, any> = {};
for (const [operation, samples] of this.metrics.entries()) {
const durations = samples.map(s => s.duration);
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
const minDuration = Math.min(...durations);
const maxDuration = Math.max(...durations);
const p95Duration = this.percentile(durations, 95);
result[operation] = {
sampleCount: samples.length,
avgDuration: Math.round(avgDuration * 100) / 100,
minDuration: Math.round(minDuration * 100) / 100,
maxDuration: Math.round(maxDuration * 100) / 100,
p95Duration: Math.round(p95Duration * 100) / 100,
failureRate: samples.filter(s => !s.success).length / samples.length
};
}
return result;
}
// Exporter les métriques pour analyse
exportMetrics() {
return {
timestamp: new Date().toISOString(),
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
metrics: this.getMetrics(),
bottlenecks: this.analyzeBottlenecks(),
memory: typeof performance !== 'undefined' && (performance as any).memory ? {
used: (performance as any).memory.usedJSHeapSize,
total: (performance as any).memory.totalJSHeapSize,
limit: (performance as any).memory.jsHeapSizeLimit
} : null
};
}
private recordSample(operation: string, duration: number, success: boolean) {
if (!this.metrics.has(operation)) {
this.metrics.set(operation, []);
}
const samples = this.metrics.get(operation)!;
samples.push({
duration,
success,
timestamp: Date.now()
});
// Garder seulement les derniers échantillons
if (samples.length > this.maxSamples) {
samples.shift();
}
}
private percentile(values: number[], p: number): number {
const sorted = [...values].sort((a, b) => a - b);
const index = (p / 100) * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
if (lower === upper) {
return sorted[lower];
}
return sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower);
}
// Réinitialiser les métriques
reset() {
this.metrics.clear();
}
}