einvoice/test/suite/einvoice_performance/test.perf-12.resource-cleanup.ts
2025-05-29 13:35:36 +00:00

709 lines
27 KiB
TypeScript

/**
* @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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoiceData.data.invoiceNumber}</cbc:ID>
<cbc:IssueDate xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoiceData.data.issueDate}</cbc:IssueDate>
</Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoice.data.invoiceNumber}</cbc:ID>
<cbc:IssueDate xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoice.data.issueDate}</cbc:IssueDate>
</Invoice>`;
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 = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>CONCURRENT-${i}</ID></Invoice>`;
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 = '<?xml version="1.0"?><Invoice>' + 'X'.repeat(1024 * 1024) + '</Invoice>';
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 = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>EVENT-${i}</ID></Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoice.data.invoiceNumber}</cbc:ID>
<cbc:IssueDate xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">${invoice.data.issueDate}</cbc:IssueDate>
</Invoice>`;
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();