einvoice/test/suite/einvoice_conversion/test.conv-12.performance.ts

490 lines
19 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
/**
* @file test.conv-12.performance.ts
* @description Performance benchmarks for format conversion operations
*/
import { 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';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('CONV-12: Conversion Performance');
tap.test('CONV-12: Conversion Performance - should meet performance targets for conversion operations', async (t) => {
// Test 1: Single conversion performance benchmarks
const singleConversionBenchmarks = await performanceTracker.measureAsync(
'single-conversion-benchmarks',
async () => {
const einvoice = new EInvoice();
const benchmarks = [];
// Define conversion scenarios
const scenarios = [
{ from: 'ubl', to: 'cii', name: 'UBL to CII' },
{ from: 'cii', to: 'ubl', name: 'CII to UBL' },
{ from: 'ubl', to: 'xrechnung', name: 'UBL to XRechnung' },
{ from: 'cii', to: 'zugferd', name: 'CII to ZUGFeRD' },
{ from: 'zugferd', to: 'xrechnung', name: 'ZUGFeRD to XRechnung' }
];
// Create test invoices for each format
const testInvoices = {
ubl: {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-UBL-001',
issueDate: '2024-01-30',
seller: { name: 'UBL Seller', address: 'UBL Street', country: 'US', taxId: 'US123456789' },
buyer: { name: 'UBL Buyer', address: 'UBL Avenue', country: 'US', taxId: 'US987654321' },
items: [{ description: 'Product', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }],
totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 }
}
},
cii: {
format: 'cii' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-CII-001',
issueDate: '2024-01-30',
seller: { name: 'CII Seller', address: 'CII Street', country: 'DE', taxId: 'DE123456789' },
buyer: { name: 'CII Buyer', address: 'CII Avenue', country: 'DE', taxId: 'DE987654321' },
items: [{ description: 'Service', quantity: 1, unitPrice: 200, vatRate: 19, lineTotal: 200 }],
totals: { netAmount: 200, vatAmount: 38, grossAmount: 238 }
}
},
zugferd: {
format: 'zugferd' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-ZF-001',
issueDate: '2024-01-30',
seller: { name: 'ZF Seller', address: 'ZF Street', country: 'DE', taxId: 'DE111222333' },
buyer: { name: 'ZF Buyer', address: 'ZF Avenue', country: 'DE', taxId: 'DE444555666' },
items: [{ description: 'Goods', quantity: 5, unitPrice: 50, vatRate: 19, lineTotal: 250 }],
totals: { netAmount: 250, vatAmount: 47.50, grossAmount: 297.50 }
}
}
};
// Run benchmarks
for (const scenario of scenarios) {
if (!testInvoices[scenario.from]) continue;
const iterations = 10;
const times = [];
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
try {
await einvoice.convertFormat(testInvoices[scenario.from], scenario.to);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds
times.push(duration);
} catch (error) {
// Conversion not supported
}
}
if (times.length > 0) {
times.sort((a, b) => a - b);
benchmarks.push({
scenario: scenario.name,
min: times[0],
max: times[times.length - 1],
avg: times.reduce((a, b) => a + b, 0) / times.length,
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)] || times[times.length - 1]
});
}
}
return benchmarks;
}
);
// Test 2: Complex invoice conversion performance
const complexInvoicePerformance = await performanceTracker.measureAsync(
'complex-invoice-performance',
async () => {
const einvoice = new EInvoice();
// Create complex invoice with many items
const complexInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-COMPLEX-001',
issueDate: '2024-01-30',
dueDate: '2024-02-29',
currency: 'EUR',
seller: {
name: 'Complex International Trading Company Ltd.',
address: 'Global Business Center, Tower A, Floor 25',
city: 'London',
postalCode: 'EC2M 7PY',
country: 'GB',
taxId: 'GB123456789',
email: 'invoicing@complex-trading.com',
phone: '+44 20 7123 4567',
registrationNumber: 'UK12345678'
},
buyer: {
name: 'Multinational Buyer Corporation GmbH',
address: 'Industriestraße 100-200',
city: 'Frankfurt',
postalCode: '60311',
country: 'DE',
taxId: 'DE987654321',
email: 'ap@buyer-corp.de',
phone: '+49 69 9876 5432'
},
items: Array.from({ length: 100 }, (_, i) => ({
description: `Product Line Item ${i + 1} - Detailed description with technical specifications and compliance information`,
quantity: Math.floor(Math.random() * 100) + 1,
unitPrice: Math.random() * 1000,
vatRate: [7, 19, 21][Math.floor(Math.random() * 3)],
lineTotal: 0, // Will be calculated
itemId: `ITEM-${String(i + 1).padStart(4, '0')}`,
additionalInfo: {
weight: `${Math.random() * 10}kg`,
dimensions: `${Math.random() * 100}x${Math.random() * 100}x${Math.random() * 100}cm`,
countryOfOrigin: ['DE', 'FR', 'IT', 'CN', 'US'][Math.floor(Math.random() * 5)]
}
})),
totals: {
netAmount: 0,
vatAmount: 0,
grossAmount: 0
},
paymentTerms: 'Net 30 days, 2% discount for payment within 10 days',
notes: 'This is a complex invoice with 100 line items for performance testing purposes. All items are subject to standard terms and conditions.'
}
};
// Calculate totals
complexInvoice.data.items.forEach(item => {
item.lineTotal = item.quantity * item.unitPrice;
complexInvoice.data.totals.netAmount += item.lineTotal;
complexInvoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100);
});
complexInvoice.data.totals.grossAmount = complexInvoice.data.totals.netAmount + complexInvoice.data.totals.vatAmount;
// Test conversions
const conversions = ['cii', 'zugferd', 'xrechnung'];
const results = [];
for (const targetFormat of conversions) {
const startTime = process.hrtime.bigint();
let success = false;
let error = null;
try {
const converted = await einvoice.convertFormat(complexInvoice, targetFormat);
success = converted !== null;
} catch (e) {
error = e.message;
}
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.push({
targetFormat,
duration,
success,
error,
itemsPerSecond: success ? (100 / (duration / 1000)).toFixed(2) : 'N/A'
});
}
return {
invoiceSize: {
items: complexInvoice.data.items.length,
netAmount: complexInvoice.data.totals.netAmount.toFixed(2),
grossAmount: complexInvoice.data.totals.grossAmount.toFixed(2)
},
conversions: results
};
}
);
// Test 3: Memory usage during conversion
const memoryUsageAnalysis = await performanceTracker.measureAsync(
'memory-usage-analysis',
async () => {
const einvoice = new EInvoice();
const memorySnapshots = [];
// Force garbage collection if available
if (global.gc) global.gc();
const baselineMemory = process.memoryUsage();
// Create invoices of increasing size
const sizes = [1, 10, 50, 100, 200];
for (const size of sizes) {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `MEM-TEST-${size}`,
issueDate: '2024-01-30',
seller: { name: 'Memory Test Seller', address: 'Test Street', country: 'US', taxId: 'US123456789' },
buyer: { name: 'Memory Test Buyer', address: 'Test Avenue', country: 'US', taxId: 'US987654321' },
items: Array.from({ length: size }, (_, i) => ({
description: `Item ${i + 1} with a reasonably long description to simulate real-world data`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: size * 100, vatAmount: size * 10, grossAmount: size * 110 }
}
};
// Perform conversion and measure memory
const beforeConversion = process.memoryUsage();
try {
const converted = await einvoice.convertFormat(invoice, 'cii');
const afterConversion = process.memoryUsage();
memorySnapshots.push({
items: size,
heapUsedBefore: Math.round((beforeConversion.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100,
heapUsedAfter: Math.round((afterConversion.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100,
heapIncrease: Math.round((afterConversion.heapUsed - beforeConversion.heapUsed) / 1024 / 1024 * 100) / 100,
external: Math.round((afterConversion.external - baselineMemory.external) / 1024 / 1024 * 100) / 100
});
} catch (error) {
// Skip if conversion fails
}
}
// Force garbage collection and measure final state
if (global.gc) global.gc();
const finalMemory = process.memoryUsage();
return {
snapshots: memorySnapshots,
totalMemoryIncrease: Math.round((finalMemory.heapUsed - baselineMemory.heapUsed) / 1024 / 1024 * 100) / 100,
memoryPerItem: memorySnapshots.length > 0 ?
(memorySnapshots[memorySnapshots.length - 1].heapIncrease / sizes[sizes.length - 1]).toFixed(3) : 'N/A'
};
}
);
// Test 4: Concurrent conversion performance
const concurrentPerformance = await performanceTracker.measureAsync(
'concurrent-conversion-performance',
async () => {
const einvoice = new EInvoice();
const concurrencyLevels = [1, 5, 10, 20];
const results = [];
// Create test invoice
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'CONC-TEST-001',
issueDate: '2024-01-30',
seller: { name: 'Concurrent Seller', address: 'Parallel Street', country: 'US', taxId: 'US123456789' },
buyer: { name: 'Concurrent Buyer', address: 'Async Avenue', country: 'US', taxId: 'US987654321' },
items: Array.from({ length: 10 }, (_, i) => ({
description: `Concurrent Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 }
}
};
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
// Create concurrent conversion tasks
const tasks = Array.from({ length: concurrency }, () =>
einvoice.convertFormat(testInvoice, 'cii').catch(() => null)
);
const taskResults = await Promise.all(tasks);
const endTime = Date.now();
const successful = taskResults.filter(r => r !== null).length;
const duration = endTime - startTime;
const throughput = (successful / (duration / 1000)).toFixed(2);
results.push({
concurrency,
duration,
successful,
failed: concurrency - successful,
throughput: `${throughput} conversions/sec`
});
}
return results;
}
);
// Test 5: Corpus conversion performance analysis
const corpusPerformance = await performanceTracker.measureAsync(
'corpus-conversion-performance',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const performanceData = {
formatStats: new Map<string, { count: number; totalTime: number; minTime: number; maxTime: number }>(),
sizeCategories: {
small: { count: 0, avgTime: 0, totalTime: 0 }, // < 10KB
medium: { count: 0, avgTime: 0, totalTime: 0 }, // 10KB - 100KB
large: { count: 0, avgTime: 0, totalTime: 0 } // > 100KB
},
totalConversions: 0,
failedConversions: 0
};
// Sample files for performance testing
const sampleFiles = files.slice(0, 50);
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const fileSize = Buffer.byteLength(content, 'utf-8');
// Categorize by size
const sizeCategory = fileSize < 10240 ? 'small' :
fileSize < 102400 ? 'medium' : 'large';
// Detect format and parse
const format = await einvoice.detectFormat(content);
if (!format || format === 'unknown') continue;
const parsed = await einvoice.parseInvoice(content, format);
// Measure conversion time
const targetFormat = format === 'ubl' ? 'cii' : 'ubl';
const startTime = process.hrtime.bigint();
try {
await einvoice.convertFormat(parsed, targetFormat);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
// Update format stats
if (!performanceData.formatStats.has(format)) {
performanceData.formatStats.set(format, {
count: 0,
totalTime: 0,
minTime: Infinity,
maxTime: 0
});
}
const stats = performanceData.formatStats.get(format)!;
stats.count++;
stats.totalTime += duration;
stats.minTime = Math.min(stats.minTime, duration);
stats.maxTime = Math.max(stats.maxTime, duration);
// Update size category stats
performanceData.sizeCategories[sizeCategory].count++;
performanceData.sizeCategories[sizeCategory].totalTime += duration;
performanceData.totalConversions++;
} catch (convError) {
performanceData.failedConversions++;
}
} catch (error) {
// Skip files that can't be processed
}
}
// Calculate averages
for (const category of Object.keys(performanceData.sizeCategories)) {
const cat = performanceData.sizeCategories[category];
if (cat.count > 0) {
cat.avgTime = cat.totalTime / cat.count;
}
}
// Format statistics
const formatStatsSummary = Array.from(performanceData.formatStats.entries()).map(([format, stats]) => ({
format,
count: stats.count,
avgTime: stats.count > 0 ? (stats.totalTime / stats.count).toFixed(2) : 'N/A',
minTime: stats.minTime === Infinity ? 'N/A' : stats.minTime.toFixed(2),
maxTime: stats.maxTime.toFixed(2)
}));
return {
totalConversions: performanceData.totalConversions,
failedConversions: performanceData.failedConversions,
successRate: ((performanceData.totalConversions - performanceData.failedConversions) / performanceData.totalConversions * 100).toFixed(2) + '%',
formatStats: formatStatsSummary,
sizeCategories: {
small: { ...performanceData.sizeCategories.small, avgTime: performanceData.sizeCategories.small.avgTime.toFixed(2) },
medium: { ...performanceData.sizeCategories.medium, avgTime: performanceData.sizeCategories.medium.avgTime.toFixed(2) },
large: { ...performanceData.sizeCategories.large, avgTime: performanceData.sizeCategories.large.avgTime.toFixed(2) }
}
};
}
);
// Summary
t.comment('\n=== CONV-12: Conversion Performance Test Summary ===');
t.comment('\nSingle Conversion Benchmarks (10 iterations each):');
singleConversionBenchmarks.result.forEach(bench => {
t.comment(` ${bench.scenario}:`);
t.comment(` - Min: ${bench.min.toFixed(2)}ms, Max: ${bench.max.toFixed(2)}ms`);
t.comment(` - Average: ${bench.avg.toFixed(2)}ms, Median: ${bench.median.toFixed(2)}ms, P95: ${bench.p95.toFixed(2)}ms`);
});
t.comment('\nComplex Invoice Performance (100 items):');
t.comment(` Invoice size: ${complexInvoicePerformance.result.invoiceSize.items} items, €${complexInvoicePerformance.result.invoiceSize.grossAmount}`);
complexInvoicePerformance.result.conversions.forEach(conv => {
t.comment(` ${conv.targetFormat}: ${conv.duration.toFixed(2)}ms (${conv.itemsPerSecond} items/sec) - ${conv.success ? 'SUCCESS' : 'FAILED'}`);
});
t.comment('\nMemory Usage Analysis:');
memoryUsageAnalysis.result.snapshots.forEach(snap => {
t.comment(` ${snap.items} items: ${snap.heapIncrease}MB heap increase`);
});
t.comment(` Average memory per item: ${memoryUsageAnalysis.result.memoryPerItem}MB`);
t.comment('\nConcurrent Conversion Performance:');
concurrentPerformance.result.forEach(result => {
t.comment(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput}`);
});
t.comment('\nCorpus Performance Analysis:');
t.comment(` Total conversions: ${corpusPerformance.result.totalConversions}`);
t.comment(` Success rate: ${corpusPerformance.result.successRate}`);
t.comment(' By format:');
corpusPerformance.result.formatStats.forEach(stat => {
t.comment(` - ${stat.format}: ${stat.count} files, avg ${stat.avgTime}ms (min: ${stat.minTime}ms, max: ${stat.maxTime}ms)`);
});
t.comment(' By size:');
Object.entries(corpusPerformance.result.sizeCategories).forEach(([size, data]: [string, any]) => {
t.comment(` - ${size}: ${data.count} files, avg ${data.avgTime}ms`);
});
// Performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();