interface TimingEntry {
operation: string;
startTime: number;
endTime?: number;
duration?: number;
success: boolean;
cached: boolean;
error?: string | null;
}
interface PerformanceMetrics {
totalRequests: number;
averageResponseTime: number;
cacheHitRate: number;
errorRate: number;
slowRequests: number; // > 1 second
operationStats: Record<string, {
count: number;
totalTime: number;
averageTime: number;
errorCount: number;
cacheHits: number;
slowCount: number;
}>;
}
export class PerformanceMonitor {
private timings: Map<string, TimingEntry> = new Map();
private completedTimings: TimingEntry[] = [];
private maxHistorySize: number;
private slowRequestThreshold: number;
constructor(maxHistorySize: number = 1000, slowRequestThreshold: number = 1000) {
this.maxHistorySize = maxHistorySize;
this.slowRequestThreshold = slowRequestThreshold;
}
/**
* Start timing an operation
*/
startTiming(operation: string, requestId?: string): string {
const id = requestId || this.generateRequestId();
const timing: TimingEntry = {
operation,
startTime: Date.now(),
success: false,
cached: false,
error: null,
};
this.timings.set(id, timing);
return id;
}
/**
* End timing for an operation
*/
endTiming(
requestId: string,
success: boolean = true,
cached: boolean = false,
error?: string
): number | null {
const timing = this.timings.get(requestId);
if (!timing) {
console.warn(`No timing found for request ID: ${requestId}`);
return null;
}
timing.endTime = Date.now();
timing.duration = timing.endTime - timing.startTime;
timing.success = success;
timing.cached = cached;
timing.error = error || null;
// Move to completed timings
this.completedTimings.push(timing);
this.timings.delete(requestId);
// Maintain history size
if (this.completedTimings.length > this.maxHistorySize) {
this.completedTimings.shift();
}
// Log slow requests
if (timing.duration > this.slowRequestThreshold) {
console.warn(
`Slow request detected: ${timing.operation} took ${timing.duration}ms`,
{ requestId, operation: timing.operation, duration: timing.duration, cached }
);
}
return timing.duration;
}
/**
* Time an async operation
*/
async timeOperation<T>(
operation: string,
fn: () => Promise<T>,
cached: boolean = false
): Promise<T> {
const requestId = this.startTiming(operation);
try {
const result = await fn();
this.endTiming(requestId, true, cached);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
this.endTiming(requestId, false, cached, errorMessage);
throw error;
}
}
/**
* Get current performance metrics
*/
getMetrics(): PerformanceMetrics {
const totalRequests = this.completedTimings.length;
if (totalRequests === 0) {
return this.getEmptyMetrics();
}
// Calculate overall metrics
const totalTime = this.completedTimings.reduce((sum, timing) => sum + (timing.duration || 0), 0);
const successfulRequests = this.completedTimings.filter(t => t.success).length;
const cachedRequests = this.completedTimings.filter(t => t.cached).length;
const slowRequests = this.completedTimings.filter(t => (t.duration || 0) > this.slowRequestThreshold).length;
// Calculate per-operation stats
const operationStats: Record<string, any> = {};
this.completedTimings.forEach(timing => {
const { operation, duration = 0, success, cached } = timing;
if (!operationStats[operation]) {
operationStats[operation] = {
count: 0,
totalTime: 0,
averageTime: 0,
errorCount: 0,
cacheHits: 0,
slowCount: 0,
};
}
const stats = operationStats[operation];
stats.count++;
stats.totalTime += duration;
stats.averageTime = stats.totalTime / stats.count;
if (!success) stats.errorCount++;
if (cached) stats.cacheHits++;
if (duration > this.slowRequestThreshold) stats.slowCount++;
});
return {
totalRequests,
averageResponseTime: totalTime / totalRequests,
cacheHitRate: (cachedRequests / totalRequests) * 100,
errorRate: ((totalRequests - successfulRequests) / totalRequests) * 100,
slowRequests,
operationStats,
};
}
/**
* Get detailed timing history
*/
getTimingHistory(operation?: string, limit?: number): TimingEntry[] {
let history = this.completedTimings;
if (operation) {
history = history.filter(timing => timing.operation === operation);
}
if (limit) {
history = history.slice(-limit);
}
return history.map(timing => ({ ...timing })); // Return copies
}
/**
* Get real-time performance summary
*/
getPerformanceSummary() {
const metrics = this.getMetrics();
const recentTimings = this.getTimingHistory(undefined, 10);
return {
overview: {
totalRequests: metrics.totalRequests,
averageResponseTime: Math.round(metrics.averageResponseTime),
cacheHitRate: Math.round(metrics.cacheHitRate * 100) / 100,
errorRate: Math.round(metrics.errorRate * 100) / 100,
slowRequests: metrics.slowRequests,
},
topOperations: Object.entries(metrics.operationStats)
.sort(([,a], [,b]) => b.count - a.count)
.slice(0, 5)
.map(([operation, stats]) => ({
operation,
count: stats.count,
averageTime: Math.round(stats.averageTime),
cacheHitRate: Math.round((stats.cacheHits / stats.count) * 100),
errorRate: Math.round((stats.errorCount / stats.count) * 100),
})),
recentActivity: recentTimings.map(timing => ({
operation: timing.operation,
duration: timing.duration,
success: timing.success,
cached: timing.cached,
timestamp: timing.startTime,
})),
alerts: this.generateAlerts(metrics),
};
}
/**
* Reset all metrics
*/
reset(): void {
this.timings.clear();
this.completedTimings = [];
}
/**
* Get active timings (operations in progress)
*/
getActiveTimings(): Array<{ requestId: string; operation: string; elapsed: number }> {
const now = Date.now();
return Array.from(this.timings.entries()).map(([requestId, timing]) => ({
requestId,
operation: timing.operation,
elapsed: now - timing.startTime,
}));
}
/**
* Generate performance alerts
*/
private generateAlerts(metrics: PerformanceMetrics): string[] {
const alerts: string[] = [];
if (metrics.averageResponseTime > 2000) {
alerts.push('High average response time detected');
}
if (metrics.errorRate > 5) {
alerts.push('High error rate detected');
}
if (metrics.cacheHitRate < 30 && metrics.totalRequests > 10) {
alerts.push('Low cache hit rate - consider cache optimization');
}
if (metrics.slowRequests > metrics.totalRequests * 0.1) {
alerts.push('High number of slow requests');
}
// Check for operations with consistently high response times
Object.entries(metrics.operationStats).forEach(([operation, stats]) => {
if (stats.averageTime > 3000 && stats.count > 5) {
alerts.push(`Operation "${operation}" has consistently high response times`);
}
});
return alerts;
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private getEmptyMetrics(): PerformanceMetrics {
return {
totalRequests: 0,
averageResponseTime: 0,
cacheHitRate: 0,
errorRate: 0,
slowRequests: 0,
operationStats: {},
};
}
}