fix(compliance): improve compliance

This commit is contained in:
2025-05-28 19:37:00 +00:00
parent 892a8392a4
commit 756964aabd
6 changed files with 1223 additions and 1823 deletions

View File

@ -3,425 +3,410 @@
* @description Performance tests for PDF extraction operations
*/
import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../plugins.js';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import { EInvoice } from '../../../ts/index.js';
import { CorpusLoader } from '../../suite/corpus.loader.js';
import { PerformanceTracker } from '../../suite/performance.tracker.js';
import { PDFDocument, rgb } from 'pdf-lib';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-03: PDF Extraction Speed');
// Simple performance tracking
class SimplePerformanceTracker {
private measurements: Map<string, number[]> = new Map();
private name: string;
tap.test('PERF-03: PDF Extraction Speed - should meet performance targets for PDF extraction', async (t) => {
// Test 1: ZUGFeRD v1 extraction performance
const zugferdV1Performance = await performanceTracker.measureAsync(
'zugferd-v1-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/ZUGFeRDv1/**/*.pdf');
const einvoice = new EInvoice();
const results = {
fileCount: 0,
extractionTimes: [],
fileSizes: [],
successCount: 0,
failureCount: 0,
bytesPerMs: []
};
// Process ZUGFeRD v1 PDFs
const sampleFiles = files.slice(0, 20);
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
const fileSize = pdfBuffer.length;
results.fileSizes.push(fileSize);
results.fileCount++;
// Measure extraction time
const startTime = process.hrtime.bigint();
const extractedXml = await einvoice.extractFromPDF(pdfBuffer);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.extractionTimes.push(duration);
if (extractedXml) {
results.successCount++;
results.bytesPerMs.push(fileSize / duration);
} else {
results.failureCount++;
}
} catch (error) {
results.failureCount++;
}
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);
}
getStats(key: string) {
const times = this.measurements.get(key) || [];
if (times.length === 0) return null;
const sorted = [...times].sort((a, b) => a - b);
return {
avg: times.reduce((a, b) => a + b, 0) / times.length,
min: sorted[0],
max: sorted[sorted.length - 1],
p95: sorted[Math.floor(sorted.length * 0.95)]
};
}
printSummary(): void {
console.log(`\n${this.name} - Performance Summary:`);
for (const [key, times] of this.measurements) {
const stats = this.getStats(key);
if (stats) {
console.log(` ${key}: avg=${stats.avg.toFixed(2)}ms, min=${stats.min.toFixed(2)}ms, max=${stats.max.toFixed(2)}ms, p95=${stats.p95.toFixed(2)}ms`);
}
// Calculate statistics
if (results.extractionTimes.length > 0) {
results.extractionTimes.sort((a, b) => a - b);
const stats = {
min: results.extractionTimes[0],
max: results.extractionTimes[results.extractionTimes.length - 1],
avg: results.extractionTimes.reduce((a, b) => a + b, 0) / results.extractionTimes.length,
median: results.extractionTimes[Math.floor(results.extractionTimes.length / 2)],
avgFileSize: results.fileSizes.reduce((a, b) => a + b, 0) / results.fileSizes.length / 1024, // KB
avgBytesPerMs: results.bytesPerMs.length > 0 ?
results.bytesPerMs.reduce((a, b) => a + b, 0) / results.bytesPerMs.length / 1024 : 0 // KB/ms
};
return { ...results, stats };
}
return results;
}
}
}
const performanceTracker = new SimplePerformanceTracker('PERF-03: PDF Extraction Speed');
// Helper to create test PDFs with embedded XML
async function createTestPdf(name: string, xmlContent: string, pages: number = 1): Promise<Buffer> {
const pdfDoc = await PDFDocument.create();
// Add pages
for (let i = 0; i < pages; i++) {
const page = pdfDoc.addPage([595, 842]); // A4
page.drawText(`Test Invoice ${name} - Page ${i + 1}`, {
x: 50,
y: 750,
size: 20
});
// Add some content
page.drawRectangle({
x: 50,
y: 600,
width: 495,
height: 100,
borderColor: rgb(0, 0, 0),
borderWidth: 1
});
}
// Attach the XML
await pdfDoc.attach(
Buffer.from(xmlContent, 'utf8'),
'invoice.xml',
{
mimeType: 'application/xml',
description: `Invoice ${name}`
}
);
// Test 2: ZUGFeRD v2/Factur-X extraction performance
const facturXPerformance = await performanceTracker.measureAsync(
'facturx-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/ZUGFeRDv2/**/*.pdf');
const einvoice = new EInvoice();
const results = {
profiles: new Map<string, { count: number; totalTime: number }>(),
extractionTimes: [],
xmlSizes: [],
largestFile: { path: '', size: 0, time: 0 },
smallestFile: { path: '', size: Infinity, time: 0 }
};
// Process Factur-X PDFs
const sampleFiles = files.slice(0, 30);
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
const fileSize = pdfBuffer.length;
// Measure extraction
const startTime = process.hrtime.bigint();
const extractedXml = await einvoice.extractFromPDF(pdfBuffer);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.extractionTimes.push(duration);
if (extractedXml) {
const xmlSize = Buffer.byteLength(extractedXml, 'utf-8');
results.xmlSizes.push(xmlSize);
// Detect profile from filename or content
const profile = file.includes('BASIC') ? 'BASIC' :
file.includes('COMFORT') ? 'COMFORT' :
file.includes('EXTENDED') ? 'EXTENDED' : 'UNKNOWN';
if (!results.profiles.has(profile)) {
results.profiles.set(profile, { count: 0, totalTime: 0 });
}
const profileStats = results.profiles.get(profile)!;
profileStats.count++;
profileStats.totalTime += duration;
// Track largest/smallest
if (fileSize > results.largestFile.size) {
results.largestFile = { path: file, size: fileSize, time: duration };
}
if (fileSize < results.smallestFile.size) {
results.smallestFile = { path: file, size: fileSize, time: duration };
}
}
} catch (error) {
// Skip failed extractions
}
}
// Calculate profile statistics
const profileStats = Array.from(results.profiles.entries()).map(([profile, data]) => ({
profile,
count: data.count,
avgTime: data.count > 0 ? (data.totalTime / data.count).toFixed(3) : 'N/A'
}));
return {
totalFiles: sampleFiles.length,
successfulExtractions: results.extractionTimes.length,
avgExtractionTime: results.extractionTimes.length > 0 ?
(results.extractionTimes.reduce((a, b) => a + b, 0) / results.extractionTimes.length).toFixed(3) : 'N/A',
avgXmlSize: results.xmlSizes.length > 0 ?
(results.xmlSizes.reduce((a, b) => a + b, 0) / results.xmlSizes.length / 1024).toFixed(2) : 'N/A',
profileStats,
largestFile: {
...results.largestFile,
sizeKB: (results.largestFile.size / 1024).toFixed(2),
timeMs: results.largestFile.time.toFixed(3)
},
smallestFile: {
...results.smallestFile,
sizeKB: (results.smallestFile.size / 1024).toFixed(2),
timeMs: results.smallestFile.time.toFixed(3)
}
};
}
);
return Buffer.from(await pdfDoc.save());
}
// Helper to create test XML
function createTestXml(id: string, lineItems: number = 10): string {
const lines = Array(lineItems).fill(null).map((_, i) => `
<cac:InvoiceLine>
<cbc:ID>${i + 1}</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Product ${i + 1}</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`).join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>${id}</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Supplier</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Customer</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Munich</cbc:CityName>
<cbc:PostalZone>80331</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="EUR">${100 * lineItems}.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
${lines}
</Invoice>`;
}
tap.test('PERF-03: Basic PDF extraction performance', async () => {
const testCases = [
{ name: 'Small PDF', pages: 1, lineItems: 10 },
{ name: 'Medium PDF', pages: 10, lineItems: 50 },
{ name: 'Large PDF', pages: 50, lineItems: 200 }
];
// Test 3: Large PDF extraction performance
const largePDFPerformance = await performanceTracker.measureAsync(
'large-pdf-extraction',
async () => {
const einvoice = new EInvoice();
const results = [];
// Create synthetic large PDFs with embedded XML
const pdfSizes = [
{ name: '1MB', size: 1024 * 1024, xmlSize: 50 * 1024 },
{ name: '5MB', size: 5 * 1024 * 1024, xmlSize: 100 * 1024 },
{ name: '10MB', size: 10 * 1024 * 1024, xmlSize: 200 * 1024 },
{ name: '20MB', size: 20 * 1024 * 1024, xmlSize: 500 * 1024 }
];
for (const pdfSpec of pdfSizes) {
// Simulate PDF content (in real scenario, would use actual PDF library)
const mockPdfBuffer = Buffer.alloc(pdfSpec.size);
// Fill with some pattern to simulate real PDF
for (let i = 0; i < mockPdfBuffer.length; i += 1024) {
mockPdfBuffer.write('%PDF-1.4\n', i);
}
// Embed mock XML at a known location
const mockXml = `<?xml version="1.0"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID>LARGE-PDF-TEST</ram:ID>
${' '.repeat(pdfSpec.xmlSize - 200)}
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
// Measure extraction time
const times = [];
const iterations = 5;
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
try {
// Simulate extraction (would use real PDF library)
await new Promise(resolve => setTimeout(resolve, pdfSpec.size / (50 * 1024 * 1024))); // Simulate 50MB/s extraction
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
times.push(duration);
} catch (error) {
// Extraction failed
}
}
if (times.length > 0) {
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.push({
size: pdfSpec.name,
sizeBytes: pdfSpec.size,
avgExtractionTime: avgTime.toFixed(3),
throughputMBps: (pdfSpec.size / avgTime / 1024).toFixed(2)
});
}
}
return results;
}
);
const iterations = 20;
// Test 4: Concurrent PDF extraction
const concurrentExtraction = await performanceTracker.measureAsync(
'concurrent-pdf-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.pdf');
const einvoice = new EInvoice();
const results = [];
for (const testCase of testCases) {
const xmlContent = createTestXml(`PDF-${testCase.name}`, testCase.lineItems);
const pdfBuffer = await createTestPdf(testCase.name, xmlContent, testCase.pages);
const times: number[] = [];
console.log(`Testing ${testCase.name}: ${(pdfBuffer.length / 1024).toFixed(2)} KB`);
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
const einvoice = await EInvoice.fromPdf(pdfBuffer);
const endTime = performance.now();
// Select sample PDFs
const samplePDFs = files.slice(0, 10);
if (samplePDFs.length === 0) {
return { error: 'No PDF files found for testing' };
const duration = endTime - startTime;
times.push(duration);
performanceTracker.addMeasurement(`extract-${testCase.name}`, duration);
if (i === 0) {
// Verify extraction worked
expect(einvoice.id).toContain(testCase.name);
}
// Test different concurrency levels
const concurrencyLevels = [1, 2, 5, 10];
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
let successCount = 0;
// Create extraction tasks
const tasks = [];
for (let i = 0; i < concurrency; i++) {
const pdfFile = samplePDFs[i % samplePDFs.length];
tasks.push(
plugins.fs.readFile(pdfFile)
.then(buffer => einvoice.extractFromPDF(buffer))
.then(xml => xml ? successCount++ : null)
.catch(() => null)
);
}
await Promise.all(tasks);
const duration = Date.now() - startTime;
results.push({
concurrency,
duration,
successCount,
throughput: (successCount / (duration / 1000)).toFixed(2),
avgTimePerExtraction: (duration / concurrency).toFixed(3)
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const bytesPerMs = pdfBuffer.length / avg;
console.log(` Average extraction time: ${avg.toFixed(3)}ms`);
console.log(` Throughput: ${(bytesPerMs / 1024).toFixed(2)} KB/ms`);
// Performance expectations
expect(avg).toBeLessThan(testCase.pages * 10 + 100); // Allow 10ms per page + 100ms base
}
});
tap.test('PERF-03: Different attachment methods performance', async () => {
const xmlContent = createTestXml('ATTACHMENT-TEST', 20);
// Test different PDF structures
const testCases = [
{
name: 'Standard attachment',
create: async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage();
await pdfDoc.attach(Buffer.from(xmlContent), 'invoice.xml', {
mimeType: 'application/xml'
});
return Buffer.from(await pdfDoc.save());
}
},
{
name: 'With AFRelationship',
create: async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage();
await pdfDoc.attach(Buffer.from(xmlContent), 'invoice.xml', {
mimeType: 'application/xml',
afRelationship: plugins.AFRelationship.Data
});
return Buffer.from(await pdfDoc.save());
}
},
{
name: 'Multiple attachments',
create: async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage();
// Main invoice
await pdfDoc.attach(Buffer.from(xmlContent), 'invoice.xml', {
mimeType: 'application/xml'
});
// Additional files
await pdfDoc.attach(Buffer.from('<extra>data</extra>'), 'extra.xml', {
mimeType: 'application/xml'
});
return Buffer.from(await pdfDoc.save());
}
return results;
}
);
];
// Test 5: Memory efficiency during extraction
const memoryEfficiency = await performanceTracker.measureAsync(
'extraction-memory-efficiency',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.pdf');
const einvoice = new EInvoice();
const results = {
memorySnapshots: [],
peakMemoryUsage: 0,
avgMemoryPerExtraction: 0
};
for (const testCase of testCases) {
const pdfBuffer = await testCase.create();
const times: number[] = [];
for (let i = 0; i < 30; i++) {
const startTime = performance.now();
const einvoice = await EInvoice.fromPdf(pdfBuffer);
const endTime = performance.now();
// Force garbage collection if available
if (global.gc) global.gc();
const baselineMemory = process.memoryUsage();
times.push(endTime - startTime);
// Process PDFs and monitor memory
const sampleFiles = files.slice(0, 20);
let extractionCount = 0;
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
// Memory before extraction
const beforeMemory = process.memoryUsage();
// Extract XML
const xml = await einvoice.extractFromPDF(pdfBuffer);
// Memory after extraction
const afterMemory = process.memoryUsage();
if (xml) {
extractionCount++;
const memoryIncrease = {
heapUsed: (afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024,
external: (afterMemory.external - beforeMemory.external) / 1024 / 1024,
fileSize: pdfBuffer.length / 1024 / 1024
};
results.memorySnapshots.push(memoryIncrease);
if (afterMemory.heapUsed > results.peakMemoryUsage) {
results.peakMemoryUsage = afterMemory.heapUsed;
}
}
} catch (error) {
// Skip failed extractions
}
if (i === 0) {
expect(einvoice.id).toEqual('ATTACHMENT-TEST');
}
// Calculate statistics
if (results.memorySnapshots.length > 0) {
const totalMemoryIncrease = results.memorySnapshots
.reduce((sum, snap) => sum + snap.heapUsed, 0);
results.avgMemoryPerExtraction = totalMemoryIncrease / results.memorySnapshots.length;
}
// Force garbage collection and measure final state
if (global.gc) global.gc();
const finalMemory = process.memoryUsage();
return {
extractionsProcessed: extractionCount,
peakMemoryMB: ((results.peakMemoryUsage - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2),
avgMemoryPerExtractionMB: results.avgMemoryPerExtraction.toFixed(2),
memoryLeakDetected: (finalMemory.heapUsed - baselineMemory.heapUsed) > 50 * 1024 * 1024,
finalMemoryIncreaseMB: ((finalMemory.heapUsed - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2)
};
}
);
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`${testCase.name}: avg=${avg.toFixed(3)}ms`);
performanceTracker.addMeasurement(`attachment-${testCase.name}`, avg);
// All methods should be reasonably fast
expect(avg).toBeLessThan(50);
}
});
// Summary
t.comment('\n=== PERF-03: PDF Extraction Speed Test Summary ===');
tap.test('PERF-03: XML size impact on extraction', async () => {
const sizes = [1, 10, 50, 100, 500];
if (zugferdV1Performance.result.stats) {
t.comment('\nZUGFeRD v1 Extraction Performance:');
t.comment(` Files processed: ${zugferdV1Performance.result.fileCount}`);
t.comment(` Success rate: ${(zugferdV1Performance.result.successCount / zugferdV1Performance.result.fileCount * 100).toFixed(1)}%`);
t.comment(` Extraction times:`);
t.comment(` - Min: ${zugferdV1Performance.result.stats.min.toFixed(3)}ms`);
t.comment(` - Max: ${zugferdV1Performance.result.stats.max.toFixed(3)}ms`);
t.comment(` - Avg: ${zugferdV1Performance.result.stats.avg.toFixed(3)}ms`);
t.comment(` - Median: ${zugferdV1Performance.result.stats.median.toFixed(3)}ms`);
t.comment(` Average file size: ${zugferdV1Performance.result.stats.avgFileSize.toFixed(2)}KB`);
t.comment(` Throughput: ${zugferdV1Performance.result.stats.avgBytesPerMs.toFixed(2)}KB/ms`);
for (const size of sizes) {
const xmlContent = createTestXml(`SIZE-${size}`, size);
const pdfBuffer = await createTestPdf(`Size test ${size} items`, xmlContent);
const times: number[] = [];
for (let i = 0; i < 20; i++) {
const startTime = performance.now();
await EInvoice.fromPdf(pdfBuffer);
const endTime = performance.now();
times.push(endTime - startTime);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const xmlSizeKB = (xmlContent.length / 1024).toFixed(2);
console.log(`XML with ${size} items (${xmlSizeKB} KB): avg=${avg.toFixed(3)}ms`);
performanceTracker.addMeasurement(`xml-size-${size}`, avg);
// Extraction time should scale reasonably with XML size
expect(avg).toBeLessThan(size * 0.5 + 30);
}
});
tap.test('PERF-03: Concurrent PDF extraction', async () => {
const xmlContent = createTestXml('CONCURRENT', 20);
const pdfBuffer = await createTestPdf('Concurrent test', xmlContent);
const concurrentCounts = [1, 5, 10];
for (const count of concurrentCounts) {
const startTime = performance.now();
const promises = Array(count).fill(null).map(() =>
EInvoice.fromPdf(pdfBuffer)
);
const results = await Promise.all(promises);
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTimePerExtraction = totalTime / count;
console.log(`Concurrent extractions (${count}): total=${totalTime.toFixed(2)}ms, avg per extraction=${avgTimePerExtraction.toFixed(2)}ms`);
// Verify all extractions succeeded
expect(results.every(e => e.id === 'CONCURRENT')).toEqual(true);
// Concurrent operations should be efficient
expect(avgTimePerExtraction).toBeLessThan(100);
}
});
tap.test('PERF-03: Error handling performance', async () => {
const errorCases = [
{
name: 'PDF without XML',
create: async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage();
// No XML attachment
return Buffer.from(await pdfDoc.save());
}
},
{
name: 'Invalid PDF',
create: async () => Buffer.from('Not a PDF')
},
{
name: 'Corrupted attachment',
create: async () => {
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage();
await pdfDoc.attach(Buffer.from('<<<invalid xml>>>'), 'invoice.xml', {
mimeType: 'application/xml'
});
return Buffer.from(await pdfDoc.save());
}
}
];
for (const errorCase of errorCases) {
const pdfBuffer = await errorCase.create();
const times: number[] = [];
for (let i = 0; i < 20; i++) {
const startTime = performance.now();
try {
await EInvoice.fromPdf(pdfBuffer);
} catch (error) {
// Expected error
}
const endTime = performance.now();
times.push(endTime - startTime);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`${errorCase.name} - Error handling: avg=${avg.toFixed(3)}ms`);
// Error cases should fail fast
expect(avg).toBeLessThan(10);
}
});
tap.test('PERF-03: Memory efficiency during extraction', async () => {
// Create a large PDF with many pages
const xmlContent = createTestXml('MEMORY-TEST', 100);
const largePdf = await createTestPdf('Memory test', xmlContent, 100);
console.log(`Large PDF size: ${(largePdf.length / 1024 / 1024).toFixed(2)} MB`);
const initialMemory = process.memoryUsage();
const extractionTimes: number[] = [];
// Extract multiple times to check for memory leaks
for (let i = 0; i < 10; i++) {
const startTime = performance.now();
const einvoice = await EInvoice.fromPdf(largePdf);
const endTime = performance.now();
extractionTimes.push(endTime - startTime);
expect(einvoice.id).toEqual('MEMORY-TEST');
}
t.comment('\nFactur-X/ZUGFeRD v2 Extraction Performance:');
t.comment(` Files processed: ${facturXPerformance.result.totalFiles}`);
t.comment(` Successful extractions: ${facturXPerformance.result.successfulExtractions}`);
t.comment(` Average extraction time: ${facturXPerformance.result.avgExtractionTime}ms`);
t.comment(` Average XML size: ${facturXPerformance.result.avgXmlSize}KB`);
t.comment(' By profile:');
facturXPerformance.result.profileStats.forEach(stat => {
t.comment(` - ${stat.profile}: ${stat.count} files, avg ${stat.avgTime}ms`);
});
t.comment(` Largest file: ${facturXPerformance.result.largestFile.sizeKB}KB in ${facturXPerformance.result.largestFile.timeMs}ms`);
t.comment(` Smallest file: ${facturXPerformance.result.smallestFile.sizeKB}KB in ${facturXPerformance.result.smallestFile.timeMs}ms`);
const finalMemory = process.memoryUsage();
const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
t.comment('\nLarge PDF Extraction Performance:');
largePDFPerformance.result.forEach(result => {
t.comment(` ${result.size}: ${result.avgExtractionTime}ms (${result.throughputMBps}MB/s)`);
});
console.log(`Memory increase after 10 extractions: ${memoryIncrease.toFixed(2)} MB`);
console.log(`Average extraction time: ${(extractionTimes.reduce((a, b) => a + b, 0) / extractionTimes.length).toFixed(2)}ms`);
t.comment('\nConcurrent Extraction Performance:');
concurrentExtraction.result.forEach(result => {
if (!result.error) {
t.comment(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput} extractions/sec`);
}
});
// Memory increase should be reasonable
expect(memoryIncrease).toBeLessThan(100); // Less than 100MB increase
});
tap.test('PERF-03: Performance Summary', async () => {
performanceTracker.printSummary();
t.comment('\nMemory Efficiency:');
t.comment(` Extractions processed: ${memoryEfficiency.result.extractionsProcessed}`);
t.comment(` Peak memory usage: ${memoryEfficiency.result.peakMemoryMB}MB`);
t.comment(` Avg memory per extraction: ${memoryEfficiency.result.avgMemoryPerExtractionMB}MB`);
t.comment(` Memory leak detected: ${memoryEfficiency.result.memoryLeakDetected ? 'YES ⚠️' : 'NO ✅'}`);
t.comment(` Final memory increase: ${memoryEfficiency.result.finalMemoryIncreaseMB}MB`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgExtractionTime = parseFloat(facturXPerformance.result.avgExtractionTime) || 0;
const targetTime = 500; // Target: <500ms for PDF extraction
if (avgExtractionTime > 0 && avgExtractionTime < targetTime) {
t.comment(`✅ PDF extraction meets target: ${avgExtractionTime}ms < ${targetTime}ms`);
} else if (avgExtractionTime > 0) {
t.comment(`⚠️ PDF extraction exceeds target: ${avgExtractionTime}ms > ${targetTime}ms`);
// Overall performance check
const stats = performanceTracker.getStats('extract-Small PDF');
if (stats) {
console.log(`\nSmall PDF extraction performance: avg=${stats.avg.toFixed(2)}ms`);
expect(stats.avg).toBeLessThan(50); // Small PDFs should extract very quickly
}
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
console.log('\nPDF extraction performance tests completed successfully');
});
tap.start();