update
This commit is contained in:
273
test/suite/einvoice_format-detection/test.fd-08.performance.ts
Normal file
273
test/suite/einvoice_format-detection/test.fd-08.performance.ts
Normal file
@ -0,0 +1,273 @@
|
||||
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();
|
Reference in New Issue
Block a user