/** * @file test.conv-12.performance.ts * @description Performance benchmarks for format conversion operations */ import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../../suite/corpus.loader.js'; import { PerformanceTracker } from '../../suite/performance.tracker.js'; const corpusLoader = new CorpusLoader(); const performanceTracker = new PerformanceTracker('CONV-12: Conversion Performance'); tap.test('CONV-12: Conversion Performance - should meet performance targets for conversion operations', async (t) => { // Test 1: Single conversion performance benchmarks const singleConversionBenchmarks = await performanceTracker.measureAsync( 'single-conversion-benchmarks', async () => { const einvoice = new EInvoice(); const benchmarks = []; // Define conversion scenarios const scenarios = [ { from: 'ubl', to: 'cii', name: 'UBL to CII' }, { from: 'cii', to: 'ubl', name: 'CII to UBL' }, { from: 'ubl', to: 'xrechnung', name: 'UBL to XRechnung' }, { from: 'cii', to: 'zugferd', name: 'CII to ZUGFeRD' }, { from: 'zugferd', to: 'xrechnung', name: 'ZUGFeRD to XRechnung' } ]; // Create test invoices for each format const testInvoices = { ubl: { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-UBL-001', issueDate: '2024-01-30', seller: { name: 'UBL Seller', address: 'UBL Street', country: 'US', taxId: 'US123456789' }, buyer: { name: 'UBL Buyer', address: 'UBL Avenue', country: 'US', taxId: 'US987654321' }, items: [{ description: 'Product', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }], totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 } } }, cii: { format: 'cii' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-CII-001', issueDate: '2024-01-30', seller: { name: 'CII Seller', address: 'CII Street', country: 'DE', taxId: 'DE123456789' }, buyer: { name: 'CII Buyer', address: 'CII Avenue', country: 'DE', taxId: 'DE987654321' }, items: [{ description: 'Service', quantity: 1, unitPrice: 200, vatRate: 19, lineTotal: 200 }], totals: { netAmount: 200, vatAmount: 38, grossAmount: 238 } } }, zugferd: { format: 'zugferd' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-ZF-001', issueDate: '2024-01-30', seller: { name: 'ZF Seller', address: 'ZF Street', country: 'DE', taxId: 'DE111222333' }, buyer: { name: 'ZF Buyer', address: 'ZF Avenue', country: 'DE', taxId: 'DE444555666' }, items: [{ description: 'Goods', quantity: 5, unitPrice: 50, vatRate: 19, lineTotal: 250 }], totals: { netAmount: 250, vatAmount: 47.50, grossAmount: 297.50 } } } }; // Run benchmarks for (const scenario of scenarios) { if (!testInvoices[scenario.from]) continue; const iterations = 10; const times = []; for (let i = 0; i < iterations; i++) { const startTime = process.hrtime.bigint(); try { await einvoice.convertFormat(testInvoices[scenario.from], scenario.to); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds times.push(duration); } catch (error) { // Conversion not supported } } 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] }); } } return benchmarks; } ); // Test 2: Complex invoice conversion performance const complexInvoicePerformance = await performanceTracker.measureAsync( 'complex-invoice-performance', async () => { const einvoice = new EInvoice(); // Create complex invoice with many items const complexInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'PERF-COMPLEX-001', issueDate: '2024-01-30', dueDate: '2024-02-29', currency: 'EUR', seller: { name: 'Complex International Trading Company Ltd.', address: 'Global Business Center, Tower A, Floor 25', city: 'London', postalCode: 'EC2M 7PY', country: 'GB', taxId: 'GB123456789', email: 'invoicing@complex-trading.com', phone: '+44 20 7123 4567', registrationNumber: 'UK12345678' }, buyer: { name: 'Multinational Buyer Corporation GmbH', address: 'Industriestraße 100-200', city: 'Frankfurt', postalCode: '60311', country: 'DE', taxId: 'DE987654321', email: 'ap@buyer-corp.de', phone: '+49 69 9876 5432' }, items: Array.from({ length: 100 }, (_, i) => ({ description: `Product Line Item ${i + 1} - Detailed description with technical specifications and compliance information`, quantity: Math.floor(Math.random() * 100) + 1, unitPrice: Math.random() * 1000, vatRate: [7, 19, 21][Math.floor(Math.random() * 3)], lineTotal: 0, // Will be calculated itemId: `ITEM-${String(i + 1).padStart(4, '0')}`, additionalInfo: { weight: `${Math.random() * 10}kg`, dimensions: `${Math.random() * 100}x${Math.random() * 100}x${Math.random() * 100}cm`, countryOfOrigin: ['DE', 'FR', 'IT', 'CN', 'US'][Math.floor(Math.random() * 5)] } })), totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }, paymentTerms: 'Net 30 days, 2% discount for payment within 10 days', notes: 'This is a complex invoice with 100 line items for performance testing purposes. All items are subject to standard terms and conditions.' } }; // Calculate totals complexInvoice.data.items.forEach(item => { item.lineTotal = item.quantity * item.unitPrice; complexInvoice.data.totals.netAmount += item.lineTotal; complexInvoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100); }); complexInvoice.data.totals.grossAmount = complexInvoice.data.totals.netAmount + complexInvoice.data.totals.vatAmount; // Test conversions const conversions = ['cii', 'zugferd', 'xrechnung']; const results = []; for (const targetFormat of conversions) { const startTime = process.hrtime.bigint(); let success = false; let error = null; try { const converted = await einvoice.convertFormat(complexInvoice, targetFormat); success = converted !== null; } catch (e) { error = e.message; } const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; results.push({ targetFormat, duration, success, error, itemsPerSecond: success ? (100 / (duration / 1000)).toFixed(2) : 'N/A' }); } return { invoiceSize: { items: complexInvoice.data.items.length, netAmount: complexInvoice.data.totals.netAmount.toFixed(2), grossAmount: complexInvoice.data.totals.grossAmount.toFixed(2) }, conversions: results }; } ); // Test 3: Memory usage during conversion const memoryUsageAnalysis = await performanceTracker.measureAsync( 'memory-usage-analysis', async () => { const einvoice = new EInvoice(); 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, 200]; for (const size of sizes) { const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: `MEM-TEST-${size}`, issueDate: '2024-01-30', seller: { name: 'Memory Test Seller', address: 'Test Street', country: 'US', taxId: 'US123456789' }, buyer: { name: 'Memory Test Buyer', address: 'Test Avenue', country: 'US', taxId: 'US987654321' }, items: Array.from({ length: size }, (_, i) => ({ description: `Item ${i + 1} with a reasonably long description to simulate real-world data`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: size * 100, vatAmount: size * 10, grossAmount: size * 110 } } }; // Perform conversion and measure memory const beforeConversion = process.memoryUsage(); try { const converted = await einvoice.convertFormat(invoice, 'cii'); const afterConversion = process.memoryUsage(); memorySnapshots.push({ items: size, heapUsedBefore: Math.round((beforeConversion.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100, heapUsedAfter: Math.round((afterConversion.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100, heapIncrease: Math.round((afterConversion.heapUsed - beforeConversion.heapUsed) / 1024 / 1024 * 100) / 100, external: Math.round((afterConversion.external - baselineMemory.external) / 1024 / 1024 * 100) / 100 }); } catch (error) { // Skip if conversion fails } } // Force garbage collection and measure final state if (global.gc) global.gc(); const finalMemory = process.memoryUsage(); return { snapshots: memorySnapshots, totalMemoryIncrease: Math.round((finalMemory.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100, memoryPerItem: memorySnapshots.length > 0 ? (memorySnapshots[memorySnapshots.length - 1].heapIncrease / sizes[sizes.length - 1]).toFixed(3) : 'N/A' }; } ); // Test 4: Concurrent conversion performance const concurrentPerformance = await performanceTracker.measureAsync( 'concurrent-conversion-performance', async () => { const einvoice = new EInvoice(); const concurrencyLevels = [1, 5, 10, 20]; const results = []; // Create test invoice const testInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'CONC-TEST-001', issueDate: '2024-01-30', seller: { name: 'Concurrent Seller', address: 'Parallel Street', country: 'US', taxId: 'US123456789' }, buyer: { name: 'Concurrent Buyer', address: 'Async Avenue', country: 'US', taxId: 'US987654321' }, items: Array.from({ length: 10 }, (_, i) => ({ description: `Concurrent Item ${i + 1}`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 } } }; for (const concurrency of concurrencyLevels) { const startTime = Date.now(); // Create concurrent conversion tasks const tasks = Array.from({ length: concurrency }, () => einvoice.convertFormat(testInvoice, 'cii').catch(() => null) ); const taskResults = await Promise.all(tasks); const endTime = Date.now(); const successful = taskResults.filter(r => r !== null).length; const duration = endTime - startTime; const throughput = (successful / (duration / 1000)).toFixed(2); results.push({ concurrency, duration, successful, failed: concurrency - successful, throughput: `${throughput} conversions/sec` }); } return results; } ); // Test 5: Corpus conversion performance analysis const corpusPerformance = await performanceTracker.measureAsync( 'corpus-conversion-performance', async () => { const files = await corpusLoader.getFilesByPattern('**/*.xml'); const einvoice = new EInvoice(); const performanceData = { formatStats: new Map(), 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 }, totalConversions: 0, failedConversions: 0 }; // Sample files for performance testing const sampleFiles = files.slice(0, 50); for (const file of sampleFiles) { try { const content = await plugins.fs.readFile(file, 'utf-8'); const fileSize = Buffer.byteLength(content, 'utf-8'); // Categorize by size const sizeCategory = fileSize < 10240 ? 'small' : fileSize < 102400 ? 'medium' : 'large'; // Detect format and parse const format = await einvoice.detectFormat(content); if (!format || format === 'unknown') continue; const parsed = await einvoice.parseInvoice(content, format); // Measure conversion time const targetFormat = format === 'ubl' ? 'cii' : 'ubl'; const startTime = process.hrtime.bigint(); try { await einvoice.convertFormat(parsed, targetFormat); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; // Update format stats if (!performanceData.formatStats.has(format)) { performanceData.formatStats.set(format, { count: 0, totalTime: 0, minTime: Infinity, maxTime: 0 }); } const stats = performanceData.formatStats.get(format)!; stats.count++; stats.totalTime += duration; stats.minTime = Math.min(stats.minTime, duration); stats.maxTime = Math.max(stats.maxTime, duration); // Update size category stats performanceData.sizeCategories[sizeCategory].count++; performanceData.sizeCategories[sizeCategory].totalTime += duration; performanceData.totalConversions++; } catch (convError) { performanceData.failedConversions++; } } catch (error) { // Skip files that can't be processed } } // 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; } } // Format statistics const formatStatsSummary = Array.from(performanceData.formatStats.entries()).map(([format, stats]) => ({ format, count: stats.count, avgTime: stats.count > 0 ? (stats.totalTime / stats.count).toFixed(2) : 'N/A', minTime: stats.minTime === Infinity ? 'N/A' : stats.minTime.toFixed(2), maxTime: stats.maxTime.toFixed(2) })); return { totalConversions: performanceData.totalConversions, failedConversions: performanceData.failedConversions, successRate: ((performanceData.totalConversions - performanceData.failedConversions) / performanceData.totalConversions * 100).toFixed(2) + '%', formatStats: formatStatsSummary, sizeCategories: { small: { ...performanceData.sizeCategories.small, avgTime: performanceData.sizeCategories.small.avgTime.toFixed(2) }, medium: { ...performanceData.sizeCategories.medium, avgTime: performanceData.sizeCategories.medium.avgTime.toFixed(2) }, large: { ...performanceData.sizeCategories.large, avgTime: performanceData.sizeCategories.large.avgTime.toFixed(2) } } }; } ); // Summary t.comment('\n=== CONV-12: Conversion Performance Test Summary ==='); t.comment('\nSingle Conversion Benchmarks (10 iterations each):'); singleConversionBenchmarks.result.forEach(bench => { t.comment(` ${bench.scenario}:`); t.comment(` - Min: ${bench.min.toFixed(2)}ms, Max: ${bench.max.toFixed(2)}ms`); t.comment(` - Average: ${bench.avg.toFixed(2)}ms, Median: ${bench.median.toFixed(2)}ms, P95: ${bench.p95.toFixed(2)}ms`); }); t.comment('\nComplex Invoice Performance (100 items):'); t.comment(` Invoice size: ${complexInvoicePerformance.result.invoiceSize.items} items, €${complexInvoicePerformance.result.invoiceSize.grossAmount}`); complexInvoicePerformance.result.conversions.forEach(conv => { t.comment(` ${conv.targetFormat}: ${conv.duration.toFixed(2)}ms (${conv.itemsPerSecond} items/sec) - ${conv.success ? 'SUCCESS' : 'FAILED'}`); }); t.comment('\nMemory Usage Analysis:'); memoryUsageAnalysis.result.snapshots.forEach(snap => { t.comment(` ${snap.items} items: ${snap.heapIncrease}MB heap increase`); }); t.comment(` Average memory per item: ${memoryUsageAnalysis.result.memoryPerItem}MB`); t.comment('\nConcurrent Conversion Performance:'); concurrentPerformance.result.forEach(result => { t.comment(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput}`); }); t.comment('\nCorpus Performance Analysis:'); t.comment(` Total conversions: ${corpusPerformance.result.totalConversions}`); t.comment(` Success rate: ${corpusPerformance.result.successRate}`); t.comment(' By format:'); corpusPerformance.result.formatStats.forEach(stat => { t.comment(` - ${stat.format}: ${stat.count} files, avg ${stat.avgTime}ms (min: ${stat.minTime}ms, max: ${stat.maxTime}ms)`); }); t.comment(' By size:'); Object.entries(corpusPerformance.result.sizeCategories).forEach(([size, data]: [string, any]) => { t.comment(` - ${size}: ${data.count} files, avg ${data.avgTime}ms`); }); // Performance summary t.comment('\n=== Overall Performance Summary ==='); performanceTracker.logSummary(); t.end(); }); tap.start();