einvoice/test/helpers/performance.tracker.ts
2025-05-25 19:45:37 +00:00

335 lines
9.7 KiB
TypeScript

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<string, any>;
}
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<string, PerformanceMetric[]>();
private static thresholds = new Map<string, { target: number; acceptable: number; maximum: number }>();
/**
* 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<T>(
operation: string,
fn: () => Promise<T>,
metadata?: Record<string, any>
): 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<string, PerformanceMetric[]> {
const result: Record<string, PerformanceMetric[]> = {};
for (const [operation, metrics] of this.metrics) {
result[operation] = metrics;
}
return result;
}
/**
* Import metrics from JSON
*/
static importMetrics(data: Record<string, PerformanceMetric[]>): void {
for (const [operation, metrics] of Object.entries(data)) {
this.metrics.set(operation, metrics);
}
}
/**
* Track concurrent operations
*/
static async trackConcurrent<T>(
operation: string,
tasks: Array<() => Promise<T>>,
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();