583 lines
22 KiB
TypeScript
583 lines
22 KiB
TypeScript
|
/**
|
||
|
* @file test.perf-04.conversion-throughput.ts
|
||
|
* @description Performance tests for format conversion throughput
|
||
|
*/
|
||
|
|
||
|
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('PERF-04: Conversion Throughput');
|
||
|
|
||
|
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
|
||
|
};
|
||
|
}
|
||
|
);
|
||
|
|
||
|
// Summary
|
||
|
t.comment('\n=== PERF-04: Conversion Throughput Test Summary ===');
|
||
|
|
||
|
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`);
|
||
|
});
|
||
|
|
||
|
t.comment('\nParallel Throughput:');
|
||
|
parallelThroughput.result.forEach(result => {
|
||
|
t.comment(` ${result.parallelism} parallel: ${result.throughput}, avg ${result.avgTimePerConversion}ms/conversion`);
|
||
|
});
|
||
|
|
||
|
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`);
|
||
|
});
|
||
|
|
||
|
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`);
|
||
|
}
|
||
|
|
||
|
// Overall performance summary
|
||
|
t.comment('\n=== Overall Performance Summary ===');
|
||
|
performanceTracker.logSummary();
|
||
|
|
||
|
t.end();
|
||
|
});
|
||
|
|
||
|
tap.start();
|