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: `123`, threshold: 1 // ms }, { name: 'Small CII', xml: ` TEST-001 `, 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: `UBL-001` }, { name: 'CII', xml: `` }, { name: 'XRechnung', xml: `urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_3.0` } ]; 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 = ``; const itemTemplate = `ITEM-{ID}Product {ID}Long description for product {ID} with lots of text to increase file size`; 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 += ''; 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();