169 lines
4.7 KiB
TypeScript
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();
|
|
}
|
|
}
|