import * as os from 'os'; /** * Performance tracking utilities for test suite */ export interface PerformanceMetric { operation: string; duration: number; timestamp: number; memory: { used: number; total: number; external: number; }; cpu?: { user: number; system: number; }; metadata?: Record; } export interface PerformanceStats { count: number; min: number; max: number; avg: number; median: number; p95: number; p99: number; stdDev: number; } export class PerformanceTracker { private static metrics = new Map(); private static thresholds = new Map(); /** * Set performance thresholds for an operation */ static setThreshold(operation: string, target: number, acceptable: number, maximum: number): void { this.thresholds.set(operation, { target, acceptable, maximum }); } /** * Initialize default thresholds based on test/readme.md */ static initializeDefaultThresholds(): void { this.setThreshold('format-detection', 5, 10, 50); this.setThreshold('xml-parsing-1mb', 50, 100, 500); this.setThreshold('validation-syntax', 20, 50, 200); this.setThreshold('validation-business', 100, 200, 1000); this.setThreshold('pdf-extraction', 200, 500, 2000); this.setThreshold('format-conversion', 100, 200, 1000); this.setThreshold('memory-per-invoice', 50, 100, 500); // MB } /** * Track a performance metric */ static async track( operation: string, fn: () => Promise, metadata?: Record ): Promise<{ result: T; metric: PerformanceMetric }> { const startMemory = process.memoryUsage(); const startCpu = process.cpuUsage(); const startTime = performance.now(); try { const result = await fn(); const endTime = performance.now(); const endMemory = process.memoryUsage(); const endCpu = process.cpuUsage(startCpu); const metric: PerformanceMetric = { operation, duration: endTime - startTime, timestamp: Date.now(), memory: { used: endMemory.heapUsed - startMemory.heapUsed, total: endMemory.heapTotal, external: endMemory.external }, cpu: { user: endCpu.user / 1000, // Convert to milliseconds system: endCpu.system / 1000 }, metadata }; // Store metric if (!this.metrics.has(operation)) { this.metrics.set(operation, []); } this.metrics.get(operation)!.push(metric); // Check threshold this.checkThreshold(operation, metric); return { result, metric }; } catch (error) { // Still track failed operations const endTime = performance.now(); const metric: PerformanceMetric = { operation, duration: endTime - startTime, timestamp: Date.now(), memory: { used: 0, total: process.memoryUsage().heapTotal, external: process.memoryUsage().external }, metadata: { ...metadata, error: error.message } }; if (!this.metrics.has(operation)) { this.metrics.set(operation, []); } this.metrics.get(operation)!.push(metric); throw error; } } /** * Get statistics for an operation */ static getStats(operation: string): PerformanceStats | null { const metrics = this.metrics.get(operation); if (!metrics || metrics.length === 0) { return null; } const durations = metrics.map(m => m.duration).sort((a, b) => a - b); const sum = durations.reduce((a, b) => a + b, 0); const avg = sum / durations.length; // Calculate standard deviation const squaredDiffs = durations.map(d => Math.pow(d - avg, 2)); const avgSquaredDiff = squaredDiffs.reduce((a, b) => a + b, 0) / durations.length; const stdDev = Math.sqrt(avgSquaredDiff); return { count: durations.length, min: durations[0], max: durations[durations.length - 1], avg, median: durations[Math.floor(durations.length / 2)], p95: durations[Math.floor(durations.length * 0.95)], p99: durations[Math.floor(durations.length * 0.99)], stdDev }; } /** * Get summary statistics for an operation (alias for getStats) */ static async getSummary(operation: string): Promise<{ average: number; min: number; max: number; p95: number; } | null> { const stats = this.getStats(operation); if (!stats) return null; return { average: stats.avg, min: stats.min, max: stats.max, p95: stats.p95 }; } /** * Get memory statistics */ static getMemoryStats(operation: string): { avgMemoryUsed: number; maxMemoryUsed: number; avgMemoryTotal: number; } | null { const metrics = this.metrics.get(operation); if (!metrics || metrics.length === 0) { return null; } const memoryUsed = metrics.map(m => m.memory.used); const memoryTotal = metrics.map(m => m.memory.total); return { avgMemoryUsed: memoryUsed.reduce((a, b) => a + b, 0) / memoryUsed.length / 1024 / 1024, // MB maxMemoryUsed: Math.max(...memoryUsed) / 1024 / 1024, // MB avgMemoryTotal: memoryTotal.reduce((a, b) => a + b, 0) / memoryTotal.length / 1024 / 1024 // MB }; } /** * Generate performance report */ static generateReport(): string { let report = '# Performance Report\n\n'; report += `Generated at: ${new Date().toISOString()}\n`; report += `Platform: ${os.platform()} ${os.arch()}\n`; report += `Node.js: ${process.version}\n`; report += `CPUs: ${os.cpus().length}x ${os.cpus()[0].model}\n`; report += `Total Memory: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)} GB\n\n`; for (const [operation, metrics] of this.metrics) { const stats = this.getStats(operation); const memStats = this.getMemoryStats(operation); const threshold = this.thresholds.get(operation); if (stats) { report += `## ${operation}\n\n`; report += `- Executions: ${stats.count}\n`; report += `- Duration:\n`; report += ` - Min: ${stats.min.toFixed(2)}ms\n`; report += ` - Max: ${stats.max.toFixed(2)}ms\n`; report += ` - Average: ${stats.avg.toFixed(2)}ms\n`; report += ` - Median: ${stats.median.toFixed(2)}ms\n`; report += ` - P95: ${stats.p95.toFixed(2)}ms\n`; report += ` - P99: ${stats.p99.toFixed(2)}ms\n`; report += ` - Std Dev: ${stats.stdDev.toFixed(2)}ms\n`; if (memStats) { report += `- Memory:\n`; report += ` - Avg Used: ${memStats.avgMemoryUsed.toFixed(2)} MB\n`; report += ` - Max Used: ${memStats.maxMemoryUsed.toFixed(2)} MB\n`; } if (threshold) { report += `- Thresholds:\n`; report += ` - Target: <${threshold.target}ms ${stats.avg <= threshold.target ? '✓' : '✗'}\n`; report += ` - Acceptable: <${threshold.acceptable}ms ${stats.avg <= threshold.acceptable ? '✓' : '✗'}\n`; report += ` - Maximum: <${threshold.maximum}ms ${stats.avg <= threshold.maximum ? '✓' : '✗'}\n`; } report += '\n'; } } return report; } /** * Check if a metric violates thresholds */ private static checkThreshold(operation: string, metric: PerformanceMetric): void { const threshold = this.thresholds.get(operation); if (!threshold) return; if (metric.duration > threshold.maximum) { console.warn(`⚠️ Performance violation: ${operation} took ${metric.duration.toFixed(2)}ms (max: ${threshold.maximum}ms)`); } else if (metric.duration > threshold.acceptable) { console.log(`⚡ Performance warning: ${operation} took ${metric.duration.toFixed(2)}ms (acceptable: ${threshold.acceptable}ms)`); } } /** * Reset all metrics */ static reset(): void { this.metrics.clear(); } /** * Export metrics to JSON */ static exportMetrics(): Record { const result: Record = {}; for (const [operation, metrics] of this.metrics) { result[operation] = metrics; } return result; } /** * Import metrics from JSON */ static importMetrics(data: Record): void { for (const [operation, metrics] of Object.entries(data)) { this.metrics.set(operation, metrics); } } /** * Track concurrent operations */ static async trackConcurrent( operation: string, tasks: Array<() => Promise>, concurrency: number = 10 ): Promise<{ results: T[]; totalDuration: number; avgDuration: number; throughput: number; }> { const startTime = performance.now(); const results: T[] = []; const durations: number[] = []; // Process in batches for (let i = 0; i < tasks.length; i += concurrency) { const batch = tasks.slice(i, i + concurrency); const batchResults = await Promise.all( batch.map(async (task) => { const { result, metric } = await this.track(`${operation}-concurrent`, task); durations.push(metric.duration); return result; }) ); results.push(...batchResults); } const totalDuration = performance.now() - startTime; const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length; const throughput = (tasks.length / totalDuration) * 1000; // ops/sec return { results, totalDuration, avgDuration, throughput }; } } // Initialize default thresholds PerformanceTracker.initializeDefaultThresholds();