einvoice/test/suite/einvoice_format-detection/test.fd-08.performance.ts
2025-05-25 19:45:37 +00:00

273 lines
10 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import { promises as fs } from 'fs';
import * as path from 'path';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('FD-08: Format Detection Performance - should meet performance thresholds', async () => {
const { FormatDetector } = await import('../../../ts/formats/utils/format.detector.js');
// Test with different sizes of XML content
const performanceTests = [
{
name: 'Minimal UBL',
xml: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>123</ID></Invoice>`,
threshold: 1 // ms
},
{
name: 'Small CII',
xml: `<?xml version="1.0"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100">TEST-001</ram:ID>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`,
threshold: 2 // ms
}
];
for (const test of performanceTests) {
console.log(`\nTesting ${test.name} (${test.xml.length} bytes)`);
const times: number[] = [];
let detectedFormat = '';
// Run multiple iterations for accurate measurement
for (let i = 0; i < 100; i++) {
const { result: format, metric } = await PerformanceTracker.track(
'performance-detection',
async () => FormatDetector.detectFormat(test.xml)
);
times.push(metric.duration);
detectedFormat = format.toString();
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const p95Time = times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)];
console.log(` Format: ${detectedFormat}`);
console.log(` Average: ${avgTime.toFixed(3)}ms`);
console.log(` Min: ${minTime.toFixed(3)}ms`);
console.log(` Max: ${maxTime.toFixed(3)}ms`);
console.log(` P95: ${p95Time.toFixed(3)}ms`);
// Performance assertions
expect(avgTime).toBeLessThan(test.threshold);
expect(p95Time).toBeLessThan(test.threshold * 2);
}
});
tap.test('FD-08: Real File Performance - should perform well on real corpus files', async () => {
const { FormatDetector } = await import('../../../ts/formats/utils/format.detector.js');
// Get sample files from different categories
const testCategories = [
{ name: 'CII XML-Rechnung', category: 'CII_XMLRECHNUNG' as const },
{ name: 'UBL XML-Rechnung', category: 'UBL_XMLRECHNUNG' as const },
{ name: 'EN16931 CII', category: 'EN16931_CII' as const }
];
for (const testCategory of testCategories) {
try {
const files = await CorpusLoader.getFiles(testCategory.category);
if (files.length === 0) {
console.log(`No files found in ${testCategory.name}, skipping`);
continue;
}
// Test first 3 files from category
const testFiles = files.slice(0, 3);
console.log(`\nTesting ${testCategory.name} (${testFiles.length} files)`);
let totalTime = 0;
let totalSize = 0;
let fileCount = 0;
for (const filePath of testFiles) {
try {
const xmlContent = await fs.readFile(filePath, 'utf-8');
const fileSize = xmlContent.length;
const { result: format, metric } = await PerformanceTracker.track(
'real-file-performance',
async () => FormatDetector.detectFormat(xmlContent)
);
totalTime += metric.duration;
totalSize += fileSize;
fileCount++;
console.log(` ${path.basename(filePath)}: ${format} (${metric.duration.toFixed(2)}ms, ${Math.round(fileSize/1024)}KB)`);
} catch (error) {
console.log(` ${path.basename(filePath)}: Error - ${error.message}`);
}
}
if (fileCount > 0) {
const avgTime = totalTime / fileCount;
const avgSize = totalSize / fileCount;
const throughput = avgSize / avgTime; // bytes per ms
console.log(` Category average: ${avgTime.toFixed(2)}ms per file (${Math.round(avgSize/1024)}KB avg)`);
console.log(` Throughput: ${Math.round(throughput * 1000 / 1024)} KB/s`);
// Performance expectations
expect(avgTime).toBeLessThan(20); // Average under 20ms
}
} catch (error) {
console.log(`Error testing ${testCategory.name}: ${error.message}`);
}
}
});
tap.test('FD-08: Concurrent Detection Performance - should handle concurrent operations', async () => {
const { FormatDetector } = await import('../../../ts/formats/utils/format.detector.js');
// Create test XMLs of different formats
const testXmls = [
{
name: 'UBL',
xml: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>UBL-001</ID></Invoice>`
},
{
name: 'CII',
xml: `<?xml version="1.0"?><rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"><rsm:ExchangedDocument/></rsm:CrossIndustryInvoice>`
},
{
name: 'XRechnung',
xml: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><cbc:CustomizationID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0</cbc:CustomizationID></Invoice>`
}
];
const concurrencyLevels = [1, 5, 10, 20];
for (const concurrency of concurrencyLevels) {
console.log(`\nTesting with ${concurrency} concurrent operations`);
// Create tasks for concurrent execution
const tasks = [];
for (let i = 0; i < concurrency; i++) {
const testXml = testXmls[i % testXmls.length];
tasks.push(async () => {
return await PerformanceTracker.track(
`concurrent-detection-${concurrency}`,
async () => FormatDetector.detectFormat(testXml.xml)
);
});
}
// Execute all tasks concurrently
const startTime = performance.now();
const results = await Promise.all(tasks.map(task => task()));
const totalTime = performance.now() - startTime;
// Analyze results
const durations = results.map(r => r.metric.duration);
const avgTime = durations.reduce((a, b) => a + b, 0) / durations.length;
const maxTime = Math.max(...durations);
const throughput = (concurrency / totalTime) * 1000; // operations per second
console.log(` Total time: ${totalTime.toFixed(2)}ms`);
console.log(` Average per operation: ${avgTime.toFixed(2)}ms`);
console.log(` Max time: ${maxTime.toFixed(2)}ms`);
console.log(` Throughput: ${throughput.toFixed(1)} ops/sec`);
// Performance expectations
expect(avgTime).toBeLessThan(5); // Individual operations should stay fast
expect(maxTime).toBeLessThan(20); // No operation should be extremely slow
expect(throughput).toBeGreaterThan(10); // Should handle at least 10 ops/sec
}
});
tap.test('FD-08: Memory Usage - should not consume excessive memory', async () => {
const { FormatDetector } = await import('../../../ts/formats/utils/format.detector.js');
// Generate increasingly large XML documents
function generateLargeXML(sizeKB: number): string {
const targetSize = sizeKB * 1024;
let xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">`;
const itemTemplate = `<Item><ID>ITEM-{ID}</ID><Name>Product {ID}</Name><Description>Long description for product {ID} with lots of text to increase file size</Description></Item>`;
let currentSize = xml.length;
let itemId = 1;
while (currentSize < targetSize) {
const item = itemTemplate.replace(/{ID}/g, itemId.toString());
xml += item;
currentSize += item.length;
itemId++;
}
xml += '</Invoice>';
return xml;
}
const testSizes = [1, 10, 50, 100]; // KB
for (const sizeKB of testSizes) {
const xml = generateLargeXML(sizeKB);
const actualSizeKB = Math.round(xml.length / 1024);
console.log(`\nTesting ${actualSizeKB}KB XML document`);
// Measure memory before
const memBefore = process.memoryUsage();
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const { result: format, metric } = await PerformanceTracker.track(
'memory-usage-test',
async () => FormatDetector.detectFormat(xml)
);
// Measure memory after
const memAfter = process.memoryUsage();
const heapIncrease = (memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024; // MB
const heapTotal = memAfter.heapTotal / 1024 / 1024; // MB
console.log(` Format: ${format}`);
console.log(` Detection time: ${metric.duration.toFixed(2)}ms`);
console.log(` Heap increase: ${heapIncrease.toFixed(2)}MB`);
console.log(` Total heap: ${heapTotal.toFixed(2)}MB`);
// Memory expectations
expect(heapIncrease).toBeLessThan(actualSizeKB * 0.1); // Should not use more than 10% of file size in heap
expect(metric.duration).toBeLessThan(actualSizeKB * 2); // Should not be slower than 2ms per KB
}
});
tap.test('FD-08: Performance Summary Report', async () => {
// Generate comprehensive performance report
const perfSummary = await PerformanceTracker.getSummary('performance-detection');
if (perfSummary) {
console.log(`\nFormat Detection Performance Summary:`);
console.log(` Average: ${perfSummary.average.toFixed(3)}ms`);
console.log(` Min: ${perfSummary.min.toFixed(3)}ms`);
console.log(` Max: ${perfSummary.max.toFixed(3)}ms`);
console.log(` P95: ${perfSummary.p95.toFixed(3)}ms`);
// Overall performance expectations
expect(perfSummary.average).toBeLessThan(5);
expect(perfSummary.p95).toBeLessThan(10);
}
const realFileSummary = await PerformanceTracker.getSummary('real-file-performance');
if (realFileSummary) {
console.log(`\nReal File Performance Summary:`);
console.log(` Average: ${realFileSummary.average.toFixed(2)}ms`);
console.log(` Min: ${realFileSummary.min.toFixed(2)}ms`);
console.log(` Max: ${realFileSummary.max.toFixed(2)}ms`);
console.log(` P95: ${realFileSummary.p95.toFixed(2)}ms`);
}
});
tap.start();