einvoice/test/suite/einvoice_performance/test.perf-07.concurrent-processing.ts

360 lines
14 KiB
TypeScript
Raw Normal View History

2025-05-25 19:45:37 +00:00
/**
* @file test.perf-07.concurrent-processing.ts
* @description Performance tests for concurrent processing capabilities
*/
import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../../plugins.js';
import { EInvoice } from '../../../ts/index.js';
2025-05-29 13:35:36 +00:00
import { FormatDetector } from '../../../ts/formats/utils/format.detector.js';
2025-05-25 19:45:37 +00:00
import { CorpusLoader } from '../../suite/corpus.loader.js';
import { PerformanceTracker } from '../../suite/performance.tracker.js';
import * as os from 'os';
const performanceTracker = new PerformanceTracker('PERF-07: Concurrent Processing');
2025-05-29 13:35:36 +00:00
tap.test('PERF-07: Concurrent Processing - should handle concurrent operations efficiently', async () => {
2025-05-25 19:45:37 +00:00
// Test 1: Concurrent format detection
2025-05-29 13:35:36 +00:00
await performanceTracker.measureAsync(
2025-05-25 19:45:37 +00:00
'concurrent-format-detection',
async () => {
// Create test data with different formats
const testData = [
...Array(25).fill(null).map((_, i) => ({
id: `ubl-${i}`,
content: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>UBL-${i}</ID></Invoice>`
})),
...Array(25).fill(null).map((_, i) => ({
id: `cii-${i}`,
content: `<?xml version="1.0"?><rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"><rsm:ExchangedDocument><ram:ID>CII-${i}</ram:ID></rsm:ExchangedDocument></rsm:CrossIndustryInvoice>`
})),
...Array(25).fill(null).map((_, i) => ({
id: `unknown-${i}`,
content: `<?xml version="1.0"?><UnknownRoot><ID>UNKNOWN-${i}</ID></UnknownRoot>`
}))
];
// Test different concurrency levels
2025-05-29 13:35:36 +00:00
const levels = [1, 4, 8, 16, 32];
console.log('\nConcurrent Format Detection:');
console.log('Concurrency | Duration | Throughput | Accuracy');
console.log('------------|----------|------------|----------');
2025-05-25 19:45:37 +00:00
for (const concurrency of levels) {
const startTime = Date.now();
let completed = 0;
let correct = 0;
// Process in batches
2025-05-29 13:35:36 +00:00
for (let i = 0; i < testData.length; i += concurrency) {
const batch = testData.slice(i, i + concurrency);
2025-05-25 19:45:37 +00:00
const promises = batch.map(async (item) => {
2025-05-29 13:35:36 +00:00
const format = await FormatDetector.detectFormat(item.content);
2025-05-25 19:45:37 +00:00
completed++;
// Verify correctness
if ((item.id.startsWith('ubl') && format === 'ubl') ||
(item.id.startsWith('cii') && format === 'cii') ||
(item.id.startsWith('unknown') && format === 'unknown')) {
correct++;
}
return format;
});
await Promise.all(promises);
}
const duration = Date.now() - startTime;
const throughput = (completed / (duration / 1000));
2025-05-29 13:35:36 +00:00
const accuracy = ((correct / completed) * 100).toFixed(2);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log(`${String(concurrency).padEnd(11)} | ${String(duration + 'ms').padEnd(8)} | ${throughput.toFixed(2).padEnd(10)}/s | ${accuracy}%`);
2025-05-25 19:45:37 +00:00
}
}
);
// Test 2: Concurrent validation
2025-05-29 13:35:36 +00:00
await performanceTracker.measureAsync(
2025-05-25 19:45:37 +00:00
'concurrent-validation',
async () => {
2025-05-29 13:35:36 +00:00
console.log('\nConcurrent Validation:');
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
// Create test invoice XMLs
const createInvoiceXml = (id: number, itemCount: number) => {
const items = Array.from({ length: itemCount }, (_, i) => `
<cac:InvoiceLine>
<cbc:ID>${i + 1}</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Item ${i + 1}</cbc:Description>
</cac:Item>
</cac:InvoiceLine>`).join('');
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
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>INV-${id}</cbc:ID>
<cbc:IssueDate>2024-02-20</cbc:IssueDate>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Seller</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Buyer</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:LegalMonetaryTotal>
<cbc:TaxExclusiveAmount currencyID="USD">${(itemCount * 100).toFixed(2)}</cbc:TaxExclusiveAmount>
<cbc:PayableAmount currencyID="USD">${(itemCount * 100).toFixed(2)}</cbc:PayableAmount>
</cac:LegalMonetaryTotal>${items}
</Invoice>`;
2025-05-25 19:45:37 +00:00
};
// Test scenarios
const scenarios = [
2025-05-29 13:35:36 +00:00
{ name: 'Small invoices (5 items)', count: 30, itemCount: 5 },
{ name: 'Medium invoices (20 items)', count: 20, itemCount: 20 },
{ name: 'Large invoices (50 items)', count: 10, itemCount: 50 }
2025-05-25 19:45:37 +00:00
];
for (const scenario of scenarios) {
2025-05-29 13:35:36 +00:00
console.log(`\n${scenario.name}:`);
const invoices = Array.from({ length: scenario.count }, (_, i) =>
createInvoiceXml(i, scenario.itemCount)
);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
const concurrency = 8;
2025-05-25 19:45:37 +00:00
const startTime = Date.now();
2025-05-29 13:35:36 +00:00
let validCount = 0;
2025-05-25 19:45:37 +00:00
// Process concurrently
for (let i = 0; i < invoices.length; i += concurrency) {
const batch = invoices.slice(i, i + concurrency);
2025-05-29 13:35:36 +00:00
const results = await Promise.all(
batch.map(async (invoiceXml) => {
try {
const einvoice = await EInvoice.fromXml(invoiceXml);
const result = await einvoice.validate();
return result.isValid;
} catch {
return false;
}
2025-05-25 19:45:37 +00:00
})
);
2025-05-29 13:35:36 +00:00
validCount += results.filter(v => v).length;
2025-05-25 19:45:37 +00:00
}
2025-05-29 13:35:36 +00:00
const duration = Date.now() - startTime;
const throughput = (scenario.count / (duration / 1000)).toFixed(2);
const validationRate = ((validCount / scenario.count) * 100).toFixed(2);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log(` - Processed: ${scenario.count} invoices`);
console.log(` - Duration: ${duration}ms`);
console.log(` - Throughput: ${throughput} invoices/sec`);
console.log(` - Validation rate: ${validationRate}%`);
2025-05-25 19:45:37 +00:00
}
}
);
// Test 3: Concurrent file processing
2025-05-29 13:35:36 +00:00
await performanceTracker.measureAsync(
2025-05-25 19:45:37 +00:00
'concurrent-file-processing',
async () => {
2025-05-29 13:35:36 +00:00
console.log('\nConcurrent File Processing:');
const testDataset = await CorpusLoader.createTestDataset({
formats: ['UBL', 'CII'],
maxFiles: 50,
validOnly: true
});
const files = testDataset.map(f => f.path).filter(p => p.endsWith('.xml'));
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log(`Processing ${files.length} files from corpus...`);
2025-05-25 19:45:37 +00:00
// Test different concurrency strategies
const strategies = [
{ name: 'Sequential', concurrency: 1 },
{ name: 'Moderate', concurrency: 8 },
2025-05-29 13:35:36 +00:00
{ name: 'Aggressive', concurrency: 16 }
2025-05-25 19:45:37 +00:00
];
for (const strategy of strategies) {
const startTime = Date.now();
let processed = 0;
let errors = 0;
// Process files with specified concurrency
2025-05-29 13:35:36 +00:00
const queue = [...files];
const activePromises = new Set<Promise<void>>();
2025-05-25 19:45:37 +00:00
while (queue.length > 0 || activePromises.size > 0) {
// Start new tasks up to concurrency limit
while (activePromises.size < strategy.concurrency && queue.length > 0) {
const file = queue.shift()!;
const promise = (async () => {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
2025-05-29 13:35:36 +00:00
const format = await FormatDetector.detectFormat(content);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
if (format && format !== 'unknown' && format !== 'pdf' && format !== 'xml') {
try {
const invoice = await EInvoice.fromXml(content);
await invoice.validate();
processed++;
} catch {
// Skip unparseable files
}
2025-05-25 19:45:37 +00:00
}
2025-05-29 13:35:36 +00:00
} catch {
2025-05-25 19:45:37 +00:00
errors++;
}
})();
activePromises.add(promise);
promise.finally(() => activePromises.delete(promise));
}
// Wait for at least one to complete
if (activePromises.size > 0) {
await Promise.race(activePromises);
}
}
const duration = Date.now() - startTime;
2025-05-29 13:35:36 +00:00
const throughput = (processed / (duration / 1000)).toFixed(2);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log(`\n${strategy.name} (concurrency: ${strategy.concurrency}):`);
console.log(` - Duration: ${duration}ms`);
console.log(` - Processed: ${processed} files`);
console.log(` - Throughput: ${throughput} files/sec`);
console.log(` - Errors: ${errors}`);
2025-05-25 19:45:37 +00:00
}
}
);
2025-05-29 13:35:36 +00:00
// Test 4: Mixed operations
await performanceTracker.measureAsync(
'mixed-operations',
2025-05-25 19:45:37 +00:00
async () => {
2025-05-29 13:35:36 +00:00
console.log('\nMixed Operations Concurrency:');
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
// Define operations
2025-05-25 19:45:37 +00:00
const operations = [
{
name: 'detect',
2025-05-29 13:35:36 +00:00
fn: async () => {
const xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>TEST</ID></Invoice>`;
return await FormatDetector.detectFormat(xml);
2025-05-25 19:45:37 +00:00
}
},
{
name: 'parse',
2025-05-29 13:35:36 +00:00
fn: async () => {
const xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>TEST</ID><IssueDate>2024-01-01</IssueDate></Invoice>`;
const invoice = await EInvoice.fromXml(xml);
return invoice.getFormat();
2025-05-25 19:45:37 +00:00
}
},
{
name: 'validate',
2025-05-29 13:35:36 +00:00
fn: async () => {
const xml = `<?xml version="1.0"?><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>TEST</cbc:ID>
<cbc:IssueDate>2024-02-20</cbc:IssueDate>
<cac:AccountingSupplierParty><cac:Party><cac:PartyName><cbc:Name>Seller</cbc:Name></cac:PartyName></cac:Party></cac:AccountingSupplierParty>
<cac:AccountingCustomerParty><cac:Party><cac:PartyName><cbc:Name>Buyer</cbc:Name></cac:PartyName></cac:Party></cac:AccountingCustomerParty>
</Invoice>`;
const invoice = await EInvoice.fromXml(xml);
return await invoice.validate();
2025-05-25 19:45:37 +00:00
}
}
];
// Test mixed workload
2025-05-29 13:35:36 +00:00
const totalOperations = 150;
2025-05-25 19:45:37 +00:00
const operationMix = Array.from({ length: totalOperations }, (_, i) => ({
operation: operations[i % operations.length],
id: i
}));
2025-05-29 13:35:36 +00:00
const concurrency = 10;
const startTime = Date.now();
const operationCounts = new Map(operations.map(op => [op.name, 0]));
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
// Process operations
for (let i = 0; i < operationMix.length; i += concurrency) {
const batch = operationMix.slice(i, i + concurrency);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
await Promise.all(batch.map(async ({ operation }) => {
try {
await operation.fn();
operationCounts.set(operation.name, operationCounts.get(operation.name)! + 1);
} catch {
// Ignore errors
2025-05-25 19:45:37 +00:00
}
2025-05-29 13:35:36 +00:00
}));
}
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
const totalDuration = Date.now() - startTime;
const throughput = (totalOperations / (totalDuration / 1000)).toFixed(2);
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log(` Total operations: ${totalOperations}`);
console.log(` Duration: ${totalDuration}ms`);
console.log(` Throughput: ${throughput} ops/sec`);
console.log(` Operation breakdown:`);
operationCounts.forEach((count, name) => {
console.log(` - ${name}: ${count} operations`);
});
2025-05-25 19:45:37 +00:00
}
);
2025-05-29 13:35:36 +00:00
// Test 5: Resource contention
await performanceTracker.measureAsync(
'resource-contention',
2025-05-25 19:45:37 +00:00
async () => {
2025-05-29 13:35:36 +00:00
console.log('\nResource Contention Test:');
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
const xml = `<?xml version="1.0"?><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>CONTENTION-TEST</cbc:ID>
<cbc:IssueDate>2024-02-20</cbc:IssueDate>
<cac:AccountingSupplierParty><cac:Party><cac:PartyName><cbc:Name>Seller</cbc:Name></cac:PartyName></cac:Party></cac:AccountingSupplierParty>
<cac:AccountingCustomerParty><cac:Party><cac:PartyName><cbc:Name>Buyer</cbc:Name></cac:PartyName></cac:Party></cac:AccountingCustomerParty>
</Invoice>`;
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
const concurrencyLevels = [1, 10, 50, 100];
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
console.log('Concurrency | Duration | Throughput');
console.log('------------|----------|------------');
for (const level of concurrencyLevels) {
const start = Date.now();
const promises = Array(level).fill(null).map(async () => {
const invoice = await EInvoice.fromXml(xml);
return invoice.validate();
});
2025-05-25 19:45:37 +00:00
2025-05-29 13:35:36 +00:00
await Promise.all(promises);
const duration = Date.now() - start;
const throughput = (level / (duration / 1000)).toFixed(2);
console.log(`${String(level).padEnd(11)} | ${String(duration + 'ms').padEnd(8)} | ${throughput} ops/sec`);
2025-05-25 19:45:37 +00:00
}
}
);
2025-05-29 13:35:36 +00:00
// Overall summary
console.log('\n=== PERF-07: Overall Performance Summary ===');
console.log(performanceTracker.getSummary());
2025-05-25 19:45:37 +00:00
});
tap.start();