einvoice/test/suite/einvoice_performance/test.perf-06.cpu-utilization.ts
2025-05-29 13:35:36 +00:00

523 lines
18 KiB
TypeScript

/**
* @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();