2025-05-25 19:45:37 +00:00
|
|
|
/**
|
|
|
|
* @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';
|
2025-05-29 13:35:36 +00:00
|
|
|
import { EInvoice, FormatDetector } from '../../../ts/index.js';
|
2025-05-25 19:45:37 +00:00
|
|
|
import { CorpusLoader } from '../../suite/corpus.loader.js';
|
|
|
|
import { PerformanceTracker } from '../../suite/performance.tracker.js';
|
|
|
|
import * as os from 'os';
|
2025-05-29 13:35:36 +00:00
|
|
|
import { EventEmitter } from 'events';
|
|
|
|
import { execSync } from 'child_process';
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
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 }
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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 }
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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>`;
|
2025-05-25 19:45:37 +00:00
|
|
|
xmlBuffers.push(Buffer.from(xml));
|
|
|
|
|
|
|
|
// Parse it back
|
2025-05-29 13:35:36 +00:00
|
|
|
await EInvoice.fromXml(xml);
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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>`;
|
2025-05-29 13:35:36 +00:00
|
|
|
const format = FormatDetector.detectFormat(xml);
|
|
|
|
const parsed = await EInvoice.fromXml(xml);
|
|
|
|
await parsed.validate();
|
2025-05-25 19:45:37 +00:00
|
|
|
})());
|
|
|
|
}
|
|
|
|
|
|
|
|
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 () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
const files = await CorpusLoader.loadPattern('**/*.xml');
|
2025-05-25 19:45:37 +00:00
|
|
|
const sampleFiles = files.slice(0, 20);
|
|
|
|
|
|
|
|
for (const file of sampleFiles) {
|
2025-05-29 13:35:36 +00:00
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Concurrent file operations',
|
|
|
|
fn: async () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
const files = await CorpusLoader.loadPattern('**/*.xml');
|
2025-05-25 19:45:37 +00:00
|
|
|
const sampleFiles = files.slice(0, 20);
|
|
|
|
|
|
|
|
await Promise.all(sampleFiles.map(async (file) => {
|
2025-05-29 13:35:36 +00:00
|
|
|
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
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
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 = () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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(() => {});
|
2025-05-25 19:45:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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 }
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
// 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();
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
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 () => {
|
2025-05-29 13:35:36 +00:00
|
|
|
const files = await CorpusLoader.loadPattern('**/*.xml');
|
2025-05-25 19:45:37 +00:00
|
|
|
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 {
|
2025-05-29 13:35:36 +00:00
|
|
|
const content = await plugins.fs.readFile(file.path, 'utf-8');
|
|
|
|
const format = FormatDetector.detectFormat(content);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
if (format && format !== 'unknown') {
|
2025-05-29 13:35:36 +00:00
|
|
|
const invoice = await EInvoice.fromXml(content);
|
|
|
|
await invoice.validate();
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
// Heavy processing for middle phase
|
|
|
|
if (phase.name === 'Heavy processing') {
|
2025-05-29 13:35:36 +00:00
|
|
|
try {
|
|
|
|
await invoice.toXmlString('cii');
|
|
|
|
} catch (error) {
|
|
|
|
// Expected for incomplete test invoices
|
|
|
|
}
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('\n=== PERF-12: Resource Cleanup Test Summary ===');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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`);
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
2025-05-29 13:35:36 +00:00
|
|
|
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 ✅'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(` Handle leaks detected: ${fileHandleCleanup.handleLeaks ? 'YES ⚠️' : 'NO ✅'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('\nEvent Listener Cleanup:');
|
|
|
|
eventListenerCleanup.listenerTests.forEach(test => {
|
|
|
|
console.log(` ${test.name}:`);
|
2025-05-25 19:45:37 +00:00
|
|
|
if (test.listenersAdded !== undefined) {
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(` - Added: ${test.listenersAdded}, Remaining: ${test.listenersRemaining}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
if (test.memoryAddedMB !== undefined) {
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(` - Memory added: ${test.memoryAddedMB}MB, Freed: ${test.memoryFreedMB}MB`);
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
|
|
|
});
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(` Memory leaks in listeners: ${eventListenerCleanup.memoryLeaks ? 'YES ⚠️' : 'NO ✅'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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 ✅'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
}
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(` Memory stabilized: ${longRunningCleanup.stabilized ? 'YES ✅' : 'NO ⚠️'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
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}%`);
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
2025-05-29 13:35:36 +00:00
|
|
|
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 ⚠️'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
// Performance targets check
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('\n=== Performance Targets Check ===');
|
|
|
|
const memoryRecoveryRate = parseFloat(memoryCleanup.cleanupEfficiency.overallRecoveryRate);
|
2025-05-25 19:45:37 +00:00
|
|
|
const targetRecoveryRate = 80; // Target: >80% memory recovery
|
2025-05-29 13:35:36 +00:00
|
|
|
const noMemoryLeaks = !memoryCleanup.cleanupEfficiency.memoryLeakDetected &&
|
|
|
|
!fileHandleCleanup.handleLeaks &&
|
|
|
|
!eventListenerCleanup.memoryLeaks &&
|
|
|
|
longRunningCleanup.stabilized;
|
2025-05-25 19:45:37 +00:00
|
|
|
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log(`Memory recovery rate: ${memoryRecoveryRate}% ${memoryRecoveryRate > targetRecoveryRate ? '✅' : '⚠️'} (target: >${targetRecoveryRate}%)`);
|
|
|
|
console.log(`Resource leak prevention: ${noMemoryLeaks ? 'PASSED ✅' : 'FAILED ⚠️'}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
|
|
// Overall performance summary
|
2025-05-29 13:35:36 +00:00
|
|
|
console.log('\n=== Overall Performance Summary ===');
|
|
|
|
performanceTracker.getSummary();
|
2025-05-25 19:45:37 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
tap.start();
|