2025-05-25 19:45:37 +00:00
|
|
|
/**
|
|
|
|
* @file test.perf-05.memory-usage.ts
|
2025-05-29 13:35:36 +00:00
|
|
|
* @description Performance tests for memory usage patterns
|
2025-05-25 19:45:37 +00:00
|
|
|
*/
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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';
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// Simple memory tracking helper
|
|
|
|
class MemoryTracker {
|
|
|
|
private name: string;
|
|
|
|
private measurements: Array<{operation: string, before: NodeJS.MemoryUsage, after: NodeJS.MemoryUsage}> = [];
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
constructor(name: string) {
|
|
|
|
this.name = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
async measureMemory<T>(operation: string, fn: () => Promise<T>): Promise<T> {
|
|
|
|
// Force garbage collection if available
|
|
|
|
if (global.gc) {
|
|
|
|
global.gc();
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
|
|
|
|
const before = process.memoryUsage();
|
|
|
|
const result = await fn();
|
|
|
|
|
|
|
|
if (global.gc) {
|
|
|
|
global.gc();
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
|
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
}
|
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
}
|
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
for (let i = 0; i < 20; i++) {
|
2025-05-29 13:35:36 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
});
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
batchIncreases.push(tracker.getMemoryIncrease(`batch-${batch}`));
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
});
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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`);
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
tracker.printSummary();
|
|
|
|
console.log(`Concurrent processing (${concurrentFiles.length} parallel): ${tracker.getMemoryIncrease('concurrent-processing').toFixed(2)} MB increase`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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);
|
|
|
|
}}
|
|
|
|
];
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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`);
|
|
|
|
}
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
tracker.printSummary();
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// All operations should be memory efficient
|
|
|
|
operations.forEach(op => {
|
|
|
|
expect(tracker.getMemoryIncrease(op.name)).toBeLessThan(100);
|
|
|
|
});
|
|
|
|
});
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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`);
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
tap.start();
|