/** * @file test.perf-05.memory-usage.ts * @description Performance tests for memory usage profiling */ 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('PERF-05: Memory Usage Profiling'); tap.test('PERF-05: Memory Usage Profiling - should maintain efficient memory usage patterns', async (t) => { // Test 1: Baseline memory usage for different operations const baselineMemoryUsage = await performanceTracker.measureAsync( 'baseline-memory-usage', async () => { const einvoice = new EInvoice(); const results = { operations: [], initialMemory: null, finalMemory: null }; // Force garbage collection if available if (global.gc) global.gc(); results.initialMemory = process.memoryUsage(); // Test different operations const operations = [ { name: 'Format Detection', fn: async () => { const xml = 'TEST'; for (let i = 0; i < 100; i++) { await einvoice.detectFormat(xml); } } }, { name: 'XML Parsing', fn: async () => { const xml = ` MEM-TEST 2024-01-01 ${Array(10).fill('Line').join('\n')} `; for (let i = 0; i < 50; i++) { await einvoice.parseInvoice(xml, 'ubl'); } } }, { name: 'Validation', fn: async () => { const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'MEM-VAL-001', issueDate: '2024-02-10', seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 20 }, (_, i) => ({ description: `Item ${i + 1}`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 } } }; for (let i = 0; i < 30; i++) { await einvoice.validateInvoice(invoice); } } }, { name: 'Format Conversion', fn: async () => { const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'MEM-CONV-001', issueDate: '2024-02-10', seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: [{ description: 'Item', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }], totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 } } }; for (let i = 0; i < 20; i++) { await einvoice.convertFormat(invoice, 'cii'); } } } ]; // Execute operations and measure memory for (const operation of operations) { if (global.gc) global.gc(); const beforeMemory = process.memoryUsage(); await operation.fn(); if (global.gc) global.gc(); const afterMemory = process.memoryUsage(); results.operations.push({ name: operation.name, heapUsedBefore: (beforeMemory.heapUsed / 1024 / 1024).toFixed(2), heapUsedAfter: (afterMemory.heapUsed / 1024 / 1024).toFixed(2), heapIncrease: ((afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024).toFixed(2), externalIncrease: ((afterMemory.external - beforeMemory.external) / 1024 / 1024).toFixed(2), rssIncrease: ((afterMemory.rss - beforeMemory.rss) / 1024 / 1024).toFixed(2) }); } if (global.gc) global.gc(); results.finalMemory = process.memoryUsage(); return results; } ); // Test 2: Memory scaling with invoice complexity const memoryScaling = await performanceTracker.measureAsync( 'memory-scaling', async () => { const einvoice = new EInvoice(); const results = { scalingData: [], memoryFormula: null }; // Test with increasing invoice sizes const itemCounts = [1, 10, 50, 100, 200, 500, 1000]; for (const itemCount of itemCounts) { if (global.gc) global.gc(); const beforeMemory = process.memoryUsage(); // Create invoice with specified number of items const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: `SCALE-${itemCount}`, issueDate: '2024-02-10', seller: { name: 'Memory Test Seller Corporation Ltd.', address: '123 Memory Lane, Suite 456', city: 'Test City', postalCode: '12345', country: 'US', taxId: 'US123456789' }, buyer: { name: 'Memory Test Buyer Enterprises Inc.', address: '789 RAM Avenue, Floor 10', city: 'Cache Town', postalCode: '67890', country: 'US', taxId: 'US987654321' }, items: Array.from({ length: itemCount }, (_, i) => ({ description: `Product Item Number ${i + 1} with detailed description and specifications`, quantity: Math.floor(Math.random() * 100) + 1, unitPrice: Math.random() * 1000, vatRate: [5, 10, 15, 20][Math.floor(Math.random() * 4)], lineTotal: 0, itemId: `ITEM-${String(i + 1).padStart(6, '0')}`, additionalInfo: { weight: `${Math.random() * 10}kg`, dimensions: `${Math.random() * 100}x${Math.random() * 100}x${Math.random() * 100}`, notes: `Additional notes for item ${i + 1}` } })), totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 } } }; // Calculate totals invoice.data.items.forEach(item => { item.lineTotal = item.quantity * item.unitPrice; invoice.data.totals.netAmount += item.lineTotal; invoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100); }); invoice.data.totals.grossAmount = invoice.data.totals.netAmount + invoice.data.totals.vatAmount; // Process invoice through multiple operations const parsed = await einvoice.parseInvoice(JSON.stringify(invoice), 'json'); await einvoice.validateInvoice(parsed); await einvoice.convertFormat(parsed, 'cii'); if (global.gc) global.gc(); const afterMemory = process.memoryUsage(); const memoryUsed = (afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024; const invoiceSize = JSON.stringify(invoice).length / 1024; // KB results.scalingData.push({ itemCount, invoiceSizeKB: invoiceSize.toFixed(2), memoryUsedMB: memoryUsed.toFixed(2), memoryPerItemKB: ((memoryUsed * 1024) / itemCount).toFixed(2), memoryEfficiency: (invoiceSize / (memoryUsed * 1024)).toFixed(3) }); } // Calculate memory scaling formula (linear regression) if (results.scalingData.length > 2) { const n = results.scalingData.length; const sumX = results.scalingData.reduce((sum, d) => sum + d.itemCount, 0); const sumY = results.scalingData.reduce((sum, d) => sum + parseFloat(d.memoryUsedMB), 0); const sumXY = results.scalingData.reduce((sum, d) => sum + d.itemCount * parseFloat(d.memoryUsedMB), 0); const sumX2 = results.scalingData.reduce((sum, d) => sum + d.itemCount * d.itemCount, 0); const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); const intercept = (sumY - slope * sumX) / n; results.memoryFormula = { slope: slope.toFixed(4), intercept: intercept.toFixed(4), formula: `Memory(MB) = ${slope.toFixed(4)} * items + ${intercept.toFixed(4)}` }; } return results; } ); // Test 3: Memory leak detection const memoryLeakDetection = await performanceTracker.measureAsync( 'memory-leak-detection', async () => { const einvoice = new EInvoice(); const results = { iterations: 100, memorySnapshots: [], leakDetected: false, leakRate: 0 }; // Test invoice for repeated operations const testInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'LEAK-TEST-001', issueDate: '2024-02-10', seller: { name: 'Leak Test Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'Leak Test Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 10 }, (_, i) => ({ description: `Item ${i + 1}`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 } } }; // Take memory snapshots during repeated operations for (let i = 0; i < results.iterations; i++) { if (i % 10 === 0) { if (global.gc) global.gc(); const memory = process.memoryUsage(); results.memorySnapshots.push({ iteration: i, heapUsedMB: memory.heapUsed / 1024 / 1024 }); } // Perform operations that might leak memory const xml = await einvoice.generateXML(testInvoice); const parsed = await einvoice.parseInvoice(xml, 'ubl'); await einvoice.validateInvoice(parsed); await einvoice.convertFormat(parsed, 'cii'); } // Final snapshot if (global.gc) global.gc(); const finalMemory = process.memoryUsage(); results.memorySnapshots.push({ iteration: results.iterations, heapUsedMB: finalMemory.heapUsed / 1024 / 1024 }); // Analyze for memory leaks if (results.memorySnapshots.length > 2) { const firstSnapshot = results.memorySnapshots[0]; const lastSnapshot = results.memorySnapshots[results.memorySnapshots.length - 1]; const memoryIncrease = lastSnapshot.heapUsedMB - firstSnapshot.heapUsedMB; results.leakRate = memoryIncrease / results.iterations; // MB per iteration results.leakDetected = results.leakRate > 0.1; // Threshold: 0.1MB per iteration // Calculate trend const midpoint = Math.floor(results.memorySnapshots.length / 2); const firstHalf = results.memorySnapshots.slice(0, midpoint); const secondHalf = results.memorySnapshots.slice(midpoint); const firstHalfAvg = firstHalf.reduce((sum, s) => sum + s.heapUsedMB, 0) / firstHalf.length; const secondHalfAvg = secondHalf.reduce((sum, s) => sum + s.heapUsedMB, 0) / secondHalf.length; results.trend = { firstHalfAvgMB: firstHalfAvg.toFixed(2), secondHalfAvgMB: secondHalfAvg.toFixed(2), increasing: secondHalfAvg > firstHalfAvg * 1.1 }; } return results; } ); // Test 4: Corpus processing memory profile const corpusMemoryProfile = await performanceTracker.measureAsync( 'corpus-memory-profile', async () => { const files = await corpusLoader.getFilesByPattern('**/*.xml'); const einvoice = new EInvoice(); const results = { filesProcessed: 0, memoryByFormat: new Map(), memoryBySize: { small: { count: 0, avgMemory: 0, total: 0 }, medium: { count: 0, avgMemory: 0, total: 0 }, large: { count: 0, avgMemory: 0, total: 0 } }, peakMemory: 0, totalAllocated: 0 }; // Initial memory state if (global.gc) global.gc(); const startMemory = process.memoryUsage(); // Process sample files const sampleFiles = files.slice(0, 30); for (const file of sampleFiles) { try { const content = await plugins.fs.readFile(file, 'utf-8'); const fileSize = Buffer.byteLength(content, 'utf-8'); const sizeCategory = fileSize < 10240 ? 'small' : fileSize < 102400 ? 'medium' : 'large'; const beforeProcess = process.memoryUsage(); // Process file const format = await einvoice.detectFormat(content); if (!format || format === 'unknown') continue; const invoice = await einvoice.parseInvoice(content, format); await einvoice.validateInvoice(invoice); const afterProcess = process.memoryUsage(); const memoryUsed = (afterProcess.heapUsed - beforeProcess.heapUsed) / 1024 / 1024; // Update statistics results.filesProcessed++; results.totalAllocated += memoryUsed; // By format if (!results.memoryByFormat.has(format)) { results.memoryByFormat.set(format, { count: 0, totalMemory: 0 }); } const formatStats = results.memoryByFormat.get(format)!; formatStats.count++; formatStats.totalMemory += memoryUsed; // By size results.memoryBySize[sizeCategory].count++; results.memoryBySize[sizeCategory].total += memoryUsed; // Track peak if (afterProcess.heapUsed > results.peakMemory) { results.peakMemory = afterProcess.heapUsed; } } catch (error) { // Skip failed files } } // Calculate averages for (const category of Object.keys(results.memoryBySize)) { const stats = results.memoryBySize[category]; if (stats.count > 0) { stats.avgMemory = stats.total / stats.count; } } // Format statistics const formatStats = Array.from(results.memoryByFormat.entries()).map(([format, stats]) => ({ format, count: stats.count, avgMemoryMB: (stats.totalMemory / stats.count).toFixed(2) })); return { filesProcessed: results.filesProcessed, totalAllocatedMB: results.totalAllocated.toFixed(2), peakMemoryMB: ((results.peakMemory - startMemory.heapUsed) / 1024 / 1024).toFixed(2), avgMemoryPerFileMB: (results.totalAllocated / results.filesProcessed).toFixed(2), formatStats, sizeStats: { small: { ...results.memoryBySize.small, avgMemory: results.memoryBySize.small.avgMemory.toFixed(2) }, medium: { ...results.memoryBySize.medium, avgMemory: results.memoryBySize.medium.avgMemory.toFixed(2) }, large: { ...results.memoryBySize.large, avgMemory: results.memoryBySize.large.avgMemory.toFixed(2) } } }; } ); // Test 5: Garbage collection impact const gcImpact = await performanceTracker.measureAsync( 'gc-impact', async () => { const einvoice = new EInvoice(); const results = { withManualGC: { times: [], avgTime: 0 }, withoutGC: { times: [], avgTime: 0 }, gcOverhead: 0 }; // Test invoice const testInvoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: 'GC-TEST-001', issueDate: '2024-02-10', seller: { name: 'GC Test Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'GC Test Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 50 }, (_, i) => ({ description: `Item ${i + 1}`, quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 })), totals: { netAmount: 5000, vatAmount: 500, grossAmount: 5500 } } }; // Test with manual GC if (global.gc) { for (let i = 0; i < 20; i++) { global.gc(); const start = process.hrtime.bigint(); await einvoice.parseInvoice(JSON.stringify(testInvoice), 'json'); await einvoice.validateInvoice(testInvoice); await einvoice.convertFormat(testInvoice, 'cii'); const end = process.hrtime.bigint(); results.withManualGC.times.push(Number(end - start) / 1_000_000); } } // Test without manual GC for (let i = 0; i < 20; i++) { const start = process.hrtime.bigint(); await einvoice.parseInvoice(JSON.stringify(testInvoice), 'json'); await einvoice.validateInvoice(testInvoice); await einvoice.convertFormat(testInvoice, 'cii'); const end = process.hrtime.bigint(); results.withoutGC.times.push(Number(end - start) / 1_000_000); } // Calculate averages if (results.withManualGC.times.length > 0) { results.withManualGC.avgTime = results.withManualGC.times.reduce((a, b) => a + b, 0) / results.withManualGC.times.length; } results.withoutGC.avgTime = results.withoutGC.times.reduce((a, b) => a + b, 0) / results.withoutGC.times.length; if (results.withManualGC.avgTime > 0) { results.gcOverhead = ((results.withManualGC.avgTime - results.withoutGC.avgTime) / results.withoutGC.avgTime * 100); } return results; } ); // Summary t.comment('\n=== PERF-05: Memory Usage Profiling Test Summary ==='); t.comment('\nBaseline Memory Usage:'); baselineMemoryUsage.result.operations.forEach(op => { t.comment(` ${op.name}:`); t.comment(` - Heap before: ${op.heapUsedBefore}MB, after: ${op.heapUsedAfter}MB`); t.comment(` - Heap increase: ${op.heapIncrease}MB`); t.comment(` - RSS increase: ${op.rssIncrease}MB`); }); t.comment('\nMemory Scaling with Invoice Complexity:'); t.comment(' Item Count | Invoice Size | Memory Used | Memory/Item | Efficiency'); t.comment(' -----------|--------------|-------------|-------------|------------'); memoryScaling.result.scalingData.forEach(data => { t.comment(` ${String(data.itemCount).padEnd(10)} | ${data.invoiceSizeKB.padEnd(12)}KB | ${data.memoryUsedMB.padEnd(11)}MB | ${data.memoryPerItemKB.padEnd(11)}KB | ${data.memoryEfficiency}`); }); if (memoryScaling.result.memoryFormula) { t.comment(` Memory scaling formula: ${memoryScaling.result.memoryFormula.formula}`); } t.comment('\nMemory Leak Detection:'); t.comment(` Iterations: ${memoryLeakDetection.result.iterations}`); t.comment(` Leak detected: ${memoryLeakDetection.result.leakDetected ? 'YES ⚠️' : 'NO ✅'}`); t.comment(` Leak rate: ${(memoryLeakDetection.result.leakRate * 1000).toFixed(3)}KB per iteration`); if (memoryLeakDetection.result.trend) { t.comment(` Memory trend: ${memoryLeakDetection.result.trend.increasing ? 'INCREASING ⚠️' : 'STABLE ✅'}`); t.comment(` - First half avg: ${memoryLeakDetection.result.trend.firstHalfAvgMB}MB`); t.comment(` - Second half avg: ${memoryLeakDetection.result.trend.secondHalfAvgMB}MB`); } t.comment('\nCorpus Memory Profile:'); t.comment(` Files processed: ${corpusMemoryProfile.result.filesProcessed}`); t.comment(` Total allocated: ${corpusMemoryProfile.result.totalAllocatedMB}MB`); t.comment(` Peak memory: ${corpusMemoryProfile.result.peakMemoryMB}MB`); t.comment(` Avg per file: ${corpusMemoryProfile.result.avgMemoryPerFileMB}MB`); t.comment(' By format:'); corpusMemoryProfile.result.formatStats.forEach(stat => { t.comment(` - ${stat.format}: ${stat.count} files, avg ${stat.avgMemoryMB}MB`); }); t.comment(' By size:'); ['small', 'medium', 'large'].forEach(size => { const stats = corpusMemoryProfile.result.sizeStats[size]; if (stats.count > 0) { t.comment(` - ${size}: ${stats.count} files, avg ${stats.avgMemory}MB`); } }); t.comment('\nGarbage Collection Impact:'); if (gcImpact.result.withManualGC.avgTime > 0) { t.comment(` With manual GC: ${gcImpact.result.withManualGC.avgTime.toFixed(3)}ms avg`); } t.comment(` Without GC: ${gcImpact.result.withoutGC.avgTime.toFixed(3)}ms avg`); if (gcImpact.result.gcOverhead !== 0) { t.comment(` GC overhead: ${gcImpact.result.gcOverhead.toFixed(1)}%`); } // Performance targets check t.comment('\n=== Performance Targets Check ==='); const avgMemoryPerInvoice = parseFloat(corpusMemoryProfile.result.avgMemoryPerFileMB); const targetMemory = 100; // Target: <100MB per invoice const leakDetected = memoryLeakDetection.result.leakDetected; t.comment(`Memory usage: ${avgMemoryPerInvoice}MB ${avgMemoryPerInvoice < targetMemory ? '✅' : '⚠️'} (target: <${targetMemory}MB per invoice)`); t.comment(`Memory leaks: ${leakDetected ? 'DETECTED ⚠️' : 'NONE ✅'}`); // Overall performance summary t.comment('\n=== Overall Performance Summary ==='); performanceTracker.logSummary(); t.end(); }); tap.start();