310 lines
10 KiB
TypeScript
310 lines
10 KiB
TypeScript
/**
|
|
* @file test.perf-04.conversion-throughput.ts
|
|
* @description Performance tests for format conversion throughput
|
|
*/
|
|
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
|
|
// Simple performance tracking
|
|
class SimplePerformanceTracker {
|
|
private measurements: Map<string, number[]> = 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);
|
|
}
|
|
|
|
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 }
|
|
];
|
|
|
|
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);
|
|
|
|
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');
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
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(); |