einvoice/test/suite/einvoice_performance/test.perf-05.memory-usage.ts

379 lines
13 KiB
TypeScript
Raw Normal View History

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();