/** * @file test.perf-12.resource-cleanup.ts * @description Performance tests for resource cleanup and management */ import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../../plugins.js'; import { EInvoice, FormatDetector } from '../../../ts/index.js'; import { CorpusLoader } from '../../suite/corpus.loader.js'; import { PerformanceTracker } from '../../suite/performance.tracker.js'; import * as os from 'os'; import { EventEmitter } from 'events'; import { execSync } from 'child_process'; const performanceTracker = new PerformanceTracker('PERF-12: Resource Cleanup'); tap.test('PERF-12: Resource Cleanup - should properly manage and cleanup resources', async (t) => { // Test 1: Memory cleanup after operations const memoryCleanup = await performanceTracker.measureAsync( 'memory-cleanup-after-operations', async () => { const results = { operations: [], cleanupEfficiency: null }; // Force initial GC to get baseline if (global.gc) global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); const baselineMemory = process.memoryUsage(); // Test operations const operations = [ { name: 'Large invoice processing', fn: async () => { const largeInvoices = Array.from({ length: 100 }, (_, i) => ({ format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: `CLEANUP-${i}`, issueDate: '2024-03-15', seller: { name: 'Large Data Seller ' + 'X'.repeat(1000), address: 'Long Address ' + 'Y'.repeat(1000), country: 'US', taxId: 'US123456789' }, buyer: { name: 'Large Data Buyer ' + 'Z'.repeat(1000), address: 'Long Address ' + 'W'.repeat(1000), country: 'US', taxId: 'US987654321' }, items: Array.from({ length: 50 }, (_, j) => ({ description: `Item ${j} with very long description `.repeat(20), quantity: Math.random() * 100, unitPrice: Math.random() * 1000, vatRate: 19, lineTotal: 0 })), totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 } } })); // Process all invoices - for resource testing, we'll create XML and parse it for (const invoiceData of largeInvoices) { // Create a simple UBL XML from the data const xml = ` ${invoiceData.data.invoiceNumber} ${invoiceData.data.issueDate} `; const invoice = await EInvoice.fromXml(xml); const validation = await invoice.validate(); // Skip conversion since it requires full data - this is a resource test } } }, { name: 'XML generation and parsing', fn: async () => { const xmlBuffers = []; for (let i = 0; i < 50; i++) { const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: `XML-GEN-${i}`, issueDate: '2024-03-15', seller: { name: 'XML Seller', address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: 'XML Buyer', address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 100 }, (_, j) => ({ description: `XML Item ${j}`, quantity: 1, unitPrice: 100, vatRate: 19, lineTotal: 100 })), totals: { netAmount: 10000, vatAmount: 1900, grossAmount: 11900 } } }; // For resource testing, create a simple XML const xml = ` ${invoice.data.invoiceNumber} ${invoice.data.issueDate} `; xmlBuffers.push(Buffer.from(xml)); // Parse it back await EInvoice.fromXml(xml); } } }, { name: 'Concurrent operations', fn: async () => { const promises = []; for (let i = 0; i < 200; i++) { promises.push((async () => { const xml = `CONCURRENT-${i}`; const format = FormatDetector.detectFormat(xml); const parsed = await EInvoice.fromXml(xml); await parsed.validate(); })()); } await Promise.all(promises); } } ]; // Execute operations and measure cleanup for (const operation of operations) { // Memory before operation if (global.gc) global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); const beforeOperation = process.memoryUsage(); // Execute operation await operation.fn(); // Memory after operation (before cleanup) const afterOperation = process.memoryUsage(); // Force cleanup if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); } // Memory after cleanup const afterCleanup = process.memoryUsage(); const memoryUsed = (afterOperation.heapUsed - beforeOperation.heapUsed) / 1024 / 1024; const memoryRecovered = (afterOperation.heapUsed - afterCleanup.heapUsed) / 1024 / 1024; const recoveryRate = memoryUsed > 0 ? (memoryRecovered / memoryUsed * 100) : 0; results.operations.push({ name: operation.name, memoryUsedMB: memoryUsed.toFixed(2), memoryRecoveredMB: memoryRecovered.toFixed(2), recoveryRate: recoveryRate.toFixed(2), finalMemoryMB: ((afterCleanup.heapUsed - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2), externalMemoryMB: ((afterCleanup.external - baselineMemory.external) / 1024 / 1024).toFixed(2) }); } // Overall cleanup efficiency const totalUsed = results.operations.reduce((sum, op) => sum + parseFloat(op.memoryUsedMB), 0); const totalRecovered = results.operations.reduce((sum, op) => sum + parseFloat(op.memoryRecoveredMB), 0); results.cleanupEfficiency = { totalMemoryUsedMB: totalUsed.toFixed(2), totalMemoryRecoveredMB: totalRecovered.toFixed(2), overallRecoveryRate: totalUsed > 0 ? (totalRecovered / totalUsed * 100).toFixed(2) : '0', memoryLeakDetected: results.operations.some(op => parseFloat(op.finalMemoryMB) > 10) }; return results; } ); // Test 2: File handle cleanup const fileHandleCleanup = await performanceTracker.measureAsync( 'file-handle-cleanup', async () => { const results = { tests: [], handleLeaks: false }; // Monitor file handles (platform-specific) const getOpenFiles = () => { try { if (process.platform === 'linux') { const pid = process.pid; const output = execSync(`ls /proc/${pid}/fd 2>/dev/null | wc -l`).toString(); return parseInt(output.trim()); } return -1; // Not supported on this platform } catch { return -1; } }; const initialHandles = getOpenFiles(); // Test scenarios const scenarios = [ { name: 'Sequential file operations', fn: async () => { const files = await CorpusLoader.loadPattern('**/*.xml'); const sampleFiles = files.slice(0, 20); for (const file of sampleFiles) { try { const fullPath = plugins.path.join(process.cwd(), 'test/assets/corpus', file.path); const content = await plugins.fs.readFile(fullPath, 'utf-8'); const format = FormatDetector.detectFormat(content); if (format && format !== 'unknown') { const invoice = await EInvoice.fromXml(content); await invoice.validate(); } } catch (error) { // Skip files that can't be read } } } }, { name: 'Concurrent file operations', fn: async () => { const files = await CorpusLoader.loadPattern('**/*.xml'); const sampleFiles = files.slice(0, 20); await Promise.all(sampleFiles.map(async (file) => { try { const fullPath = plugins.path.join(process.cwd(), 'test/assets/corpus', file.path); const content = await plugins.fs.readFile(fullPath, 'utf-8'); const format = FormatDetector.detectFormat(content); if (format && format !== 'unknown') { const invoice = await EInvoice.fromXml(content); await invoice.validate(); } } catch (error) { // Skip files that can't be read } })); } }, { name: 'Large file streaming', fn: async () => { // Create temporary large file const tempFile = '/tmp/einvoice-test-large.xml'; const largeContent = '' + 'X'.repeat(1024 * 1024) + ''; await plugins.fs.writeFile(tempFile, largeContent); try { // Read in chunks const chunkSize = 64 * 1024; const fd = await plugins.fs.open(tempFile, 'r'); const buffer = Buffer.alloc(chunkSize); let position = 0; while (true) { const { bytesRead } = await fd.read(buffer, 0, chunkSize, position); if (bytesRead === 0) break; position += bytesRead; } await fd.close(); } finally { // Cleanup await plugins.fs.unlink(tempFile).catch(() => {}); } } } ]; // Execute scenarios for (const scenario of scenarios) { const beforeHandles = getOpenFiles(); await scenario.fn(); // Allow time for handle cleanup await new Promise(resolve => setTimeout(resolve, 100)); const afterHandles = getOpenFiles(); results.tests.push({ name: scenario.name, beforeHandles: beforeHandles === -1 ? 'N/A' : beforeHandles, afterHandles: afterHandles === -1 ? 'N/A' : afterHandles, handleIncrease: beforeHandles === -1 || afterHandles === -1 ? 'N/A' : afterHandles - beforeHandles }); } // Check for handle leaks const finalHandles = getOpenFiles(); if (initialHandles !== -1 && finalHandles !== -1) { results.handleLeaks = finalHandles > initialHandles + 5; // Allow small variance } return results; } ); // Test 3: Event listener cleanup const eventListenerCleanup = await performanceTracker.measureAsync( 'event-listener-cleanup', async () => { const results = { listenerTests: [], memoryLeaks: false }; // Test event emitter scenarios const scenarios = [ { name: 'Proper listener removal', fn: async () => { const emitter = new EventEmitter(); const listeners = []; // Add listeners for (let i = 0; i < 100; i++) { const listener = () => { // Process invoice event - for resource testing, just simulate work const xml = `EVENT-${i}`; EInvoice.fromXml(xml).then(inv => inv.validate()).catch(() => {}); }; listeners.push(listener); emitter.on('invoice', listener); } // Trigger events for (let i = 0; i < 10; i++) { emitter.emit('invoice'); } // Remove listeners for (const listener of listeners) { emitter.removeListener('invoice', listener); } return { listenersAdded: listeners.length, listenersRemaining: emitter.listenerCount('invoice') }; } }, { name: 'Once listeners', fn: async () => { const emitter = new EventEmitter(); let triggeredCount = 0; // Add once listeners for (let i = 0; i < 100; i++) { emitter.once('process', () => { triggeredCount++; }); } // Trigger event emitter.emit('process'); return { listenersAdded: 100, triggered: triggeredCount, listenersRemaining: emitter.listenerCount('process') }; } }, { name: 'Memory pressure with listeners', fn: async () => { const emitter = new EventEmitter(); const startMemory = process.memoryUsage().heapUsed; // Add many listeners with closures for (let i = 0; i < 1000; i++) { const largeData = Buffer.alloc(1024); // 1KB per listener emitter.on('data', () => { // Closure captures largeData return largeData.length; }); } const afterAddMemory = process.memoryUsage().heapUsed; // Remove all listeners emitter.removeAllListeners('data'); // Force GC if (global.gc) global.gc(); await new Promise(resolve => setTimeout(resolve, 100)); const afterRemoveMemory = process.memoryUsage().heapUsed; return { memoryAddedMB: ((afterAddMemory - startMemory) / 1024 / 1024).toFixed(2), memoryFreedMB: ((afterAddMemory - afterRemoveMemory) / 1024 / 1024).toFixed(2), listenersRemaining: emitter.listenerCount('data') }; } } ]; // Execute scenarios for (const scenario of scenarios) { const result = await scenario.fn(); results.listenerTests.push({ name: scenario.name, ...result }); } // Check for memory leaks const memoryTest = results.listenerTests.find(t => t.name === 'Memory pressure with listeners'); if (memoryTest) { const freed = parseFloat(memoryTest.memoryFreedMB); const added = parseFloat(memoryTest.memoryAddedMB); results.memoryLeaks = freed < added * 0.8; // Should free at least 80% } return results; } ); // Test 4: Long-running operation cleanup const longRunningCleanup = await performanceTracker.measureAsync( 'long-running-cleanup', async () => { const results = { iterations: 0, memorySnapshots: [], stabilized: false, trend: null }; // Simulate long-running process const testDuration = 10000; // 10 seconds const snapshotInterval = 1000; // Every second const startTime = Date.now(); const startMemory = process.memoryUsage(); let iteration = 0; const snapshotTimer = setInterval(() => { const memory = process.memoryUsage(); results.memorySnapshots.push({ time: Date.now() - startTime, heapUsedMB: (memory.heapUsed / 1024 / 1024).toFixed(2), externalMB: (memory.external / 1024 / 1024).toFixed(2), iteration }); }, snapshotInterval); // Continuous operations while (Date.now() - startTime < testDuration) { // Create and process invoice const invoice = { format: 'ubl' as const, data: { documentType: 'INVOICE', invoiceNumber: `LONG-RUN-${iteration}`, issueDate: '2024-03-15', seller: { name: `Seller ${iteration}`, address: 'Address', country: 'US', taxId: 'US123' }, buyer: { name: `Buyer ${iteration}`, address: 'Address', country: 'US', taxId: 'US456' }, items: Array.from({ length: 10 }, (_, i) => ({ description: `Item ${i}`, quantity: 1, unitPrice: 100, vatRate: 19, lineTotal: 100 })), totals: { netAmount: 1000, vatAmount: 190, grossAmount: 1190 } } }; // For resource testing, create and validate an invoice const xml = ` ${invoice.data.invoiceNumber} ${invoice.data.issueDate} `; const inv = await EInvoice.fromXml(xml); await inv.validate(); iteration++; results.iterations = iteration; // Periodic cleanup if (iteration % 50 === 0 && global.gc) { global.gc(); } // Small delay to prevent CPU saturation await new Promise(resolve => setTimeout(resolve, 10)); } clearInterval(snapshotTimer); // Analyze memory trend if (results.memorySnapshots.length >= 5) { const firstHalf = results.memorySnapshots.slice(0, Math.floor(results.memorySnapshots.length / 2)); const secondHalf = results.memorySnapshots.slice(Math.floor(results.memorySnapshots.length / 2)); const avgFirstHalf = firstHalf.reduce((sum, s) => sum + parseFloat(s.heapUsedMB), 0) / firstHalf.length; const avgSecondHalf = secondHalf.reduce((sum, s) => sum + parseFloat(s.heapUsedMB), 0) / secondHalf.length; results.trend = { firstHalfAvgMB: avgFirstHalf.toFixed(2), secondHalfAvgMB: avgSecondHalf.toFixed(2), increasing: avgSecondHalf > avgFirstHalf * 1.1, stable: Math.abs(avgSecondHalf - avgFirstHalf) < avgFirstHalf * 0.1 }; results.stabilized = results.trend.stable; } return results; } ); // Test 5: Corpus cleanup verification const corpusCleanupVerification = await performanceTracker.measureAsync( 'corpus-cleanup-verification', async () => { const files = await CorpusLoader.loadPattern('**/*.xml'); const results = { phases: [], overallCleanup: null }; // Process corpus in phases const phases = [ { name: 'Initial batch', count: 50 }, { name: 'Heavy processing', count: 100 }, { name: 'Final batch', count: 50 } ]; if (global.gc) global.gc(); const initialMemory = process.memoryUsage(); for (const phase of phases) { const phaseStart = process.memoryUsage(); const startTime = Date.now(); // Process files const phaseFiles = files.slice(0, phase.count); let processed = 0; let errors = 0; for (const file of phaseFiles) { try { const content = await plugins.fs.readFile(file.path, 'utf-8'); const format = FormatDetector.detectFormat(content); if (format && format !== 'unknown') { const invoice = await EInvoice.fromXml(content); await invoice.validate(); // Heavy processing for middle phase if (phase.name === 'Heavy processing') { try { await invoice.toXmlString('cii'); } catch (error) { // Expected for incomplete test invoices } } processed++; } } catch (error) { errors++; } } const phaseEnd = process.memoryUsage(); // Cleanup between phases if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 200)); } const afterCleanup = process.memoryUsage(); results.phases.push({ name: phase.name, filesProcessed: processed, errors, duration: Date.now() - startTime, memoryUsedMB: ((phaseEnd.heapUsed - phaseStart.heapUsed) / 1024 / 1024).toFixed(2), memoryAfterCleanupMB: ((afterCleanup.heapUsed - phaseStart.heapUsed) / 1024 / 1024).toFixed(2), cleanupEfficiency: ((phaseEnd.heapUsed - afterCleanup.heapUsed) / (phaseEnd.heapUsed - phaseStart.heapUsed) * 100).toFixed(2) }); } // Final cleanup if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 500)); } const finalMemory = process.memoryUsage(); results.overallCleanup = { initialMemoryMB: (initialMemory.heapUsed / 1024 / 1024).toFixed(2), finalMemoryMB: (finalMemory.heapUsed / 1024 / 1024).toFixed(2), totalIncreaseMB: ((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024).toFixed(2), acceptableIncrease: (finalMemory.heapUsed - initialMemory.heapUsed) < 50 * 1024 * 1024 // Less than 50MB }; return results; } ); // Summary console.log('\n=== PERF-12: Resource Cleanup Test Summary ==='); console.log('\nMemory Cleanup After Operations:'); console.log(' Operation | Used | Recovered | Recovery % | Final | External'); console.log(' -------------------------|---------|-----------|------------|---------|----------'); memoryCleanup.operations.forEach(op => { console.log(` ${op.name.padEnd(24)} | ${op.memoryUsedMB.padEnd(7)}MB | ${op.memoryRecoveredMB.padEnd(9)}MB | ${op.recoveryRate.padEnd(10)}% | ${op.finalMemoryMB.padEnd(7)}MB | ${op.externalMemoryMB}MB`); }); console.log(` Overall efficiency:`); console.log(` - Total used: ${memoryCleanup.cleanupEfficiency.totalMemoryUsedMB}MB`); console.log(` - Total recovered: ${memoryCleanup.cleanupEfficiency.totalMemoryRecoveredMB}MB`); console.log(` - Recovery rate: ${memoryCleanup.cleanupEfficiency.overallRecoveryRate}%`); console.log(` - Memory leak detected: ${memoryCleanup.cleanupEfficiency.memoryLeakDetected ? 'YES ⚠️' : 'NO ✅'}`); console.log('\nFile Handle Cleanup:'); fileHandleCleanup.tests.forEach(test => { console.log(` ${test.name}:`); console.log(` - Before: ${test.beforeHandles}, After: ${test.afterHandles}`); console.log(` - Handle increase: ${test.handleIncrease}`); }); console.log(` Handle leaks detected: ${fileHandleCleanup.handleLeaks ? 'YES ⚠️' : 'NO ✅'}`); console.log('\nEvent Listener Cleanup:'); eventListenerCleanup.listenerTests.forEach(test => { console.log(` ${test.name}:`); if (test.listenersAdded !== undefined) { console.log(` - Added: ${test.listenersAdded}, Remaining: ${test.listenersRemaining}`); } if (test.memoryAddedMB !== undefined) { console.log(` - Memory added: ${test.memoryAddedMB}MB, Freed: ${test.memoryFreedMB}MB`); } }); console.log(` Memory leaks in listeners: ${eventListenerCleanup.memoryLeaks ? 'YES ⚠️' : 'NO ✅'}`); console.log('\nLong-Running Operation Cleanup:'); console.log(` Iterations: ${longRunningCleanup.iterations}`); console.log(` Memory snapshots: ${longRunningCleanup.memorySnapshots.length}`); if (longRunningCleanup.trend) { console.log(` Memory trend:`); console.log(` - First half avg: ${longRunningCleanup.trend.firstHalfAvgMB}MB`); console.log(` - Second half avg: ${longRunningCleanup.trend.secondHalfAvgMB}MB`); console.log(` - Trend: ${longRunningCleanup.trend.increasing ? 'INCREASING ⚠️' : longRunningCleanup.trend.stable ? 'STABLE ✅' : 'DECREASING ✅'}`); } console.log(` Memory stabilized: ${longRunningCleanup.stabilized ? 'YES ✅' : 'NO ⚠️'}`); console.log('\nCorpus Cleanup Verification:'); console.log(' Phase | Files | Duration | Memory Used | After Cleanup | Efficiency'); console.log(' -------------------|-------|----------|-------------|---------------|------------'); corpusCleanupVerification.phases.forEach(phase => { console.log(` ${phase.name.padEnd(18)} | ${String(phase.filesProcessed).padEnd(5)} | ${String(phase.duration + 'ms').padEnd(8)} | ${phase.memoryUsedMB.padEnd(11)}MB | ${phase.memoryAfterCleanupMB.padEnd(13)}MB | ${phase.cleanupEfficiency}%`); }); console.log(` Overall cleanup:`); console.log(` - Initial memory: ${corpusCleanupVerification.overallCleanup.initialMemoryMB}MB`); console.log(` - Final memory: ${corpusCleanupVerification.overallCleanup.finalMemoryMB}MB`); console.log(` - Total increase: ${corpusCleanupVerification.overallCleanup.totalIncreaseMB}MB`); console.log(` - Acceptable increase: ${corpusCleanupVerification.overallCleanup.acceptableIncrease ? 'YES ✅' : 'NO ⚠️'}`); // Performance targets check console.log('\n=== Performance Targets Check ==='); const memoryRecoveryRate = parseFloat(memoryCleanup.cleanupEfficiency.overallRecoveryRate); const targetRecoveryRate = 80; // Target: >80% memory recovery const noMemoryLeaks = !memoryCleanup.cleanupEfficiency.memoryLeakDetected && !fileHandleCleanup.handleLeaks && !eventListenerCleanup.memoryLeaks && longRunningCleanup.stabilized; console.log(`Memory recovery rate: ${memoryRecoveryRate}% ${memoryRecoveryRate > targetRecoveryRate ? '✅' : '⚠️'} (target: >${targetRecoveryRate}%)`); console.log(`Resource leak prevention: ${noMemoryLeaks ? 'PASSED ✅' : 'FAILED ⚠️'}`); // Overall performance summary console.log('\n=== Overall Performance Summary ==='); performanceTracker.getSummary(); }); tap.start();