/**
* Performance metrics tracking for Local Explorer MCP
*
* Provides performance monitoring, metrics collection, and reporting.
* Tracks operation durations, success rates, and percentiles (P50/P95/P99).
*
* @module performanceMetrics
*/
/**
* Metrics for a single operation
*/
export interface OperationMetrics {
/** Tool name */
toolName: string;
/** Operation name (e.g., 'search', 'fetch', 'list') */
operation: string;
/** Start timestamp */
startTime: number;
/** End timestamp */
endTime: number;
/** Duration in milliseconds */
durationMs: number;
/** Whether the operation succeeded */
success: boolean;
/** Error message if failed */
error?: string;
/** Additional metadata */
metadata?: Record<string, unknown>;
}
/**
* Aggregated metrics for a tool/operation combination
*/
export interface AggregatedMetrics {
/** Tool name */
toolName: string;
/** Operation name */
operation: string;
/** Total number of operations */
count: number;
/** Total duration across all operations */
totalDurationMs: number;
/** Average duration */
averageDurationMs: number;
/** Minimum duration */
minDurationMs: number;
/** Maximum duration */
maxDurationMs: number;
/** Success rate (0-1) */
successRate: number;
/** 50th percentile (median) */
p50: number;
/** 95th percentile */
p95: number;
/** 99th percentile */
p99: number;
}
/**
* Performance metrics collector
* Tracks operation performance with minimal overhead
*/
class PerformanceMetricsCollector {
private metrics: OperationMetrics[] = [];
private readonly maxMetrics = 1000;
private activeOperations: Map<string, {
toolName: string;
operation: string;
startTime: number;
metadata?: Record<string, unknown>;
}> = new Map();
/**
* Start tracking an operation
*
* @param toolName - Name of the tool
* @param operation - Name of the operation
* @param metadata - Optional metadata to track
* @returns Unique operation ID
*/
startOperation(
toolName: string,
operation: string,
metadata?: Record<string, unknown>
): string {
const id = `${toolName}_${operation}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
this.activeOperations.set(id, {
toolName,
operation,
startTime: Date.now(),
metadata
});
return id;
}
/**
* End tracking an operation
*
* @param id - Operation ID from startOperation
* @param success - Whether the operation succeeded
* @param error - Error message if failed
*/
endOperation(id: string, success: boolean, error?: string): void {
const active = this.activeOperations.get(id);
if (!active) {
console.warn(`[PerformanceMetrics] Unknown operation ID: ${id}`);
return;
}
const endTime = Date.now();
const completedMetric: OperationMetrics = {
toolName: active.toolName,
operation: active.operation,
startTime: active.startTime,
endTime,
durationMs: endTime - active.startTime,
success,
error,
metadata: active.metadata
};
this.metrics.push(completedMetric);
this.activeOperations.delete(id);
// Trim if exceeds max
if (this.metrics.length > this.maxMetrics) {
this.metrics.shift();
}
// Warn on slow operations (>5s)
if (completedMetric.durationMs > 5000) {
console.warn(
`[Performance] Slow operation: ${completedMetric.toolName}.${completedMetric.operation} ` +
`took ${completedMetric.durationMs}ms`
);
}
}
/**
* Get aggregated metrics for specific tool or all tools
*
* @param toolName - Optional tool name to filter by
* @returns Array of aggregated metrics
*/
getAggregatedMetrics(toolName?: string): AggregatedMetrics[] {
// Group by tool + operation
const grouped = new Map<string, OperationMetrics[]>();
this.metrics
.filter(m => !toolName || m.toolName === toolName)
.forEach(m => {
const key = `${m.toolName}::${m.operation}`;
if (!grouped.has(key)) {
grouped.set(key, []);
}
grouped.get(key)!.push(m);
});
// Calculate statistics
const aggregated: AggregatedMetrics[] = [];
for (const [key, metrics] of grouped) {
const [tool, op] = key.split('::');
const durations = metrics.map(m => m.durationMs).sort((a, b) => a - b);
const successCount = metrics.filter(m => m.success).length;
aggregated.push({
toolName: tool,
operation: op,
count: metrics.length,
totalDurationMs: durations.reduce((a, b) => a + b, 0),
averageDurationMs: durations.reduce((a, b) => a + b, 0) / metrics.length,
minDurationMs: durations[0],
maxDurationMs: durations[durations.length - 1],
successRate: successCount / metrics.length,
p50: this.percentile(durations, 0.5),
p95: this.percentile(durations, 0.95),
p99: this.percentile(durations, 0.99)
});
}
// Sort by total duration (slowest first)
return aggregated.sort((a, b) => b.totalDurationMs - a.totalDurationMs);
}
/**
* Calculate percentile from sorted array
*/
private percentile(sorted: number[], p: number): number {
if (sorted.length === 0) return 0;
const index = Math.ceil(sorted.length * p) - 1;
return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
}
/**
* Get summary report of all metrics
*
* @returns Formatted performance report
*/
getSummaryReport(): string {
const aggregated = this.getAggregatedMetrics();
const lines = [
'Performance Metrics Summary',
'='.repeat(60),
''
];
if (aggregated.length === 0) {
lines.push('No metrics collected yet.');
return lines.join('\n');
}
for (const metric of aggregated) {
lines.push(
`${metric.toolName}.${metric.operation}:`,
` Count: ${metric.count} operations`,
` Average: ${Math.round(metric.averageDurationMs)}ms`,
` P50: ${Math.round(metric.p50)}ms`,
` P95: ${Math.round(metric.p95)}ms`,
` P99: ${Math.round(metric.p99)}ms`,
` Min/Max: ${Math.round(metric.minDurationMs)}ms / ${Math.round(metric.maxDurationMs)}ms`,
` Success Rate: ${(metric.successRate * 100).toFixed(1)}%`,
''
);
}
// Add summary statistics
const totalOps = aggregated.reduce((sum, m) => sum + m.count, 0);
const avgDuration = aggregated.reduce((sum, m) => sum + m.averageDurationMs * m.count, 0) / totalOps;
const overallSuccessRate = aggregated.reduce((sum, m) => sum + m.successRate * m.count, 0) / totalOps;
lines.push(
'Overall Statistics:',
` Total Operations: ${totalOps}`,
` Average Duration: ${Math.round(avgDuration)}ms`,
` Overall Success Rate: ${(overallSuccessRate * 100).toFixed(1)}%`,
` Active Operations: ${this.activeOperations.size}`,
` Stored Metrics: ${this.metrics.length} (max: ${this.maxMetrics})`
);
return lines.join('\n');
}
/**
* Get recent failed operations
*
* @param limit - Maximum number of failures to return
* @returns Recent failed operations
*/
getRecentFailures(limit = 10): OperationMetrics[] {
return this.metrics
.filter(m => !m.success)
.slice(-limit)
.reverse();
}
/**
* Get slowest operations
*
* @param limit - Maximum number of operations to return
* @returns Slowest operations
*/
getSlowestOperations(limit = 10): OperationMetrics[] {
return [...this.metrics]
.sort((a, b) => b.durationMs - a.durationMs)
.slice(0, limit);
}
/**
* Clear all collected metrics
*/
clear(): void {
this.metrics = [];
this.activeOperations.clear();
}
}
/**
* Global performance metrics collector instance
*/
export const performanceMetrics = new PerformanceMetricsCollector();
/**
* Wrapper function to track performance of async operations
*
* @param toolName - Name of the tool
* @param operation - Name of the operation
* @param fn - Async function to execute
* @param metadata - Optional metadata to track
* @returns Result of the function
*
* @example
* ```typescript
* const result = await trackPerformance(
* 'local_ripgrep',
* 'search',
* async () => {
* return await performSearch(query);
* },
* { pattern: query.pattern, filesOnly: query.filesOnly }
* );
* ```
*/
export async function trackPerformance<T>(
toolName: string,
operation: string,
fn: () => Promise<T>,
metadata?: Record<string, unknown>
): Promise<T> {
const id = performanceMetrics.startOperation(toolName, operation, metadata);
try {
const result = await fn();
performanceMetrics.endOperation(id, true);
return result;
} catch (error) {
performanceMetrics.endOperation(
id,
false,
error instanceof Error ? error.message : String(error)
);
throw error;
}
}