/** * @file test.perf-06.cpu-utilization.ts * @description Performance tests for CPU utilization monitoring */ import { tap, expect } from '@git.zone/tstest/tapbundle'; import { EInvoice, ValidationLevel } from '../../../ts/index.js'; import { FormatDetector } from '../../../ts/formats/utils/format.detector.js'; import { CorpusLoader } from '../../helpers/corpus.loader.js'; import * as os from 'os'; tap.test('PERF-06: CPU Utilization - should maintain efficient CPU usage patterns', async () => { // Helper function to get CPU usage const getCPUUsage = () => { const cpus = os.cpus(); let user = 0; let nice = 0; let sys = 0; let idle = 0; let irq = 0; for (const cpu of cpus) { user += cpu.times.user; nice += cpu.times.nice; sys += cpu.times.sys; idle += cpu.times.idle; irq += cpu.times.irq; } const total = user + nice + sys + idle + irq; return { user: user / total, system: sys / total, idle: idle / total, total: total }; }; // Load corpus files for testing const corpusFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII', 'ZUGFeRD'], maxFiles: 50, validOnly: true }); // Filter out very large files to avoid timeouts const testFiles = corpusFiles.filter(f => f.size < 500 * 1024); // Max 500KB console.log(`\nUsing ${testFiles.length} corpus files for CPU testing`); // Test 1: CPU usage baseline for operations console.log('\n=== CPU Usage Baseline ==='); const results = { operations: [], cpuCount: os.cpus().length, cpuModel: os.cpus()[0]?.model || 'Unknown' }; // Operations to test with real corpus files const operations = [ { name: 'Idle baseline', fn: async () => { await new Promise(resolve => setTimeout(resolve, 100)); } }, { name: 'Format detection (corpus)', fn: async () => { // Test format detection on a sample of corpus files const sampleFiles = testFiles.slice(0, 20); for (const file of sampleFiles) { const content = await CorpusLoader.loadFile(file.path); FormatDetector.detectFormat(content.toString()); } } }, { name: 'XML parsing (corpus)', fn: async () => { // Parse a sample of corpus files const sampleFiles = testFiles.slice(0, 10); for (const file of sampleFiles) { const content = await CorpusLoader.loadFile(file.path); try { await EInvoice.fromXml(content.toString()); } catch (e) { // Some files might fail parsing, that's ok } } } }, { name: 'Validation (corpus)', fn: async () => { // Validate a sample of corpus files const sampleFiles = testFiles.slice(0, 10); for (const file of sampleFiles) { const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); await einvoice.validate(ValidationLevel.SYNTAX); } catch (e) { // Some files might fail validation, that's ok } } } }, { name: 'Get format (corpus)', fn: async () => { // Get format on parsed corpus files const sampleFiles = testFiles.slice(0, 15); for (const file of sampleFiles) { const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); einvoice.getFormat(); } catch (e) { // Ignore errors } } } } ]; // Execute operations and measure CPU for (const operation of operations) { const startTime = Date.now(); const startUsage = process.cpuUsage(); await operation.fn(); const endUsage = process.cpuUsage(startUsage); const endTime = Date.now(); const duration = endTime - startTime; const userCPU = endUsage.user / 1000; // Convert to milliseconds const systemCPU = endUsage.system / 1000; results.operations.push({ name: operation.name, duration, userCPU: userCPU.toFixed(2), systemCPU: systemCPU.toFixed(2), totalCPU: (userCPU + systemCPU).toFixed(2), cpuPercentage: ((userCPU + systemCPU) / duration * 100).toFixed(2), efficiency: (duration / (userCPU + systemCPU)).toFixed(2) }); } // Test 2: Multi-core utilization with corpus files console.log('\n=== Multi-core Utilization ==='); const multiCoreResults = { coreCount: os.cpus().length, parallelTests: [] }; // Use a subset of corpus files for parallel testing const parallelTestFiles = testFiles.slice(0, 20); // Test different parallelism levels const parallelismLevels = [1, 2, 4, Math.min(8, multiCoreResults.coreCount)]; for (const parallelism of parallelismLevels) { const startUsage = process.cpuUsage(); const startTime = Date.now(); // Process files in parallel const batchSize = Math.ceil(parallelTestFiles.length / parallelism); const promises = []; for (let i = 0; i < parallelism; i++) { const batch = parallelTestFiles.slice(i * batchSize, (i + 1) * batchSize); promises.push( Promise.all(batch.map(async (file) => { const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); await einvoice.validate(ValidationLevel.SYNTAX); return einvoice.getFormat(); } catch (e) { return null; } })) ); } await Promise.all(promises); const endTime = Date.now(); const endUsage = process.cpuUsage(startUsage); const duration = endTime - startTime; const totalCPU = (endUsage.user + endUsage.system) / 1000; const theoreticalSpeedup = parallelism; const actualSpeedup = multiCoreResults.parallelTests.length > 0 ? multiCoreResults.parallelTests[0].duration / duration : 1; multiCoreResults.parallelTests.push({ parallelism, duration, totalCPU: totalCPU.toFixed(2), cpuEfficiency: ((totalCPU / duration) * 100).toFixed(2), theoreticalSpeedup, actualSpeedup: actualSpeedup.toFixed(2), efficiency: ((actualSpeedup / theoreticalSpeedup) * 100).toFixed(2) }); } // Test 3: CPU-intensive operations profiling with corpus files console.log('\n=== CPU-intensive Operations ==='); const cpuIntensiveResults = { operations: [] }; // Find complex corpus files for intensive operations const complexFiles = await CorpusLoader.createTestDataset({ categories: ['CII_XMLRECHNUNG', 'UBL_XMLRECHNUNG'], maxFiles: 10, validOnly: true }); // Test scenarios with real corpus files const scenarios = [ { name: 'Complex validation (corpus)', fn: async () => { for (const file of complexFiles.slice(0, 3)) { const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); await einvoice.validate(ValidationLevel.SYNTAX); await einvoice.validate(ValidationLevel.BUSINESS); } catch (e) { // Some validations might fail } } } }, { name: 'Large XML processing (corpus)', fn: async () => { // Find larger files (but not too large) const largerFiles = testFiles .filter(f => f.size > 50 * 1024 && f.size < 200 * 1024) .slice(0, 3); for (const file of largerFiles) { const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); await einvoice.validate(ValidationLevel.SYNTAX); } catch (e) { // Ignore errors } } } }, { name: 'Multiple operations (corpus)', fn: async () => { const mixedFiles = testFiles.slice(0, 5); for (const file of mixedFiles) { const content = await CorpusLoader.loadFile(file.path); try { // Detect format const format = FormatDetector.detectFormat(content.toString()); // Parse const einvoice = await EInvoice.fromXml(content.toString()); // Validate await einvoice.validate(ValidationLevel.SYNTAX); // Get format einvoice.getFormat(); } catch (e) { // Ignore errors } } } } ]; // Profile each scenario for (const scenario of scenarios) { const iterations = 3; const measurements = []; for (let i = 0; i < iterations; i++) { const startUsage = process.cpuUsage(); const startTime = process.hrtime.bigint(); await scenario.fn(); const endTime = process.hrtime.bigint(); const endUsage = process.cpuUsage(startUsage); const duration = Number(endTime - startTime) / 1_000_000; const cpuTime = (endUsage.user + endUsage.system) / 1000; measurements.push({ duration, cpuTime, efficiency: cpuTime / duration }); } // Calculate averages const avgDuration = measurements.reduce((sum, m) => sum + m.duration, 0) / iterations; const avgCpuTime = measurements.reduce((sum, m) => sum + m.cpuTime, 0) / iterations; const avgEfficiency = measurements.reduce((sum, m) => sum + m.efficiency, 0) / iterations; cpuIntensiveResults.operations.push({ name: scenario.name, iterations, avgDuration: avgDuration.toFixed(2), avgCpuTime: avgCpuTime.toFixed(2), avgEfficiency: (avgEfficiency * 100).toFixed(2), cpuIntensity: avgCpuTime > avgDuration * 0.8 ? 'HIGH' : avgCpuTime > avgDuration * 0.5 ? 'MEDIUM' : 'LOW' }); } // Test 4: Sample processing CPU profile console.log('\n=== Sample Processing CPU Profile ==='); const sampleCPUResults = { filesProcessed: 0, totalCPUTime: 0, totalWallTime: 0, cpuByOperation: { detection: { time: 0, count: 0 }, parsing: { time: 0, count: 0 }, validation: { time: 0, count: 0 }, getformat: { time: 0, count: 0 } } }; // Process a sample of corpus files const sampleFiles = testFiles.slice(0, 10); const overallStart = Date.now(); for (const file of sampleFiles) { try { const content = await CorpusLoader.loadFile(file.path); const contentStr = content.toString(); // Format detection let startUsage = process.cpuUsage(); const format = FormatDetector.detectFormat(contentStr); let endUsage = process.cpuUsage(startUsage); sampleCPUResults.cpuByOperation.detection.time += (endUsage.user + endUsage.system) / 1000; sampleCPUResults.cpuByOperation.detection.count++; if (!format || format === 'unknown') continue; // Parsing startUsage = process.cpuUsage(); const einvoice = await EInvoice.fromXml(contentStr); endUsage = process.cpuUsage(startUsage); sampleCPUResults.cpuByOperation.parsing.time += (endUsage.user + endUsage.system) / 1000; sampleCPUResults.cpuByOperation.parsing.count++; // Validation startUsage = process.cpuUsage(); await einvoice.validate(ValidationLevel.SYNTAX); endUsage = process.cpuUsage(startUsage); sampleCPUResults.cpuByOperation.validation.time += (endUsage.user + endUsage.system) / 1000; sampleCPUResults.cpuByOperation.validation.count++; // Get format startUsage = process.cpuUsage(); einvoice.getFormat(); endUsage = process.cpuUsage(startUsage); sampleCPUResults.cpuByOperation.getformat.time += (endUsage.user + endUsage.system) / 1000; sampleCPUResults.cpuByOperation.getformat.count++; sampleCPUResults.filesProcessed++; } catch (error) { // Skip failed files } } sampleCPUResults.totalWallTime = Date.now() - overallStart; // Calculate totals and averages for (const op of Object.keys(sampleCPUResults.cpuByOperation)) { const opData = sampleCPUResults.cpuByOperation[op]; sampleCPUResults.totalCPUTime += opData.time; } // Test 5: Sustained CPU load test with corpus files console.log('\n=== Sustained CPU Load Test ==='); const testDuration = 2000; // 2 seconds const sustainedResults = { samples: [], avgCPUUsage: 0, peakCPUUsage: 0, consistency: 0 }; // Use a small set of corpus files for sustained load const sustainedFiles = testFiles.slice(0, 5); const startTime = Date.now(); let sampleCount = 0; // Run sustained load while (Date.now() - startTime < testDuration) { const sampleStart = process.cpuUsage(); const sampleStartTime = Date.now(); // Perform operations on corpus files const file = sustainedFiles[sampleCount % sustainedFiles.length]; const content = await CorpusLoader.loadFile(file.path); try { const einvoice = await EInvoice.fromXml(content.toString()); await einvoice.validate(ValidationLevel.SYNTAX); einvoice.getFormat(); } catch (e) { // Ignore errors } const sampleEndTime = Date.now(); const sampleEnd = process.cpuUsage(sampleStart); const sampleDuration = sampleEndTime - sampleStartTime; const cpuTime = (sampleEnd.user + sampleEnd.system) / 1000; const cpuUsage = (cpuTime / sampleDuration) * 100; sustainedResults.samples.push(cpuUsage); if (cpuUsage > sustainedResults.peakCPUUsage) { sustainedResults.peakCPUUsage = cpuUsage; } sampleCount++; } // Calculate statistics if (sustainedResults.samples.length > 0) { sustainedResults.avgCPUUsage = sustainedResults.samples.reduce((a, b) => a + b, 0) / sustainedResults.samples.length; // Calculate standard deviation for consistency const variance = sustainedResults.samples.reduce((sum, val) => sum + Math.pow(val - sustainedResults.avgCPUUsage, 2), 0) / sustainedResults.samples.length; const stdDev = Math.sqrt(variance); sustainedResults.consistency = 100 - (stdDev / Math.max(sustainedResults.avgCPUUsage, 1) * 100); } // Summary console.log('\n=== PERF-06: CPU Utilization Test Summary ==='); console.log('\nCPU Baseline:'); console.log(` System: ${results.cpuCount} cores, ${results.cpuModel}`); console.log(' Operation benchmarks:'); results.operations.forEach(op => { console.log(` ${op.name}:`); console.log(` - Duration: ${op.duration}ms`); console.log(` - CPU time: ${op.totalCPU}ms (user: ${op.userCPU}ms, system: ${op.systemCPU}ms)`); console.log(` - CPU usage: ${op.cpuPercentage}%`); console.log(` - Efficiency: ${op.efficiency}x`); }); console.log('\nMulti-Core Utilization:'); console.log(' Parallelism | Duration | CPU Time | Efficiency | Speedup | Scaling'); console.log(' ------------|----------|----------|------------|---------|--------'); multiCoreResults.parallelTests.forEach(test => { console.log(` ${String(test.parallelism).padEnd(11)} | ${String(test.duration + 'ms').padEnd(8)} | ${test.totalCPU.padEnd(8)}ms | ${test.cpuEfficiency.padEnd(10)}% | ${test.actualSpeedup.padEnd(7)}x | ${test.efficiency}%`); }); console.log('\nCPU-Intensive Operations:'); cpuIntensiveResults.operations.forEach(op => { console.log(` ${op.name}:`); console.log(` - Avg duration: ${op.avgDuration}ms`); console.log(` - Avg CPU time: ${op.avgCpuTime}ms`); console.log(` - CPU efficiency: ${op.avgEfficiency}%`); console.log(` - Intensity: ${op.cpuIntensity}`); }); console.log('\nSample CPU Profile:'); console.log(` Files processed: ${sampleCPUResults.filesProcessed}`); console.log(` Total wall time: ${sampleCPUResults.totalWallTime}ms`); console.log(` Total CPU time: ${sampleCPUResults.totalCPUTime.toFixed(2)}ms`); const cpuEfficiency = sampleCPUResults.totalWallTime > 0 ? ((sampleCPUResults.totalCPUTime / sampleCPUResults.totalWallTime) * 100).toFixed(2) : '0'; console.log(` CPU efficiency: ${cpuEfficiency}%`); console.log(' By operation:'); Object.entries(sampleCPUResults.cpuByOperation).forEach(([op, data]) => { const avgTime = data.count > 0 ? (data.time / data.count).toFixed(3) : 'N/A'; const percentage = sampleCPUResults.totalCPUTime > 0 ? ((data.time / sampleCPUResults.totalCPUTime) * 100).toFixed(1) : '0'; console.log(` - ${op}: ${data.time.toFixed(2)}ms (${percentage}%), avg ${avgTime}ms`); }); console.log('\nSustained CPU Load (2 seconds):'); console.log(` Samples: ${sustainedResults.samples.length}`); console.log(` Average CPU usage: ${sustainedResults.avgCPUUsage.toFixed(2)}%`); console.log(` Peak CPU usage: ${sustainedResults.peakCPUUsage.toFixed(2)}%`); console.log(` Consistency: ${sustainedResults.consistency.toFixed(2)}%`); const stable = sustainedResults.consistency > 60; console.log(` Stable performance: ${stable ? 'YES ✅' : 'NO ⚠️'}`); // Performance targets check console.log('\n=== Performance Targets Check ==='); const avgCPUEfficiency = parseFloat(cpuEfficiency); console.log(`CPU efficiency: ${avgCPUEfficiency}% ${avgCPUEfficiency > 30 ? '✅' : '⚠️'} (target: >30%)`); console.log(`CPU stability: ${stable ? 'STABLE ✅' : 'UNSTABLE ⚠️'}`); // Verify basic functionality works expect(results.operations.length).toBeGreaterThan(0); expect(multiCoreResults.parallelTests.length).toBeGreaterThan(0); expect(cpuIntensiveResults.operations.length).toBeGreaterThan(0); expect(sustainedResults.samples.length).toBeGreaterThan(0); console.log('\n=== CPU Utilization Tests Completed Successfully ==='); console.log('All tests used real invoice files from the test corpus'); console.log(`Tested with ${testFiles.length} corpus files from various formats`); }); tap.start();