/** * @file test.perf-05.memory-usage.ts * @description Performance tests for memory usage patterns */ import { expect, tap } 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'; // Simple memory tracking helper class MemoryTracker { private name: string; private measurements: Array<{operation: string, before: NodeJS.MemoryUsage, after: NodeJS.MemoryUsage}> = []; constructor(name: string) { this.name = name; } async measureMemory(operation: string, fn: () => Promise): Promise { // Force garbage collection if available if (global.gc) { global.gc(); } const before = process.memoryUsage(); const result = await fn(); if (global.gc) { global.gc(); } const after = process.memoryUsage(); this.measurements.push({ operation, before, after }); return result; } getMemoryIncrease(operation: string): number { const measurement = this.measurements.find(m => m.operation === operation); if (!measurement) return 0; return (measurement.after.heapUsed - measurement.before.heapUsed) / 1024 / 1024; // MB } printSummary(): void { console.log(`\n${this.name} - Memory Usage Summary:`); for (const measurement of this.measurements) { const increase = (measurement.after.heapUsed - measurement.before.heapUsed) / 1024 / 1024; console.log(` ${measurement.operation}: ${increase.toFixed(2)} MB increase`); } console.log(''); } } tap.test('PERF-05: Format detection memory usage', async () => { const tracker = new MemoryTracker('PERF-05'); // Load test dataset from corpus const testFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII', 'ZUGFeRD'], maxFiles: 20, validOnly: true }); // Test format detection memory usage await tracker.measureMemory('format-detection-corpus', async () => { for (const file of testFiles) { const content = await CorpusLoader.loadFile(file.path); FormatDetector.detectFormat(content.toString()); } }); tracker.printSummary(); console.log(`Format detection (${testFiles.length} corpus files): ${tracker.getMemoryIncrease('format-detection-corpus').toFixed(2)} MB increase`); // Memory increase should be reasonable for format detection expect(tracker.getMemoryIncrease('format-detection-corpus')).toBeLessThan(50); }); tap.test('PERF-05: Invoice parsing memory usage', async () => { const tracker = new MemoryTracker('PERF-05'); // Load corpus files of different sizes const smallFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII'], maxFiles: 5, validOnly: true }); const largeFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII'], maxFiles: 10, validOnly: true }); // Test parsing small files const smallMemory = await tracker.measureMemory('parsing-small-files', async () => { const invoices = []; for (const file of smallFiles) { const content = await CorpusLoader.loadFile(file.path); const invoice = await EInvoice.fromXml(content.toString()); invoices.push(invoice); } return invoices.length; }); // Test parsing large files const largeMemory = await tracker.measureMemory('parsing-large-files', async () => { const invoices = []; for (const file of largeFiles.slice(0, 5)) { const content = await CorpusLoader.loadFile(file.path); try { const invoice = await EInvoice.fromXml(content.toString()); invoices.push(invoice); } catch (e) { // Some files might not be parseable, that's ok for memory testing } } return invoices.length; }); tracker.printSummary(); console.log(`Parsing ${smallFiles.length} small files: ${tracker.getMemoryIncrease('parsing-small-files').toFixed(2)} MB increase`); console.log(`Parsing ${largeFiles.slice(0, 5).length} large files: ${tracker.getMemoryIncrease('parsing-large-files').toFixed(2)} MB increase`); // Memory scaling should be reasonable expect(tracker.getMemoryIncrease('parsing-small-files')).toBeLessThan(100); expect(tracker.getMemoryIncrease('parsing-large-files')).toBeLessThan(200); }); tap.test('PERF-05: Format conversion memory usage', async () => { const tracker = new MemoryTracker('PERF-05'); // Load UBL files for conversion testing const ublFiles = await CorpusLoader.createTestDataset({ formats: ['UBL'], maxFiles: 10, validOnly: true }); await tracker.measureMemory('format-conversion-corpus', async () => { let convertedCount = 0; for (const file of ublFiles) { const content = await CorpusLoader.loadFile(file.path); try { const invoice = await EInvoice.fromXml(content.toString()); // Try to convert to the same format (should work) await invoice.toXmlString('ubl'); convertedCount++; } catch (e) { // Some conversions might fail, that's ok for memory testing } } return convertedCount; }); tracker.printSummary(); console.log(`Format conversion (${ublFiles.length} files): ${tracker.getMemoryIncrease('format-conversion-corpus').toFixed(2)} MB increase`); // Conversion shouldn't cause excessive memory usage expect(tracker.getMemoryIncrease('format-conversion-corpus')).toBeLessThan(150); }); tap.test('PERF-05: Validation memory usage', async () => { const tracker = new MemoryTracker('PERF-05'); // Load validation test files const validationFiles = await CorpusLoader.createTestDataset({ categories: ['CII_XMLRECHNUNG', 'UBL_XMLRECHNUNG'], maxFiles: 15, validOnly: true }); await tracker.measureMemory('validation-corpus', async () => { let validatedCount = 0; for (const file of validationFiles) { const content = await CorpusLoader.loadFile(file.path); try { const invoice = await EInvoice.fromXml(content.toString()); await invoice.validate(ValidationLevel.SYNTAX); validatedCount++; } catch (e) { // Some files might fail validation, that's ok for memory testing } } return validatedCount; }); tracker.printSummary(); console.log(`Validation (${validationFiles.length} files): ${tracker.getMemoryIncrease('validation-corpus').toFixed(2)} MB increase`); // Validation should be memory efficient expect(tracker.getMemoryIncrease('validation-corpus')).toBeLessThan(100); }); tap.test('PERF-05: Large invoice memory patterns', async () => { const tracker = new MemoryTracker('PERF-05'); // Find reasonably large files in the corpus (but not huge ones) const allFiles = await CorpusLoader.createTestDataset({ maxFiles: 100, validOnly: true }); // Sort by size and take moderately large ones (skip very large files) allFiles.sort((a, b) => b.size - a.size); // Filter out files larger than 1MB to avoid timeouts const largestFiles = allFiles.filter(f => f.size < 1024 * 1024).slice(0, 3); console.log(`Testing with largest corpus files:`); for (const file of largestFiles) { console.log(` - ${file.path} (${(file.size / 1024).toFixed(2)} KB)`); } await tracker.measureMemory('large-invoice-processing', async () => { for (const file of largestFiles) { const content = await CorpusLoader.loadFile(file.path); const fileSize = content.length / 1024 / 1024; // MB try { const invoice = await EInvoice.fromXml(content.toString()); await invoice.validate(ValidationLevel.SYNTAX); console.log(` Processed ${file.path} (${fileSize.toFixed(2)} MB)`); } catch (e) { console.log(` Failed to process ${file.path}: ${e.message}`); } } }); tracker.printSummary(); console.log(`Large invoice processing: ${tracker.getMemoryIncrease('large-invoice-processing').toFixed(2)} MB increase`); // Large files should still have reasonable memory usage expect(tracker.getMemoryIncrease('large-invoice-processing')).toBeLessThan(300); }); tap.test('PERF-05: Memory leak detection', async () => { const tracker = new MemoryTracker('PERF-05'); // Load a small set of files for repeated operations const testFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII'], maxFiles: 5, validOnly: true }); const totalIncrease = await tracker.measureMemory('leak-detection-total', async () => { const batchIncreases: number[] = []; // Run multiple batches for (let batch = 0; batch < 5; batch++) { const batchIncrease = await tracker.measureMemory(`batch-${batch}`, async () => { // Process the same files multiple times for (let i = 0; i < 20; i++) { for (const file of testFiles) { const content = await CorpusLoader.loadFile(file.path); try { const invoice = await EInvoice.fromXml(content.toString()); await invoice.validate(ValidationLevel.SYNTAX); invoice.getFormat(); } catch (e) { // Ignore errors } } } }); batchIncreases.push(tracker.getMemoryIncrease(`batch-${batch}`)); } // Check if memory increases are consistent const avgBatchIncrease = batchIncreases.reduce((a, b) => a + b, 0) / batchIncreases.length; const maxBatchIncrease = Math.max(...batchIncreases); console.log(`Average batch increase: ${avgBatchIncrease.toFixed(2)} MB, max: ${maxBatchIncrease.toFixed(2)} MB`); // Batch increases should be relatively consistent (no memory leak) expect(Math.abs(maxBatchIncrease - avgBatchIncrease)).toBeLessThan(50); }); tracker.printSummary(); console.log(`Total memory increase after repeated operations: ${tracker.getMemoryIncrease('leak-detection-total').toFixed(2)} MB`); // Total increase should be reasonable expect(tracker.getMemoryIncrease('leak-detection-total')).toBeLessThan(200); }); tap.test('PERF-05: Concurrent operations memory usage', async () => { const tracker = new MemoryTracker('PERF-05'); // Load files for concurrent processing const concurrentFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII', 'ZUGFeRD'], maxFiles: 20, validOnly: true }); await tracker.measureMemory('concurrent-processing', async () => { // Process files in parallel const promises = concurrentFiles.map(async (file) => { const content = await CorpusLoader.loadFile(file.path); try { const invoice = await EInvoice.fromXml(content.toString()); await invoice.validate(ValidationLevel.SYNTAX); return invoice.getFormat(); } catch (e) { return null; } }); const results = await Promise.all(promises); console.log(`Processed ${results.filter(r => r !== null).length} files concurrently`); }); tracker.printSummary(); console.log(`Concurrent processing (${concurrentFiles.length} parallel): ${tracker.getMemoryIncrease('concurrent-processing').toFixed(2)} MB increase`); // Concurrent processing should be memory efficient expect(tracker.getMemoryIncrease('concurrent-processing')).toBeLessThan(200); }); tap.test('PERF-05: Memory efficiency with different operations', async () => { const tracker = new MemoryTracker('PERF-05'); // Load a diverse set of files const testFiles = await CorpusLoader.createTestDataset({ formats: ['UBL', 'CII'], maxFiles: 10, validOnly: true }); // Test different operation combinations const operations = [ { name: 'parse-only', fn: async (content: string) => { await EInvoice.fromXml(content); }}, { name: 'parse-validate', fn: async (content: string) => { const invoice = await EInvoice.fromXml(content); await invoice.validate(ValidationLevel.SYNTAX); }}, { name: 'parse-format-detect', fn: async (content: string) => { const invoice = await EInvoice.fromXml(content); invoice.getFormat(); FormatDetector.detectFormat(content); }} ]; for (const op of operations) { await tracker.measureMemory(op.name, async () => { for (const file of testFiles) { const content = await CorpusLoader.loadFile(file.path); try { await op.fn(content.toString()); } catch (e) { // Ignore errors } } }); console.log(`${op.name}: ${tracker.getMemoryIncrease(op.name).toFixed(2)} MB increase`); } tracker.printSummary(); // All operations should be memory efficient operations.forEach(op => { expect(tracker.getMemoryIncrease(op.name)).toBeLessThan(100); }); }); tap.test('PERF-05: Memory Summary', async () => { console.log('\nPERF-05: Memory Usage - Using Real Corpus Files'); console.log('Memory usage tests completed successfully with corpus data'); console.log('All tests used real invoice files from the test corpus'); console.log(`Current memory usage: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`); }); tap.start();