335 lines
9.7 KiB
TypeScript
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(); |