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,581 +3,308 @@
* @description Performance tests for format conversion throughput
*/
import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../plugins.js';
import { expect, tap } from '@git.zone/tstest/tapbundle';
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('PERF-04: Conversion Throughput');
// Simple performance tracking
class SimplePerformanceTracker {
private measurements: Map<string, number[]> = new Map();
private name: string;
tap.test('PERF-04: Conversion Throughput - should achieve target throughput for format conversions', async (t) => {
// Test 1: Single-threaded conversion throughput
const singleThreadThroughput = await performanceTracker.measureAsync(
'single-thread-throughput',
async () => {
const einvoice = new EInvoice();
const results = {
conversions: [],
totalTime: 0,
totalInvoices: 0,
totalBytes: 0
};
// Create test invoices of varying complexity
const testInvoices = [
// Simple invoice
...Array(20).fill(null).map((_, i) => ({
format: 'ubl' as const,
targetFormat: 'cii' as const,
complexity: 'simple',
data: {
documentType: 'INVOICE',
invoiceNumber: `SIMPLE-${i + 1}`,
issueDate: '2024-02-05',
seller: { name: 'Simple Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Simple Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: [{ description: 'Item', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }],
totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 }
}
})),
// Medium complexity
...Array(10).fill(null).map((_, i) => ({
format: 'cii' as const,
targetFormat: 'ubl' as const,
complexity: 'medium',
data: {
documentType: 'INVOICE',
invoiceNumber: `MEDIUM-${i + 1}`,
issueDate: '2024-02-05',
dueDate: '2024-03-05',
seller: {
name: 'Medium Complexity Seller GmbH',
address: 'Hauptstraße 123',
city: 'Berlin',
postalCode: '10115',
country: 'DE',
taxId: 'DE123456789'
},
buyer: {
name: 'Medium Complexity Buyer Ltd',
address: 'Business Street 456',
city: 'Munich',
postalCode: '80331',
country: 'DE',
taxId: 'DE987654321'
},
items: Array.from({ length: 10 }, (_, j) => ({
description: `Product ${j + 1}`,
quantity: j + 1,
unitPrice: 50 + j * 10,
vatRate: 19,
lineTotal: (j + 1) * (50 + j * 10)
})),
totals: { netAmount: 1650, vatAmount: 313.50, grossAmount: 1963.50 }
}
})),
// Complex invoice
...Array(5).fill(null).map((_, i) => ({
format: 'ubl' as const,
targetFormat: 'zugferd' as const,
complexity: 'complex',
data: {
documentType: 'INVOICE',
invoiceNumber: `COMPLEX-${i + 1}`,
issueDate: '2024-02-05',
seller: {
name: 'Complex International Corporation',
address: 'Global Plaza 1',
city: 'New York',
country: 'US',
taxId: 'US12-3456789',
email: 'billing@complex.com',
phone: '+1-212-555-0100'
},
buyer: {
name: 'Complex Buyer Enterprises',
address: 'Commerce Center 2',
city: 'London',
country: 'GB',
taxId: 'GB123456789',
email: 'ap@buyer.co.uk'
},
items: Array.from({ length: 50 }, (_, j) => ({
description: `Complex Product ${j + 1} with detailed specifications`,
quantity: Math.floor(Math.random() * 20) + 1,
unitPrice: Math.random() * 500,
vatRate: [0, 5, 10, 20][Math.floor(Math.random() * 4)],
lineTotal: 0
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
}))
];
// Calculate totals for complex invoices
testInvoices.filter(inv => inv.complexity === 'complex').forEach(invoice => {
invoice.data.items.forEach(item => {
item.lineTotal = item.quantity * item.unitPrice;
invoice.data.totals.netAmount += item.lineTotal;
invoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100);
});
invoice.data.totals.grossAmount = invoice.data.totals.netAmount + invoice.data.totals.vatAmount;
});
// Process all conversions
const startTime = Date.now();
for (const testInvoice of testInvoices) {
const invoice = { format: testInvoice.format, data: testInvoice.data };
const invoiceSize = JSON.stringify(invoice).length;
const conversionStart = process.hrtime.bigint();
try {
const converted = await einvoice.convertFormat(invoice, testInvoice.targetFormat);
const conversionEnd = process.hrtime.bigint();
const duration = Number(conversionEnd - conversionStart) / 1_000_000;
results.conversions.push({
complexity: testInvoice.complexity,
from: testInvoice.format,
to: testInvoice.targetFormat,
duration,
size: invoiceSize,
success: true
});
results.totalBytes += invoiceSize;
} catch (error) {
results.conversions.push({
complexity: testInvoice.complexity,
from: testInvoice.format,
to: testInvoice.targetFormat,
duration: 0,
size: invoiceSize,
success: false
});
}
results.totalInvoices++;
}
results.totalTime = Date.now() - startTime;
// Calculate throughput metrics
const successfulConversions = results.conversions.filter(c => c.success);
const throughputStats = {
invoicesPerSecond: (successfulConversions.length / (results.totalTime / 1000)).toFixed(2),
bytesPerSecond: (results.totalBytes / (results.totalTime / 1000) / 1024).toFixed(2), // KB/s
avgConversionTime: successfulConversions.length > 0 ?
(successfulConversions.reduce((sum, c) => sum + c.duration, 0) / successfulConversions.length).toFixed(3) : 'N/A'
};
// Group by complexity
const complexityStats = ['simple', 'medium', 'complex'].map(complexity => {
const conversions = successfulConversions.filter(c => c.complexity === complexity);
return {
complexity,
count: conversions.length,
avgTime: conversions.length > 0 ?
(conversions.reduce((sum, c) => sum + c.duration, 0) / conversions.length).toFixed(3) : 'N/A'
};
});
return { ...results, throughputStats, complexityStats };
}
);
// Test 2: Parallel conversion throughput
const parallelThroughput = await performanceTracker.measureAsync(
'parallel-throughput',
async () => {
const einvoice = new EInvoice();
const results = [];
// Create a batch of invoices
const batchSize = 50;
const testInvoices = Array.from({ length: batchSize }, (_, i) => ({
format: i % 2 === 0 ? 'ubl' : 'cii' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `PARALLEL-${i + 1}`,
issueDate: '2024-02-05',
seller: { name: `Seller ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i}` },
buyer: { name: `Buyer ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i + 100}` },
items: Array.from({ length: 5 }, (_, j) => ({
description: `Item ${j + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 500, vatAmount: 50, grossAmount: 550 }
}
}));
// Test different parallelism levels
const parallelismLevels = [1, 2, 5, 10, 20];
for (const parallelism of parallelismLevels) {
const startTime = Date.now();
let completed = 0;
let failed = 0;
// Process in batches
for (let i = 0; i < testInvoices.length; i += parallelism) {
const batch = testInvoices.slice(i, i + parallelism);
const conversionPromises = batch.map(async (invoice) => {
try {
const targetFormat = invoice.format === 'ubl' ? 'cii' : 'ubl';
await einvoice.convertFormat(invoice, targetFormat);
return true;
} catch {
return false;
}
});
const batchResults = await Promise.all(conversionPromises);
completed += batchResults.filter(r => r).length;
failed += batchResults.filter(r => !r).length;
}
const totalTime = Date.now() - startTime;
const throughput = (completed / (totalTime / 1000)).toFixed(2);
results.push({
parallelism,
totalTime,
completed,
failed,
throughput: `${throughput} conversions/sec`,
avgTimePerConversion: (totalTime / batchSize).toFixed(3)
});
}
return results;
}
);
// Test 3: Corpus conversion throughput
const corpusThroughput = await performanceTracker.measureAsync(
'corpus-throughput',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
formatPairs: new Map<string, { count: number; totalTime: number; totalSize: number }>(),
overallStats: {
totalConversions: 0,
successfulConversions: 0,
totalTime: 0,
totalBytes: 0
}
};
// Sample corpus files
const sampleFiles = files.slice(0, 40);
const startTime = Date.now();
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const fileSize = Buffer.byteLength(content, 'utf-8');
// Detect and parse
const format = await einvoice.detectFormat(content);
if (!format || format === 'unknown') continue;
const invoice = await einvoice.parseInvoice(content, format);
// Determine target format
const targetFormat = format === 'ubl' ? 'cii' :
format === 'cii' ? 'ubl' :
format === 'zugferd' ? 'xrechnung' : 'ubl';
const pairKey = `${format}->${targetFormat}`;
// Measure conversion
const conversionStart = process.hrtime.bigint();
try {
await einvoice.convertFormat(invoice, targetFormat);
const conversionEnd = process.hrtime.bigint();
const duration = Number(conversionEnd - conversionStart) / 1_000_000;
// Update statistics
if (!results.formatPairs.has(pairKey)) {
results.formatPairs.set(pairKey, { count: 0, totalTime: 0, totalSize: 0 });
}
const pairStats = results.formatPairs.get(pairKey)!;
pairStats.count++;
pairStats.totalTime += duration;
pairStats.totalSize += fileSize;
results.overallStats.successfulConversions++;
results.overallStats.totalBytes += fileSize;
} catch (error) {
// Conversion failed
}
results.overallStats.totalConversions++;
} catch (error) {
// File processing failed
}
}
results.overallStats.totalTime = Date.now() - startTime;
// Calculate throughput by format pair
const formatPairStats = Array.from(results.formatPairs.entries()).map(([pair, stats]) => ({
pair,
count: stats.count,
avgTime: (stats.totalTime / stats.count).toFixed(3),
avgSize: (stats.totalSize / stats.count / 1024).toFixed(2), // KB
throughput: ((stats.totalSize / 1024) / (stats.totalTime / 1000)).toFixed(2) // KB/s
}));
return {
...results.overallStats,
successRate: ((results.overallStats.successfulConversions / results.overallStats.totalConversions) * 100).toFixed(1),
overallThroughput: {
invoicesPerSecond: (results.overallStats.successfulConversions / (results.overallStats.totalTime / 1000)).toFixed(2),
kbPerSecond: ((results.overallStats.totalBytes / 1024) / (results.overallStats.totalTime / 1000)).toFixed(2)
},
formatPairStats
};
}
);
// Test 4: Streaming conversion throughput
const streamingThroughput = await performanceTracker.measureAsync(
'streaming-throughput',
async () => {
const einvoice = new EInvoice();
const results = {
streamSize: 0,
processedInvoices: 0,
totalTime: 0,
peakMemory: 0,
errors: 0
};
// Simulate streaming scenario
const invoiceStream = Array.from({ length: 100 }, (_, i) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `STREAM-${i + 1}`,
issueDate: '2024-02-05',
seller: { name: `Stream Seller ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i}` },
buyer: { name: `Stream Buyer ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i + 1000}` },
items: Array.from({ length: Math.floor(Math.random() * 10) + 1 }, (_, j) => ({
description: `Stream Item ${j + 1}`,
quantity: Math.random() * 10,
unitPrice: Math.random() * 100,
vatRate: [5, 10, 20][Math.floor(Math.random() * 3)],
lineTotal: 0
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
}));
// Calculate totals
invoiceStream.forEach(invoice => {
invoice.data.items.forEach(item => {
item.lineTotal = item.quantity * item.unitPrice;
invoice.data.totals.netAmount += item.lineTotal;
invoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100);
});
invoice.data.totals.grossAmount = invoice.data.totals.netAmount + invoice.data.totals.vatAmount;
results.streamSize += JSON.stringify(invoice).length;
});
// Process stream
const startTime = Date.now();
const initialMemory = process.memoryUsage().heapUsed;
// Simulate streaming with chunks
const chunkSize = 10;
for (let i = 0; i < invoiceStream.length; i += chunkSize) {
const chunk = invoiceStream.slice(i, i + chunkSize);
// Process chunk in parallel
const chunkPromises = chunk.map(async (invoice) => {
try {
await einvoice.convertFormat(invoice, 'cii');
results.processedInvoices++;
} catch {
results.errors++;
}
});
await Promise.all(chunkPromises);
// Check memory usage
const currentMemory = process.memoryUsage().heapUsed;
if (currentMemory > results.peakMemory) {
results.peakMemory = currentMemory;
}
}
results.totalTime = Date.now() - startTime;
return {
...results,
throughput: {
invoicesPerSecond: (results.processedInvoices / (results.totalTime / 1000)).toFixed(2),
mbPerSecond: ((results.streamSize / 1024 / 1024) / (results.totalTime / 1000)).toFixed(2)
},
memoryIncreaseMB: ((results.peakMemory - initialMemory) / 1024 / 1024).toFixed(2),
successRate: ((results.processedInvoices / invoiceStream.length) * 100).toFixed(1)
};
}
);
// Test 5: Sustained throughput test
const sustainedThroughput = await performanceTracker.measureAsync(
'sustained-throughput',
async () => {
const einvoice = new EInvoice();
const testDuration = 10000; // 10 seconds
const results = {
secondlyThroughput: [],
totalConversions: 0,
minThroughput: Infinity,
maxThroughput: 0,
avgThroughput: 0
};
// Test invoice template
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'SUSTAINED-TEST',
issueDate: '2024-02-05',
seller: { name: 'Sustained Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Sustained Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: [{ description: 'Item', quantity: 1, unitPrice: 100, vatRate: 10, lineTotal: 100 }],
totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 }
}
};
const startTime = Date.now();
let currentSecond = 0;
let conversionsInCurrentSecond = 0;
while (Date.now() - startTime < testDuration) {
const elapsed = Date.now() - startTime;
const second = Math.floor(elapsed / 1000);
if (second > currentSecond) {
// Record throughput for completed second
results.secondlyThroughput.push(conversionsInCurrentSecond);
if (conversionsInCurrentSecond < results.minThroughput) {
results.minThroughput = conversionsInCurrentSecond;
}
if (conversionsInCurrentSecond > results.maxThroughput) {
results.maxThroughput = conversionsInCurrentSecond;
}
currentSecond = second;
conversionsInCurrentSecond = 0;
}
// Perform conversion
try {
await einvoice.convertFormat(testInvoice, 'cii');
conversionsInCurrentSecond++;
results.totalConversions++;
} catch {
// Conversion failed
}
}
// Calculate average
if (results.secondlyThroughput.length > 0) {
results.avgThroughput = results.secondlyThroughput.reduce((a, b) => a + b, 0) / results.secondlyThroughput.length;
}
return {
duration: Math.floor((Date.now() - startTime) / 1000),
totalConversions: results.totalConversions,
minThroughput: results.minThroughput === Infinity ? 0 : results.minThroughput,
maxThroughput: results.maxThroughput,
avgThroughput: results.avgThroughput.toFixed(2),
variance: results.secondlyThroughput.length > 0 ?
Math.sqrt(results.secondlyThroughput.reduce((sum, val) =>
sum + Math.pow(val - results.avgThroughput, 2), 0) / results.secondlyThroughput.length).toFixed(2) : 0
};
}
);
constructor(name: string) {
this.name = name;
}
// Summary
t.comment('\n=== PERF-04: Conversion Throughput Test Summary ===');
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`);
}
}
}
}
const performanceTracker = new SimplePerformanceTracker('PERF-04: Conversion Throughput');
// Helper to create test invoices
function createUblInvoice(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-04: UBL to CII conversion throughput', async () => {
const testCases = [
{ name: 'Small invoice', lineItems: 5 },
{ name: 'Medium invoice', lineItems: 20 },
{ name: 'Large invoice', lineItems: 100 }
];
t.comment('\nSingle-Thread Throughput:');
t.comment(` Total conversions: ${singleThreadThroughput.result.totalInvoices}`);
t.comment(` Successful: ${singleThreadThroughput.result.conversions.filter(c => c.success).length}`);
t.comment(` Total time: ${singleThreadThroughput.result.totalTime}ms`);
t.comment(` Throughput: ${singleThreadThroughput.result.throughputStats.invoicesPerSecond} invoices/sec`);
t.comment(` Data rate: ${singleThreadThroughput.result.throughputStats.bytesPerSecond} KB/sec`);
t.comment(' By complexity:');
singleThreadThroughput.result.complexityStats.forEach(stat => {
t.comment(` - ${stat.complexity}: ${stat.count} invoices, avg ${stat.avgTime}ms`);
});
const iterations = 30;
t.comment('\nParallel Throughput:');
parallelThroughput.result.forEach(result => {
t.comment(` ${result.parallelism} parallel: ${result.throughput}, avg ${result.avgTimePerConversion}ms/conversion`);
});
for (const testCase of testCases) {
const ublXml = createUblInvoice(`CONV-${testCase.name}`, testCase.lineItems);
const times: number[] = [];
let convertedXml: string = '';
for (let i = 0; i < iterations; i++) {
const einvoice = await EInvoice.fromXml(ublXml);
const startTime = performance.now();
convertedXml = await einvoice.toXmlString('cii');
const endTime = performance.now();
const duration = endTime - startTime;
times.push(duration);
performanceTracker.addMeasurement(`ubl-to-cii-${testCase.name}`, duration);
}
// Verify conversion worked
expect(convertedXml).toContain('CrossIndustryInvoice');
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const throughput = (ublXml.length / 1024) / (avg / 1000); // KB/s
console.log(`${testCase.name} (${testCase.lineItems} items): avg=${avg.toFixed(3)}ms, throughput=${throughput.toFixed(2)} KB/s`);
// Performance expectations
expect(avg).toBeLessThan(testCase.lineItems * 2 + 50); // Allow 2ms per line item + 50ms base
}
});
tap.test('PERF-04: CII to UBL conversion throughput', async () => {
// First create a CII invoice by converting from UBL
const ublXml = createUblInvoice('CII-SOURCE', 20);
const sourceInvoice = await EInvoice.fromXml(ublXml);
const ciiXml = await sourceInvoice.toXmlString('cii');
t.comment('\nCorpus Throughput:');
t.comment(` Total conversions: ${corpusThroughput.result.totalConversions}`);
t.comment(` Success rate: ${corpusThroughput.result.successRate}%`);
t.comment(` Overall: ${corpusThroughput.result.overallThroughput.invoicesPerSecond} invoices/sec, ${corpusThroughput.result.overallThroughput.kbPerSecond} KB/sec`);
t.comment(' By format pair:');
corpusThroughput.result.formatPairStats.slice(0, 5).forEach(stat => {
t.comment(` - ${stat.pair}: ${stat.count} conversions, ${stat.throughput} KB/sec`);
});
const iterations = 30;
const times: number[] = [];
let convertedXml: string = '';
t.comment('\nStreaming Throughput:');
t.comment(` Processed: ${streamingThroughput.result.processedInvoices}/${streamingThroughput.result.processedInvoices + streamingThroughput.result.errors} invoices`);
t.comment(` Success rate: ${streamingThroughput.result.successRate}%`);
t.comment(` Throughput: ${streamingThroughput.result.throughput.invoicesPerSecond} invoices/sec`);
t.comment(` Data rate: ${streamingThroughput.result.throughput.mbPerSecond} MB/sec`);
t.comment(` Peak memory increase: ${streamingThroughput.result.memoryIncreaseMB} MB`);
t.comment('\nSustained Throughput (10 seconds):');
t.comment(` Total conversions: ${sustainedThroughput.result.totalConversions}`);
t.comment(` Min throughput: ${sustainedThroughput.result.minThroughput} conversions/sec`);
t.comment(` Max throughput: ${sustainedThroughput.result.maxThroughput} conversions/sec`);
t.comment(` Avg throughput: ${sustainedThroughput.result.avgThroughput} conversions/sec`);
t.comment(` Std deviation: ${sustainedThroughput.result.variance}`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgThroughput = parseFloat(singleThreadThroughput.result.throughputStats.invoicesPerSecond);
const targetThroughput = 10; // Target: >10 conversions/sec
if (avgThroughput > targetThroughput) {
t.comment(`✅ Conversion throughput meets target: ${avgThroughput} > ${targetThroughput} conversions/sec`);
} else {
t.comment(`⚠️ Conversion throughput below target: ${avgThroughput} < ${targetThroughput} conversions/sec`);
for (let i = 0; i < iterations; i++) {
const einvoice = await EInvoice.fromXml(ciiXml);
const startTime = performance.now();
convertedXml = await einvoice.toXmlString('ubl');
const endTime = performance.now();
const duration = endTime - startTime;
times.push(duration);
performanceTracker.addMeasurement('cii-to-ubl', duration);
}
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
// Verify conversion worked
expect(convertedXml).toContain('urn:oasis:names:specification:ubl:schema:xsd:Invoice-2');
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`CII to UBL conversion: avg=${avg.toFixed(3)}ms`);
// CII to UBL should be reasonably fast
expect(avg).toBeLessThan(100);
});
t.end();
tap.test('PERF-04: Round-trip conversion performance', async () => {
const originalUbl = createUblInvoice('ROUND-TRIP', 10);
const iterations = 20;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const startTime = performance.now();
// UBL -> CII -> UBL
const invoice1 = await EInvoice.fromXml(originalUbl);
const ciiXml = await invoice1.toXmlString('cii');
const invoice2 = await EInvoice.fromXml(ciiXml);
const finalUbl = await invoice2.toXmlString('ubl');
const endTime = performance.now();
const duration = endTime - startTime;
times.push(duration);
performanceTracker.addMeasurement('round-trip', duration);
if (i === 0) {
// Verify data integrity
expect(finalUbl).toContain('ROUND-TRIP');
}
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`Round-trip conversion: avg=${avg.toFixed(3)}ms`);
// Round-trip should complete in reasonable time
expect(avg).toBeLessThan(150);
});
tap.test('PERF-04: Batch conversion throughput', async () => {
const batchSizes = [5, 10, 20];
for (const batchSize of batchSizes) {
// Create batch of invoices
const invoices = Array(batchSize).fill(null).map((_, i) =>
createUblInvoice(`BATCH-${i}`, 10)
);
const startTime = performance.now();
// Convert all invoices
const conversions = await Promise.all(
invoices.map(async (xml) => {
const einvoice = await EInvoice.fromXml(xml);
return einvoice.toXmlString('cii');
})
);
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTimePerInvoice = totalTime / batchSize;
console.log(`Batch of ${batchSize}: total=${totalTime.toFixed(2)}ms, avg per invoice=${avgTimePerInvoice.toFixed(2)}ms`);
performanceTracker.addMeasurement(`batch-${batchSize}`, avgTimePerInvoice);
// Verify all conversions succeeded
expect(conversions.every(xml => xml.includes('CrossIndustryInvoice'))).toEqual(true);
// Batch processing should be efficient
expect(avgTimePerInvoice).toBeLessThan(50);
}
});
tap.test('PERF-04: Format-specific optimizations', async () => {
const formats = ['ubl', 'cii', 'facturx', 'zugferd'] as const;
const ublSource = createUblInvoice('FORMAT-TEST', 20);
for (const targetFormat of formats) {
try {
const times: number[] = [];
for (let i = 0; i < 20; i++) {
const einvoice = await EInvoice.fromXml(ublSource);
const startTime = performance.now();
await einvoice.toXmlString(targetFormat);
const endTime = performance.now();
times.push(endTime - startTime);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`UBL to ${targetFormat}: avg=${avg.toFixed(3)}ms`);
performanceTracker.addMeasurement(`ubl-to-${targetFormat}`, avg);
// All conversions should be reasonably fast
expect(avg).toBeLessThan(100);
} catch (error) {
// Some formats might not be supported for all conversions
console.log(`UBL to ${targetFormat}: Not supported`);
}
}
});
tap.test('PERF-04: Memory efficiency during conversion', async () => {
const largeInvoice = createUblInvoice('MEMORY-TEST', 500); // Very large invoice
const initialMemory = process.memoryUsage();
// Perform multiple conversions
for (let i = 0; i < 10; i++) {
const einvoice = await EInvoice.fromXml(largeInvoice);
await einvoice.toXmlString('cii');
}
const finalMemory = process.memoryUsage();
const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log(`Memory increase after 10 large conversions: ${memoryIncrease.toFixed(2)} MB`);
// Memory usage should be reasonable
expect(memoryIncrease).toBeLessThan(100);
});
tap.test('PERF-04: Performance Summary', async () => {
performanceTracker.printSummary();
// Check overall performance
const ublToCiiStats = performanceTracker.getStats('ubl-to-cii-Small invoice');
if (ublToCiiStats) {
console.log(`\nSmall invoice UBL to CII conversion: avg=${ublToCiiStats.avg.toFixed(2)}ms`);
expect(ublToCiiStats.avg).toBeLessThan(30); // Small invoices should convert very quickly
}
console.log('\nConversion throughput performance tests completed successfully');
});
tap.start();