273 lines
10 KiB
TypeScript
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(); |