/** * @file test.conv-12.performance.ts * @description Performance benchmarks for format conversion operations */ import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice } from '../../../ts/index.js'; tap.test('CONV-12: Performance - should measure single XML load/export performance', async () => { const einvoice = new EInvoice(); const benchmarks = []; // Define test scenarios const scenarios = [ { format: 'ubl', name: 'UBL Load/Export' }, { format: 'cii', name: 'CII Load/Export' } ]; // Create test invoices for each format const testInvoices = { ubl: ` PERF-UBL-001 2024-01-30 380 EUR UBL Performance Test Seller Test Street 1 Test City 12345 DE UBL Performance Test Buyer Buyer Street 10 Buyer City 54321 DE 1 1 100.00 Product 100.00 110.00 `, cii: ` PERF-CII-001 380 20240130 1 Product 2 200.00 CII Performance Test Seller Test Street 1 Test City 12345 DE CII Performance Test Buyer Buyer Street 10 Buyer City 54321 DE EUR 200.00 38.00 238.00 238.00 ` }; // Run benchmarks for (const scenario of scenarios) { const iterations = 10; const times = []; for (let i = 0; i < iterations; i++) { const startTime = process.hrtime.bigint(); try { // Load XML await einvoice.loadXml(testInvoices[scenario.format]); // Export back to XML await einvoice.toXmlString(scenario.format as any); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds times.push(duration); } catch (error) { console.log(`Error in ${scenario.name}:`, error); } } if (times.length > 0) { times.sort((a, b) => a - b); benchmarks.push({ scenario: scenario.name, min: times[0], max: times[times.length - 1], avg: times.reduce((a, b) => a + b, 0) / times.length, median: times[Math.floor(times.length / 2)], p95: times[Math.floor(times.length * 0.95)] || times[times.length - 1] }); } } console.log('\nSingle Operation Benchmarks (10 iterations each):'); benchmarks.forEach(bench => { console.log(` ${bench.scenario}:`); console.log(` - Min: ${bench.min.toFixed(2)}ms, Max: ${bench.max.toFixed(2)}ms`); console.log(` - Average: ${bench.avg.toFixed(2)}ms, Median: ${bench.median.toFixed(2)}ms, P95: ${bench.p95.toFixed(2)}ms`); }); expect(benchmarks.length).toBeGreaterThan(0); benchmarks.forEach(bench => { expect(bench.avg).toBeLessThan(100); // Should process in less than 100ms on average }); }); tap.test('CONV-12: Performance - should handle complex invoice with many items', async () => { const einvoice = new EInvoice(); // Create complex invoice with many items const itemCount = 100; const complexInvoice = ` PERF-COMPLEX-001 2024-01-30 2024-02-29 380 EUR This is a complex invoice with ${itemCount} line items for performance testing purposes. Complex International Trading Company Ltd. Global Business Center, Tower A, Floor 25 London EC2M 7PY GB GB123456789 VAT Multinational Buyer Corporation GmbH Industriestraße 100-200 Frankfurt 60311 DE ${Array.from({ length: itemCount }, (_, i) => ` ${i + 1} ${Math.floor(Math.random() * 100) + 1} ${(Math.random() * 1000).toFixed(2)} Product Line Item ${i + 1} - Detailed description with technical specifications ${(Math.random() * 100).toFixed(2)} `).join('')} 50000.00 50000.00 59500.00 59500.00 `; const results = []; const operations = ['load', 'export']; for (const operation of operations) { const startTime = process.hrtime.bigint(); let success = false; try { if (operation === 'load') { await einvoice.loadXml(complexInvoice); success = einvoice.id === 'PERF-COMPLEX-001'; } else { const exported = await einvoice.toXmlString('ubl'); success = exported.includes('PERF-COMPLEX-001'); } } catch (e) { console.log(`Error in ${operation}:`, e); } const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; results.push({ operation, duration, success, itemsPerSecond: success ? (itemCount / (duration / 1000)).toFixed(2) : 'N/A' }); } console.log('\nComplex Invoice Performance (100 items):'); results.forEach(result => { console.log(` ${result.operation}: ${result.duration.toFixed(2)}ms (${result.itemsPerSecond} items/sec) - ${result.success ? 'SUCCESS' : 'FAILED'}`); }); expect(results.filter(r => r.success).length).toBeGreaterThan(0); }); tap.test('CONV-12: Performance - should analyze memory usage during operations', async () => { const memorySnapshots = []; // Force garbage collection if available if (global.gc) global.gc(); const baselineMemory = process.memoryUsage(); // Create invoices of increasing size const sizes = [1, 10, 50, 100]; for (const size of sizes) { const einvoice = new EInvoice(); const invoice = ` MEM-TEST-${size} 2024-01-30 380 EUR Memory Test Seller Memory Street 1 Memory City 10000 DE Memory Test Buyer Buyer Street 5 Buyer City 20000 DE ${Array.from({ length: size }, (_, i) => ` ${i + 1} 1 100.00 Item ${i + 1} with a reasonably long description to simulate real-world data 100.00 `).join('')} ${size * 110}.00 `; // Measure memory before and after operations const beforeOperation = process.memoryUsage(); try { await einvoice.loadXml(invoice); await einvoice.toXmlString('ubl'); const afterOperation = process.memoryUsage(); memorySnapshots.push({ items: size, heapUsedBefore: Math.round((beforeOperation.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100, heapUsedAfter: Math.round((afterOperation.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100, heapIncrease: Math.round((afterOperation.heapUsed - beforeOperation.heapUsed) / 1024 / 1024 * 100) / 100, external: Math.round((afterOperation.external - baselineMemory.external) / 1024 / 1024 * 100) / 100 }); } catch (error) { // Skip if operation fails } } // Force garbage collection and measure final state if (global.gc) global.gc(); const finalMemory = process.memoryUsage(); const totalMemoryIncrease = Math.round((finalMemory.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100; const memoryPerItem = memorySnapshots.length > 0 ? (memorySnapshots[memorySnapshots.length - 1].heapIncrease / sizes[sizes.length - 1]).toFixed(3) : 'N/A'; console.log('\nMemory Usage Analysis:'); memorySnapshots.forEach(snap => { console.log(` ${snap.items} items: ${snap.heapIncrease}MB heap increase`); }); console.log(` Total memory increase: ${totalMemoryIncrease}MB`); console.log(` Average memory per item: ${memoryPerItem}MB`); expect(memorySnapshots.length).toBeGreaterThan(0); // Memory increase should be reasonable expect(totalMemoryIncrease).toBeLessThan(50); }); tap.test('CONV-12: Performance - should handle concurrent operations', async () => { const concurrencyLevels = [1, 5, 10]; const results = []; // Create test invoice const testInvoice = ` CONC-TEST-001 2024-01-30 380 EUR Concurrent Seller Seller Street 1 Seller City 11111 DE Concurrent Buyer Buyer Street 1 Buyer City 22222 DE 1 10 1000.00 Test Product 1100.00 `; for (const concurrency of concurrencyLevels) { const startTime = Date.now(); // Create concurrent load/export tasks const tasks = Array.from({ length: concurrency }, async () => { try { const einvoice = new EInvoice(); await einvoice.loadXml(testInvoice); await einvoice.toXmlString('ubl'); return true; } catch { return false; } }); const taskResults = await Promise.all(tasks); const endTime = Date.now(); const successful = taskResults.filter(r => r).length; const duration = endTime - startTime; const throughput = (successful / (duration / 1000)).toFixed(2); results.push({ concurrency, duration, successful, failed: concurrency - successful, throughput: `${throughput} operations/sec` }); } console.log('\nConcurrent Operations Performance:'); results.forEach(result => { console.log(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput}`); }); expect(results.every(r => r.successful > 0)).toEqual(true); }); tap.test('CONV-12: Performance - should analyze corpus file processing performance', async () => { const corpusDir = plugins.path.join(process.cwd(), 'test/assets/corpus'); const performanceData = { totalFiles: 0, successfulLoads: 0, processingTimes: [] as number[], sizeCategories: { small: { count: 0, avgTime: 0, totalTime: 0 }, // < 10KB medium: { count: 0, avgTime: 0, totalTime: 0 }, // 10KB - 100KB large: { count: 0, avgTime: 0, totalTime: 0 } // > 100KB } }; // Sample a few known corpus files const testFiles = [ 'XML-Rechnung/UBL/EN16931_Einfach.ubl.xml', 'XML-Rechnung/CII/EN16931_Einfach.cii.xml', 'XML-Rechnung/UBL/EN16931_Rabatte.ubl.xml', 'XML-Rechnung/CII/EN16931_Rabatte.cii.xml', 'PEPPOL/Valid/billing-3.0-invoice-full-sample.xml' ]; for (const file of testFiles) { const fullPath = plugins.path.join(corpusDir, file); try { const content = await plugins.fs.readFile(fullPath, 'utf-8'); const fileSize = Buffer.byteLength(content, 'utf-8'); performanceData.totalFiles++; // Categorize by size const sizeCategory = fileSize < 10240 ? 'small' : fileSize < 102400 ? 'medium' : 'large'; // Measure load time const startTime = process.hrtime.bigint(); try { const einvoice = new EInvoice(); await einvoice.loadXml(content); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; if (einvoice.id) { performanceData.successfulLoads++; performanceData.processingTimes.push(duration); // Update size category stats performanceData.sizeCategories[sizeCategory].count++; performanceData.sizeCategories[sizeCategory].totalTime += duration; } } catch (error) { // Skip files that can't be loaded } } catch (error) { // File doesn't exist } } // Calculate averages for (const category of Object.keys(performanceData.sizeCategories)) { const cat = performanceData.sizeCategories[category]; if (cat.count > 0) { cat.avgTime = cat.totalTime / cat.count; } } const avgProcessingTime = performanceData.processingTimes.length > 0 ? performanceData.processingTimes.reduce((a, b) => a + b, 0) / performanceData.processingTimes.length : 0; console.log('\nCorpus File Processing Performance:'); console.log(` Files tested: ${performanceData.totalFiles}`); console.log(` Successfully loaded: ${performanceData.successfulLoads}`); console.log(` Average processing time: ${avgProcessingTime.toFixed(2)}ms`); console.log(' By size:'); Object.entries(performanceData.sizeCategories).forEach(([size, data]) => { if (data.count > 0) { console.log(` - ${size}: ${data.count} files, avg ${data.avgTime.toFixed(2)}ms`); } }); expect(performanceData.successfulLoads).toBeGreaterThan(0); // Average processing time should be reasonable expect(avgProcessingTime).toBeLessThan(500); }); tap.start();