import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { rgb } from 'pdf-lib'; // Simple instance-based performance tracker for this test class SimplePerformanceTracker { private measurements: Map = new Map(); private name: string; constructor(name: string) { this.name = name; } addMeasurement(key: string, time: number): void { if (!this.measurements.has(key)) { this.measurements.set(key, []); } this.measurements.get(key)!.push(time); } getAverageTime(): number { let total = 0; let count = 0; for (const times of this.measurements.values()) { for (const time of times) { total += time; count++; } } return count > 0 ? total / count : 0; } printSummary(): void { console.log(`\n${this.name} - Performance Summary:`); for (const [key, times] of this.measurements) { const avg = times.reduce((a, b) => a + b, 0) / times.length; const min = Math.min(...times); const max = Math.max(...times); console.log(` ${key}: avg=${avg.toFixed(2)}ms, min=${min.toFixed(2)}ms, max=${max.toFixed(2)}ms (${times.length} runs)`); } console.log(` Overall average: ${this.getAverageTime().toFixed(2)}ms`); } } const performanceTracker = new SimplePerformanceTracker('PDF-08: Large PDF Performance'); tap.test('PDF-08: Process PDFs of increasing size', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; // Test different PDF sizes const sizes = [ { pages: 1, name: '1-page', expectedTime: 1000 }, { pages: 10, name: '10-page', expectedTime: 2000 }, { pages: 50, name: '50-page', expectedTime: 5000 }, { pages: 100, name: '100-page', expectedTime: 10000 } ]; for (const sizeTest of sizes) { const sizeStartTime = performance.now(); const pdfDoc = await PDFDocument.create(); // Create multiple pages for (let i = 0; i < sizeTest.pages; i++) { const page = pdfDoc.addPage([595, 842]); // A4 // Add content to each page page.drawText(`Invoice Page ${i + 1} of ${sizeTest.pages}`, { x: 50, y: 750, size: 20 }); // Add some graphics to increase file size page.drawRectangle({ x: 50, y: 600, width: 495, height: 100, borderColor: rgb(0, 0, 0), borderWidth: 1 }); // Add text content for (let j = 0; j < 20; j++) { page.drawText(`Line item ${j + 1}: Product description with details`, { x: 60, y: 580 - (j * 20), size: 10 }); } } // Add a simple but valid UBL invoice XML const xmlContent = ` LARGE-PDF-${sizeTest.name} 2025-01-25 380 EUR Test Supplier Berlin 10115 DE Test Customer Munich 80331 DE 100.00 1 1 100.00 Test item for ${sizeTest.pages} page PDF 100.00 `; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: `Invoice for ${sizeTest.pages} page document` } ); const pdfBytes = await pdfDoc.save(); const sizeMB = (pdfBytes.length / 1024 / 1024).toFixed(2); // Test extraction performance const extractStartTime = performance.now(); try { const einvoice = await EInvoice.fromPdf(pdfBytes); const xmlString = await einvoice.toXmlString('ubl'); expect(xmlString).toContain(`LARGE-PDF-${sizeTest.name}`); const extractTime = performance.now() - extractStartTime; console.log(`${sizeTest.name} (${sizeMB} MB): Extraction took ${extractTime.toFixed(2)}ms`); // Check if extraction time is reasonable expect(extractTime).toBeLessThan(sizeTest.expectedTime); } catch (error) { console.log(`${sizeTest.name} extraction error:`, error.message); } const sizeElapsed = performance.now() - sizeStartTime; performanceTracker.addMeasurement(`size-${sizeTest.name}`, sizeElapsed); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('increasing-sizes', elapsed); }); tap.test('PDF-08: Memory usage with large PDFs', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); // Monitor memory usage const initialMemory = process.memoryUsage(); console.log('Initial memory (MB):', { rss: (initialMemory.rss / 1024 / 1024).toFixed(2), heapUsed: (initialMemory.heapUsed / 1024 / 1024).toFixed(2) }); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // Create a large PDF with many objects const pageCount = 200; for (let i = 0; i < pageCount; i++) { const page = pdfDoc.addPage(); // Add many small objects to increase complexity for (let j = 0; j < 50; j++) { page.drawText(`Item ${i}-${j}`, { x: 50 + (j % 10) * 50, y: 700 - Math.floor(j / 10) * 20, size: 8 }); } } // Add large but valid UBL XML attachment let xmlContent = ` LARGE-MEMORY-TEST 2025-01-25 380 EUR Test Supplier Berlin 10115 DE Test Customer Munich 80331 DE ${1000 * 99.99} `; // Add many line items to increase file size for (let i = 0; i < 1000; i++) { xmlContent += ` ${i + 1} 10 999.90 Product item ${i} with long description text that increases file size 99.99 `; } xmlContent += '\n'; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'large-invoice.xml', { mimeType: 'application/xml', description: 'Large invoice with many line items' } ); const pdfBytes = await pdfDoc.save(); const sizeMB = (pdfBytes.length / 1024 / 1024).toFixed(2); console.log(`Created large PDF: ${sizeMB} MB`); // Test memory usage during processing const einvoice = await EInvoice.fromPdf(pdfBytes); const afterMemory = process.memoryUsage(); console.log('After processing memory (MB):', { rss: (afterMemory.rss / 1024 / 1024).toFixed(2), heapUsed: (afterMemory.heapUsed / 1024 / 1024).toFixed(2) }); const memoryIncrease = afterMemory.heapUsed - initialMemory.heapUsed; console.log(`Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`); // Force garbage collection if available if (global.gc) { global.gc(); const gcMemory = process.memoryUsage(); console.log('After GC memory (MB):', { heapUsed: (gcMemory.heapUsed / 1024 / 1024).toFixed(2) }); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('memory-usage', elapsed); }); tap.test('PDF-08: Streaming vs loading performance', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; // Create a moderately large PDF const pdfDoc = await PDFDocument.create(); for (let i = 0; i < 50; i++) { const page = pdfDoc.addPage(); page.drawText(`Page ${i + 1}`, { x: 50, y: 700, size: 20 }); } const xmlContent = ` STREAM-TEST 2025-01-25 380 EUR Test Supplier Berlin 10115 DE Test Customer Munich 80331 DE 100.00 1 1 100.00 Test item 100.00 `; await pdfDoc.attach( Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml' } ); const pdfBytes = await pdfDoc.save(); // Test full loading const loadStartTime = performance.now(); const einvoice1 = await EInvoice.fromPdf(pdfBytes); const loadTime = performance.now() - loadStartTime; console.log(`Full loading time: ${loadTime.toFixed(2)}ms`); // Note: Actual streaming would require stream API support // This is a placeholder for streaming performance comparison console.log('Streaming API would potentially reduce memory usage for large files'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('streaming-comparison', elapsed); }); tap.test('PDF-08: Concurrent large PDF processing', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; // Create multiple PDFs for concurrent processing const createPdf = async (id: string, pages: number) => { const pdfDoc = await PDFDocument.create(); for (let i = 0; i < pages; i++) { const page = pdfDoc.addPage(); page.drawText(`Document ${id} - Page ${i + 1}`, { x: 50, y: 700, size: 16 }); } // Create a minimal valid UBL invoice const minimalUbl = ` ${id} 2025-01-25 380 EUR Supplier Berlin 10115 DE Customer Munich 80331 DE 100.00 1 1 100.00 Item 100.00 `; await pdfDoc.attach( Buffer.from(minimalUbl, 'utf8'), 'invoice.xml', { mimeType: 'application/xml' } ); return pdfDoc.save(); }; // Create PDFs const pdfPromises = [ createPdf('PDF-A', 30), createPdf('PDF-B', 40), createPdf('PDF-C', 50), createPdf('PDF-D', 60) ]; const pdfs = await Promise.all(pdfPromises); // Process concurrently const concurrentStartTime = performance.now(); const processPromises = pdfs.map(async (pdfBytes: Buffer) => { const einvoice = await EInvoice.fromPdf(pdfBytes); return einvoice.toXmlString('ubl'); }); const results = await Promise.all(processPromises); const concurrentTime = performance.now() - concurrentStartTime; expect(results.length).toEqual(4); results.forEach((xml: string, index: number) => { expect(xml).toContain(`PDF-${String.fromCharCode(65 + index)}`); }); console.log(`Concurrent processing of 4 PDFs: ${concurrentTime.toFixed(2)}ms`); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('concurrent-processing', elapsed); }); tap.test('PDF-08: Large PDF with complex structure', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; const pdfDoc = await PDFDocument.create(); // Create complex structure with forms, annotations, etc. const formPage = pdfDoc.addPage(); // Add form fields (simplified - actual forms require more setup) formPage.drawText('Invoice Form', { x: 50, y: 750, size: 24 }); formPage.drawRectangle({ x: 50, y: 700, width: 200, height: 30, borderColor: rgb(0, 0, 0.5), borderWidth: 1 }); formPage.drawText('Invoice Number:', { x: 55, y: 710, size: 12 }); // Add multiple embedded files const attachments = [ { name: 'invoice.xml', size: 10000 }, { name: 'terms.pdf', size: 50000 }, { name: 'logo.png', size: 20000 } ]; for (const att of attachments) { const content = Buffer.alloc(att.size, 'A'); // Dummy content await pdfDoc.attach(content, att.name, { mimeType: att.name.endsWith('.xml') ? 'application/xml' : 'application/octet-stream', description: `Attachment: ${att.name}` }); } // Add many pages with different content types for (let i = 0; i < 25; i++) { const page = pdfDoc.addPage(); // Alternate between text-heavy and graphic-heavy pages if (i % 2 === 0) { // Text-heavy page for (let j = 0; j < 40; j++) { page.drawText(`Line ${j + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.`, { x: 50, y: 750 - (j * 18), size: 10 }); } } else { // Graphic-heavy page for (let j = 0; j < 10; j++) { for (let k = 0; k < 10; k++) { page.drawRectangle({ x: 50 + (k * 50), y: 700 - (j * 50), width: 45, height: 45, color: rgb(Math.random(), Math.random(), Math.random()) }); } } } } const pdfBytes = await pdfDoc.save(); const sizeMB = (pdfBytes.length / 1024 / 1024).toFixed(2); console.log(`Complex PDF size: ${sizeMB} MB`); // Test processing const processStartTime = performance.now(); try { const einvoice = await EInvoice.fromPdf(pdfBytes); const processTime = performance.now() - processStartTime; console.log(`Complex PDF processed in: ${processTime.toFixed(2)}ms`); } catch (error) { console.log('Complex PDF processing error:', error.message); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('complex-structure', elapsed); }); tap.test('PDF-08: Corpus large PDF analysis', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; let largeFileCount = 0; let totalSize = 0; let processedCount = 0; const sizeDistribution = { small: 0, // < 100KB medium: 0, // 100KB - 1MB large: 0, // 1MB - 10MB veryLarge: 0 // > 10MB }; // Create test PDFs of various sizes to simulate corpus const testPdfs: Array<{ path: string; content: Buffer }> = []; // Create small PDFs for (let i = 0; i < 5; i++) { const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage(); page.drawText(`Small PDF ${i}`, { x: 50, y: 700, size: 12 }); const xmlContent = ` SMALL-${i} 2025-01-25 380 EUR Supplier Berlin 10115 DE Customer Munich 80331 DE 100.00 1 1 100.00 Item 100.00 `; await pdfDoc.attach(Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: 'Invoice XML' }); const pdfBytes = await pdfDoc.save(); testPdfs.push({ path: `small-${i}.pdf`, content: Buffer.from(pdfBytes) }); } // Create medium PDFs for (let i = 0; i < 3; i++) { const pdfDoc = await PDFDocument.create(); // Add multiple pages for (let j = 0; j < 50; j++) { const page = pdfDoc.addPage(); page.drawText(`Medium PDF ${i} - Page ${j}`, { x: 50, y: 700, size: 12 }); // Add content to increase size for (let k = 0; k < 20; k++) { page.drawText(`Line ${k}: Lorem ipsum dolor sit amet`, { x: 50, y: 650 - (k * 20), size: 10 }); } } let xmlContent = ` MEDIUM-${i} 2025-01-25 380 EUR Supplier Berlin 10115 DE Customer Munich 80331 DE 500.00 `; // Add multiple line items for (let j = 0; j < 50; j++) { xmlContent += ` ${j + 1} 1 10.00 Item ${j} 10.00 `; } xmlContent += '\n'; await pdfDoc.attach(Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: 'Invoice XML' }); const pdfBytes = await pdfDoc.save(); testPdfs.push({ path: `medium-${i}.pdf`, content: Buffer.from(pdfBytes) }); } // Create large PDFs for (let i = 0; i < 2; i++) { const pdfDoc = await PDFDocument.create(); // Add many pages for (let j = 0; j < 200; j++) { const page = pdfDoc.addPage(); page.drawText(`Large PDF ${i} - Page ${j}`, { x: 50, y: 700, size: 12 }); // Add dense content for (let k = 0; k < 40; k++) { page.drawText(`Line ${k}: Lorem ipsum dolor sit amet, consectetur adipiscing elit`, { x: 50, y: 650 - (k * 15), size: 8 }); } } const xmlContent = ` LARGE-${i} 2025-01-25 380 EUR Supplier Berlin 10115 DE Customer Munich 80331 DE 10000.00 1 1 10000.00 Large item 10000.00 `; await pdfDoc.attach(Buffer.from(xmlContent, 'utf8'), 'invoice.xml', { mimeType: 'application/xml', description: 'Invoice XML' }); const pdfBytes = await pdfDoc.save(); testPdfs.push({ path: `large-${i}.pdf`, content: Buffer.from(pdfBytes) }); } // Process test PDFs for (const testPdf of testPdfs) { const sizeMB = testPdf.content.length / 1024 / 1024; totalSize += testPdf.content.length; if (testPdf.content.length < 100 * 1024) { sizeDistribution.small++; } else if (testPdf.content.length < 1024 * 1024) { sizeDistribution.medium++; } else if (testPdf.content.length < 10 * 1024 * 1024) { sizeDistribution.large++; largeFileCount++; } else { sizeDistribution.veryLarge++; largeFileCount++; } // Test large file processing if (sizeMB > 1) { const testStartTime = performance.now(); try { const einvoice = await EInvoice.fromPdf(testPdf.content); const testTime = performance.now() - testStartTime; console.log(`Large file ${testPdf.path} (${sizeMB.toFixed(2)} MB) processed in ${testTime.toFixed(2)}ms`); } catch (error) { console.log(`Large file ${testPdf.path} processing failed:`, error.message); } } processedCount++; } const avgSize = totalSize / processedCount / 1024; console.log(`Corpus PDF analysis (${processedCount} files):`); console.log(`- Average size: ${avgSize.toFixed(2)} KB`); console.log(`- Large files (>1MB): ${largeFileCount}`); console.log('Size distribution:', sizeDistribution); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('corpus-large-pdfs', elapsed); }); tap.test('PDF-08: Performance degradation test', async () => { const startTime = performance.now(); // Dynamic import for EInvoice const { EInvoice } = await import('../../../ts/index.js'); const { PDFDocument } = plugins; const processingTimes: number[] = []; // Test if performance degrades with repeated operations for (let iteration = 0; iteration < 5; iteration++) { const iterStartTime = performance.now(); // Create PDF const pdfDoc = await PDFDocument.create(); for (let i = 0; i < 20; i++) { const page = pdfDoc.addPage(); page.drawText(`Iteration ${iteration + 1} - Page ${i + 1}`, { x: 50, y: 700, size: 16 }); } // Create a minimal valid UBL invoice for performance test const perfUbl = ` PERF-${iteration} 2025-01-25 380 EUR Supplier Berlin 10115 DE Customer Munich 80331 DE 100.00 1 1 100.00 Item 100.00 `; await pdfDoc.attach( Buffer.from(perfUbl, 'utf8'), 'invoice.xml', { mimeType: 'application/xml' } ); const pdfBytes = await pdfDoc.save(); // Process PDF const einvoice = await EInvoice.fromPdf(pdfBytes); await einvoice.toXmlString('ubl'); const iterTime = performance.now() - iterStartTime; processingTimes.push(iterTime); console.log(`Iteration ${iteration + 1}: ${iterTime.toFixed(2)}ms`); // Allow for cleanup between iterations if (global.gc && iteration < 4) { global.gc(); } // Small delay to stabilize performance await new Promise(resolve => setTimeout(resolve, 10)); } // Check for performance degradation const firstTime = processingTimes[0]; const lastTime = processingTimes[processingTimes.length - 1]; const degradation = ((lastTime - firstTime) / firstTime) * 100; console.log(`Performance degradation: ${degradation.toFixed(2)}%`); expect(Math.abs(degradation)).toBeLessThan(150); // Allow up to 150% variation for performance tests const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('degradation-test', elapsed); }); tap.test('PDF-08: Performance Summary', async () => { // Print performance summary performanceTracker.printSummary(); // Performance assertions const avgTime = performanceTracker.getAverageTime(); expect(avgTime).toBeLessThan(5000); // Large PDFs may take longer console.log('PDF-08: Large PDF Performance tests completed'); }); tap.start();