523 lines
18 KiB
TypeScript
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(); |