einvoice/test/suite/einvoice_performance/test.perf-04.conversion-throughput.ts

310 lines
10 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
/**
* @file test.perf-04.conversion-throughput.ts
* @description Performance tests for format conversion throughput
*/
2025-05-28 19:37:00 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
2025-05-25 19:45:37 +00:00
import { EInvoice } from '../../../ts/index.js';
2025-05-28 19:37:00 +00:00
// Simple performance tracking
class SimplePerformanceTracker {
private measurements: Map<string, number[]> = new Map();
private name: string;
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
constructor(name: string) {
this.name = name;
}
addMeasurement(key: string, time: number): void {
if (!this.measurements.has(key)) {
this.measurements.set(key, []);
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
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`);
2025-05-25 19:45:37 +00:00
}
}
2025-05-28 19:37:00 +00:00
}
}
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 }
];
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const iterations = 30;
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);
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const startTime = performance.now();
convertedXml = await einvoice.toXmlString('cii');
const endTime = performance.now();
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const duration = endTime - startTime;
times.push(duration);
performanceTracker.addMeasurement(`ubl-to-cii-${testCase.name}`, duration);
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
// 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');
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const iterations = 30;
const times: number[] = [];
let convertedXml: string = '';
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);
}
// 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);
});
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');
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
}
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
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[] = [];
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
for (let i = 0; i < 20; i++) {
const einvoice = await EInvoice.fromXml(ublSource);
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const startTime = performance.now();
await einvoice.toXmlString(targetFormat);
const endTime = performance.now();
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
times.push(endTime - startTime);
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
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);
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
// 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`);
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
}
});
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
tap.test('PERF-04: Memory efficiency during conversion', async () => {
const largeInvoice = createUblInvoice('MEMORY-TEST', 500); // Very large invoice
const initialMemory = process.memoryUsage();
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
// Perform multiple conversions
for (let i = 0; i < 10; i++) {
const einvoice = await EInvoice.fromXml(largeInvoice);
await einvoice.toXmlString('cii');
}
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
const finalMemory = process.memoryUsage();
const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
console.log(`Memory increase after 10 large conversions: ${memoryIncrease.toFixed(2)} MB`);
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
// Memory usage should be reasonable
expect(memoryIncrease).toBeLessThan(100);
});
tap.test('PERF-04: Performance Summary', async () => {
performanceTracker.printSummary();
2025-05-25 19:45:37 +00:00
2025-05-28 19:37:00 +00:00
// 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
2025-05-25 19:45:37 +00:00
}
2025-05-28 19:37:00 +00:00
console.log('\nConversion throughput performance tests completed successfully');
2025-05-25 19:45:37 +00:00
});
tap.start();