This commit is contained in:
2025-05-25 19:45:37 +00:00
parent e89675c319
commit 39942638d9
110 changed files with 49183 additions and 3104 deletions

View File

@ -0,0 +1,386 @@
/**
* @file test.perf-01.detection-speed.ts
* @description Performance tests for format detection speed
*/
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-01: Format Detection Speed');
tap.test('PERF-01: Format Detection Speed - should meet performance targets for format detection', async (t) => {
// Test 1: Single file detection benchmarks
const singleFileDetection = await performanceTracker.measureAsync(
'single-file-detection',
async () => {
const einvoice = new EInvoice();
const benchmarks = [];
// Test different format samples
const testCases = [
{
name: 'Small UBL',
content: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-001</ID>
<IssueDate>2024-01-01</IssueDate>
</Invoice>`,
expectedFormat: 'ubl'
},
{
name: 'Small CII',
content: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument><ram:ID>TEST-002</ram:ID></rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`,
expectedFormat: 'cii'
},
{
name: 'Large UBL',
content: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-003</ID>
<IssueDate>2024-01-01</IssueDate>
${Array(100).fill('<InvoiceLine><ID>Line</ID></InvoiceLine>').join('\n')}
</Invoice>`,
expectedFormat: 'ubl'
}
];
// Run multiple iterations for accuracy
const iterations = 100;
for (const testCase of testCases) {
const times = [];
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
const format = await einvoice.detectFormat(testCase.content);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000; // Convert to ms
times.push(duration);
if (i === 0 && format !== testCase.expectedFormat) {
t.comment(`Warning: ${testCase.name} detected as ${format}, expected ${testCase.expectedFormat}`);
}
}
// Calculate statistics
times.sort((a, b) => a - b);
const stats = {
name: testCase.name,
min: times[0],
max: times[times.length - 1],
avg: times.reduce((a, b) => a + b, 0) / times.length,
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)],
p99: times[Math.floor(times.length * 0.99)]
};
benchmarks.push(stats);
}
return benchmarks;
}
);
// Test 2: Corpus detection performance
const corpusDetection = await performanceTracker.measureAsync(
'corpus-detection-performance',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
totalFiles: 0,
detectionTimes: [],
formatDistribution: new Map<string, number>(),
sizeCategories: {
small: { count: 0, avgTime: 0, times: [] }, // < 10KB
medium: { count: 0, avgTime: 0, times: [] }, // 10-100KB
large: { count: 0, avgTime: 0, times: [] }, // > 100KB
},
failures: 0
};
// Process sample of corpus files
const sampleFiles = files.slice(0, 100);
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const fileSize = Buffer.byteLength(content, 'utf-8');
const sizeCategory = fileSize < 10240 ? 'small' :
fileSize < 102400 ? 'medium' : 'large';
results.totalFiles++;
// Measure detection time
const startTime = process.hrtime.bigint();
const format = await einvoice.detectFormat(content);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.detectionTimes.push(duration);
results.sizeCategories[sizeCategory].times.push(duration);
results.sizeCategories[sizeCategory].count++;
// Track format distribution
if (format && format !== 'unknown') {
results.formatDistribution.set(format,
(results.formatDistribution.get(format) || 0) + 1
);
} else {
results.failures++;
}
} catch (error) {
results.failures++;
}
}
// Calculate averages
for (const category of Object.keys(results.sizeCategories)) {
const cat = results.sizeCategories[category];
if (cat.times.length > 0) {
cat.avgTime = cat.times.reduce((a, b) => a + b, 0) / cat.times.length;
}
}
// Overall statistics
results.detectionTimes.sort((a, b) => a - b);
const overallStats = {
min: results.detectionTimes[0],
max: results.detectionTimes[results.detectionTimes.length - 1],
avg: results.detectionTimes.reduce((a, b) => a + b, 0) / results.detectionTimes.length,
median: results.detectionTimes[Math.floor(results.detectionTimes.length / 2)],
p95: results.detectionTimes[Math.floor(results.detectionTimes.length * 0.95)]
};
return {
...results,
overallStats,
formatDistribution: Array.from(results.formatDistribution.entries())
};
}
);
// Test 3: Concurrent detection performance
const concurrentDetection = await performanceTracker.measureAsync(
'concurrent-detection',
async () => {
const einvoice = new EInvoice();
const concurrencyLevels = [1, 5, 10, 20, 50];
const results = [];
// Create test content
const testContent = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CONCURRENT-TEST</ID>
<IssueDate>2024-01-01</IssueDate>
<AccountingSupplierParty><Party><PartyName><Name>Test Supplier</Name></PartyName></Party></AccountingSupplierParty>
<AccountingCustomerParty><Party><PartyName><Name>Test Customer</Name></PartyName></Party></AccountingCustomerParty>
</Invoice>`;
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
// Create concurrent detection tasks
const tasks = Array(concurrency).fill(null).map(() =>
einvoice.detectFormat(testContent)
);
const detectionResults = await Promise.all(tasks);
const endTime = Date.now();
const duration = endTime - startTime;
const throughput = (concurrency / (duration / 1000)).toFixed(2);
results.push({
concurrency,
duration,
throughput: `${throughput} detections/sec`,
allSuccessful: detectionResults.every(r => r === 'ubl')
});
}
return results;
}
);
// Test 4: Edge case detection performance
const edgeCaseDetection = await performanceTracker.measureAsync(
'edge-case-detection',
async () => {
const einvoice = new EInvoice();
const edgeCases = [
{
name: 'Minimal XML',
content: '<?xml version="1.0"?><root/>'
},
{
name: 'No XML declaration',
content: '<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>1</ID></Invoice>'
},
{
name: 'With comments',
content: '<?xml version="1.0"?><!-- Comment --><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><!-- Another comment --><ID>1</ID></Invoice>'
},
{
name: 'With processing instructions',
content: '<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="style.xsl"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>1</ID></Invoice>'
},
{
name: 'Mixed namespaces',
content: '<?xml version="1.0"?><ns1:Invoice xmlns:ns1="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:ns2="http://example.com"><ns1:ID>1</ns1:ID></ns1:Invoice>'
},
{
name: 'Large with whitespace',
content: '<?xml version="1.0"?>\n\n\n' + ' '.repeat(10000) + '<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">\n' + ' '.repeat(5000) + '<ID>1</ID>\n' + ' '.repeat(5000) + '</Invoice>'
}
];
const results = [];
for (const edgeCase of edgeCases) {
const times = [];
const iterations = 50;
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
const format = await einvoice.detectFormat(edgeCase.content);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
times.push(duration);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.push({
name: edgeCase.name,
avgTime: avgTime.toFixed(3),
contentSize: edgeCase.content.length
});
}
return results;
}
);
// Test 5: Performance under memory pressure
const memoryPressureDetection = await performanceTracker.measureAsync(
'memory-pressure-detection',
async () => {
const einvoice = new EInvoice();
const results = {
baseline: null,
underPressure: null,
degradation: null
};
// Baseline measurement
const baselineTimes = [];
const testXml = '<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>MEM-TEST</ID></Invoice>';
for (let i = 0; i < 50; i++) {
const start = process.hrtime.bigint();
await einvoice.detectFormat(testXml);
const end = process.hrtime.bigint();
baselineTimes.push(Number(end - start) / 1_000_000);
}
results.baseline = baselineTimes.reduce((a, b) => a + b, 0) / baselineTimes.length;
// Create memory pressure by allocating large arrays
const memoryHogs = [];
for (let i = 0; i < 10; i++) {
memoryHogs.push(new Array(1_000_000).fill(Math.random()));
}
// Measurement under pressure
const pressureTimes = [];
for (let i = 0; i < 50; i++) {
const start = process.hrtime.bigint();
await einvoice.detectFormat(testXml);
const end = process.hrtime.bigint();
pressureTimes.push(Number(end - start) / 1_000_000);
}
results.underPressure = pressureTimes.reduce((a, b) => a + b, 0) / pressureTimes.length;
results.degradation = ((results.underPressure - results.baseline) / results.baseline * 100).toFixed(2) + '%';
// Cleanup
memoryHogs.length = 0;
return results;
}
);
// Summary
t.comment('\n=== PERF-01: Format Detection Speed Test Summary ===');
t.comment('\nSingle File Detection Benchmarks (100 iterations each):');
singleFileDetection.result.forEach(bench => {
t.comment(` ${bench.name}:`);
t.comment(` - Min: ${bench.min.toFixed(3)}ms, Max: ${bench.max.toFixed(3)}ms`);
t.comment(` - Avg: ${bench.avg.toFixed(3)}ms, Median: ${bench.median.toFixed(3)}ms`);
t.comment(` - P95: ${bench.p95.toFixed(3)}ms, P99: ${bench.p99.toFixed(3)}ms`);
});
t.comment(`\nCorpus Detection Performance (${corpusDetection.result.totalFiles} files):`);
t.comment(` Overall statistics:`);
t.comment(` - Min: ${corpusDetection.result.overallStats.min.toFixed(3)}ms`);
t.comment(` - Max: ${corpusDetection.result.overallStats.max.toFixed(3)}ms`);
t.comment(` - Avg: ${corpusDetection.result.overallStats.avg.toFixed(3)}ms`);
t.comment(` - Median: ${corpusDetection.result.overallStats.median.toFixed(3)}ms`);
t.comment(` - P95: ${corpusDetection.result.overallStats.p95.toFixed(3)}ms`);
t.comment(` By file size:`);
Object.entries(corpusDetection.result.sizeCategories).forEach(([size, data]: [string, any]) => {
if (data.count > 0) {
t.comment(` - ${size}: ${data.count} files, avg ${data.avgTime.toFixed(3)}ms`);
}
});
t.comment(` Format distribution:`);
corpusDetection.result.formatDistribution.forEach(([format, count]) => {
t.comment(` - ${format}: ${count} files`);
});
t.comment('\nConcurrent Detection Performance:');
concurrentDetection.result.forEach(result => {
t.comment(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput}`);
});
t.comment('\nEdge Case Detection:');
edgeCaseDetection.result.forEach(result => {
t.comment(` ${result.name} (${result.contentSize} bytes): ${result.avgTime}ms avg`);
});
t.comment('\nMemory Pressure Impact:');
t.comment(` Baseline: ${memoryPressureDetection.result.baseline.toFixed(3)}ms`);
t.comment(` Under pressure: ${memoryPressureDetection.result.underPressure.toFixed(3)}ms`);
t.comment(` Performance degradation: ${memoryPressureDetection.result.degradation}`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgDetectionTime = corpusDetection.result.overallStats.avg;
const targetTime = 10; // Target: <10ms for format detection
if (avgDetectionTime < targetTime) {
t.comment(`✅ Format detection meets target: ${avgDetectionTime.toFixed(3)}ms < ${targetTime}ms`);
} else {
t.comment(`⚠️ Format detection exceeds target: ${avgDetectionTime.toFixed(3)}ms > ${targetTime}ms`);
}
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,518 @@
/**
* @file test.perf-02.validation-performance.ts
* @description Performance tests for invoice validation operations
*/
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-02: Validation Performance');
tap.test('PERF-02: Validation Performance - should meet performance targets for validation operations', async (t) => {
// Test 1: Syntax validation performance
const syntaxValidation = await performanceTracker.measureAsync(
'syntax-validation-performance',
async () => {
const einvoice = new EInvoice();
const results = [];
// Create test invoices of varying complexity
const testInvoices = [
{
name: 'Minimal Invoice',
invoice: {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-VAL-001',
issueDate: '2024-02-01',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: '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 }
}
}
},
{
name: 'Standard Invoice (10 items)',
invoice: {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-VAL-002',
issueDate: '2024-02-01',
dueDate: '2024-03-01',
currency: 'EUR',
seller: {
name: 'Complex Seller GmbH',
address: 'Hauptstraße 123',
city: 'Berlin',
postalCode: '10115',
country: 'DE',
taxId: 'DE123456789',
email: 'info@seller.de',
phone: '+49 30 12345678'
},
buyer: {
name: 'Complex Buyer Ltd',
address: 'Business Park 456',
city: 'Munich',
postalCode: '80331',
country: 'DE',
taxId: 'DE987654321',
email: 'ap@buyer.de'
},
items: Array.from({ length: 10 }, (_, i) => ({
description: `Product Line ${i + 1}`,
quantity: i + 1,
unitPrice: 50.00 + i * 10,
vatRate: 19,
lineTotal: (i + 1) * (50.00 + i * 10),
itemId: `ITEM-${i + 1}`
})),
totals: {
netAmount: 1650.00,
vatAmount: 313.50,
grossAmount: 1963.50
}
}
}
},
{
name: 'Complex Invoice (50 items)',
invoice: {
format: 'cii' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PERF-VAL-003',
issueDate: '2024-02-01',
seller: { name: 'Mega Seller', address: 'Complex Street', country: 'FR', taxId: 'FR12345678901' },
buyer: { name: 'Mega Buyer', address: 'Complex Avenue', country: 'FR', taxId: 'FR98765432109' },
items: Array.from({ length: 50 }, (_, i) => ({
description: `Complex Item ${i + 1} with detailed specifications`,
quantity: Math.floor(Math.random() * 10) + 1,
unitPrice: Math.random() * 500,
vatRate: [5.5, 10, 20][i % 3],
lineTotal: 0 // Will be calculated
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
}
}
];
// Calculate totals for complex invoice
testInvoices[2].invoice.data.items.forEach(item => {
item.lineTotal = item.quantity * item.unitPrice;
testInvoices[2].invoice.data.totals.netAmount += item.lineTotal;
testInvoices[2].invoice.data.totals.vatAmount += item.lineTotal * (item.vatRate / 100);
});
testInvoices[2].invoice.data.totals.grossAmount =
testInvoices[2].invoice.data.totals.netAmount + testInvoices[2].invoice.data.totals.vatAmount;
// Run validation benchmarks
for (const test of testInvoices) {
const times = [];
const iterations = 50;
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
const validationResult = await einvoice.validateInvoice(test.invoice, { level: 'syntax' });
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
times.push(duration);
}
times.sort((a, b) => a - b);
results.push({
name: test.name,
itemCount: test.invoice.data.items.length,
min: times[0],
max: times[times.length - 1],
avg: times.reduce((a, b) => a + b, 0) / times.length,
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)]
});
}
return results;
}
);
// Test 2: Business rule validation performance
const businessRuleValidation = await performanceTracker.measureAsync(
'business-rule-validation',
async () => {
const einvoice = new EInvoice();
const results = {
ruleCategories: [],
totalRulesChecked: 0,
avgTimePerRule: 0
};
// Create test invoice with various business rule scenarios
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'BR-TEST-001',
issueDate: '2024-02-01',
dueDate: '2024-03-01',
currency: 'EUR',
seller: {
name: 'Business Rule Test Seller',
address: 'Test Street 1',
city: 'Berlin',
country: 'DE',
taxId: 'DE123456789',
registrationNumber: 'HRB12345'
},
buyer: {
name: 'Business Rule Test Buyer',
address: 'Test Avenue 2',
city: 'Paris',
country: 'FR',
taxId: 'FR98765432109'
},
items: [
{
description: 'Standard Product',
quantity: 10,
unitPrice: 100.00,
vatRate: 19,
lineTotal: 1000.00
},
{
description: 'Reduced VAT Product',
quantity: 5,
unitPrice: 50.00,
vatRate: 7,
lineTotal: 250.00
},
{
description: 'Zero VAT Export',
quantity: 2,
unitPrice: 200.00,
vatRate: 0,
lineTotal: 400.00
}
],
totals: {
netAmount: 1650.00,
vatAmount: 207.50,
grossAmount: 1857.50
},
paymentTerms: 'Net 30 days',
paymentMeans: {
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX'
}
}
};
// Test different validation rule sets
const ruleSets = [
{ name: 'BR-CO (Calculations)', rules: ['BR-CO-*'] },
{ name: 'BR-CL (Codelists)', rules: ['BR-CL-*'] },
{ name: 'BR-S (VAT)', rules: ['BR-S-*'] },
{ name: 'BR-DE (Germany)', rules: ['BR-DE-*'] },
{ name: 'All Rules', rules: ['*'] }
];
for (const ruleSet of ruleSets) {
const times = [];
const iterations = 20;
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
const validationResult = await einvoice.validateInvoice(testInvoice, {
level: 'business',
rules: ruleSet.rules
});
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
times.push(duration);
if (i === 0) {
results.totalRulesChecked += validationResult.rulesChecked || 0;
}
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.ruleCategories.push({
name: ruleSet.name,
avgTime: avgTime.toFixed(3),
rulesPerMs: ((validationResult.rulesChecked || 1) / avgTime).toFixed(2)
});
}
return results;
}
);
// Test 3: Corpus validation performance
const corpusValidation = await performanceTracker.measureAsync(
'corpus-validation-performance',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
totalFiles: 0,
validationTimes: {
syntax: [],
semantic: [],
business: []
},
formatPerformance: new Map<string, { count: number; totalTime: number }>(),
errors: 0
};
// Sample corpus files
const sampleFiles = files.slice(0, 50);
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
// Detect format
const format = await einvoice.detectFormat(content);
if (!format || format === 'unknown') continue;
// Parse invoice
const invoice = await einvoice.parseInvoice(content, format);
results.totalFiles++;
// Initialize format stats
if (!results.formatPerformance.has(format)) {
results.formatPerformance.set(format, { count: 0, totalTime: 0 });
}
// Measure validation at different levels
const levels = ['syntax', 'semantic', 'business'] as const;
for (const level of levels) {
const startTime = process.hrtime.bigint();
await einvoice.validateInvoice(invoice, { level });
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.validationTimes[level].push(duration);
if (level === 'business') {
const formatStats = results.formatPerformance.get(format)!;
formatStats.count++;
formatStats.totalTime += duration;
}
}
} catch (error) {
results.errors++;
}
}
// Calculate statistics
const stats = {};
for (const level of Object.keys(results.validationTimes)) {
const times = results.validationTimes[level];
if (times.length > 0) {
times.sort((a, b) => a - b);
stats[level] = {
min: times[0],
max: times[times.length - 1],
avg: times.reduce((a, b) => a + b, 0) / times.length,
median: times[Math.floor(times.length / 2)],
p95: times[Math.floor(times.length * 0.95)]
};
}
}
return {
...results,
stats,
formatPerformance: Array.from(results.formatPerformance.entries()).map(([format, data]) => ({
format,
avgTime: data.count > 0 ? (data.totalTime / data.count).toFixed(3) : 'N/A'
}))
};
}
);
// Test 4: Incremental validation performance
const incrementalValidation = await performanceTracker.measureAsync(
'incremental-validation',
async () => {
const einvoice = new EInvoice();
const results = [];
// Base invoice
const baseInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'INCR-001',
issueDate: '2024-02-01',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: [],
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Measure validation time as we add items
const itemCounts = [1, 5, 10, 20, 50, 100];
for (const count of itemCounts) {
// Add items incrementally
while (baseInvoice.data.items.length < count) {
const item = {
description: `Item ${baseInvoice.data.items.length + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 19,
lineTotal: 100
};
baseInvoice.data.items.push(item);
baseInvoice.data.totals.netAmount += 100;
baseInvoice.data.totals.vatAmount += 19;
baseInvoice.data.totals.grossAmount += 119;
}
// Measure validation time
const times = [];
for (let i = 0; i < 30; i++) {
const startTime = process.hrtime.bigint();
await einvoice.validateInvoice(baseInvoice);
const endTime = process.hrtime.bigint();
times.push(Number(endTime - startTime) / 1_000_000);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.push({
itemCount: count,
avgValidationTime: avgTime.toFixed(3),
timePerItem: (avgTime / count).toFixed(4)
});
}
return results;
}
);
// Test 5: Parallel validation performance
const parallelValidation = await performanceTracker.measureAsync(
'parallel-validation-performance',
async () => {
const einvoice = new EInvoice();
const results = [];
// Create test invoice
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'PARALLEL-001',
issueDate: '2024-02-01',
seller: { name: 'Parallel Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Parallel Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 20 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 }
}
};
// Test different concurrency levels
const concurrencyLevels = [1, 2, 5, 10, 20];
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
// Create parallel validation tasks
const tasks = Array(concurrency).fill(null).map(() =>
einvoice.validateInvoice(testInvoice)
);
const results = await Promise.all(tasks);
const endTime = Date.now();
const duration = endTime - startTime;
const throughput = (concurrency / (duration / 1000)).toFixed(2);
results.push({
concurrency,
duration,
throughput: `${throughput} validations/sec`,
allValid: results.every(r => r.isValid)
});
}
return results;
}
);
// Summary
t.comment('\n=== PERF-02: Validation Performance Test Summary ===');
t.comment('\nSyntax Validation Performance:');
syntaxValidation.result.forEach(result => {
t.comment(` ${result.name} (${result.itemCount} items):`);
t.comment(` - Min: ${result.min.toFixed(3)}ms, Max: ${result.max.toFixed(3)}ms`);
t.comment(` - Avg: ${result.avg.toFixed(3)}ms, Median: ${result.median.toFixed(3)}ms`);
t.comment(` - P95: ${result.p95.toFixed(3)}ms`);
});
t.comment('\nBusiness Rule Validation:');
businessRuleValidation.result.ruleCategories.forEach(category => {
t.comment(` ${category.name}: ${category.avgTime}ms avg (${category.rulesPerMs} rules/ms)`);
});
t.comment(`\nCorpus Validation (${corpusValidation.result.totalFiles} files):`);
Object.entries(corpusValidation.result.stats).forEach(([level, stats]: [string, any]) => {
t.comment(` ${level} validation:`);
t.comment(` - Min: ${stats.min.toFixed(3)}ms, Max: ${stats.max.toFixed(3)}ms`);
t.comment(` - Avg: ${stats.avg.toFixed(3)}ms, Median: ${stats.median.toFixed(3)}ms`);
});
t.comment(' By format:');
corpusValidation.result.formatPerformance.forEach(perf => {
t.comment(` - ${perf.format}: ${perf.avgTime}ms avg`);
});
t.comment('\nIncremental Validation Scaling:');
incrementalValidation.result.forEach(result => {
t.comment(` ${result.itemCount} items: ${result.avgValidationTime}ms (${result.timePerItem}ms/item)`);
});
t.comment('\nParallel Validation:');
parallelValidation.result.forEach(result => {
t.comment(` ${result.concurrency} concurrent: ${result.duration}ms, ${result.throughput}`);
});
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const syntaxAvg = syntaxValidation.result[1].avg; // Standard invoice
const businessAvg = businessRuleValidation.result.ruleCategories.find(r => r.name === 'All Rules')?.avgTime || 0;
t.comment(`Syntax validation: ${syntaxAvg.toFixed(3)}ms ${syntaxAvg < 50 ? '✅' : '⚠️'} (target: <50ms)`);
t.comment(`Business validation: ${businessAvg}ms ${parseFloat(businessAvg) < 200 ? '✅' : '⚠️'} (target: <200ms)`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,427 @@
/**
* @file test.perf-03.pdf-extraction.ts
* @description Performance tests for PDF extraction operations
*/
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-03: PDF Extraction Speed');
tap.test('PERF-03: PDF Extraction Speed - should meet performance targets for PDF extraction', async (t) => {
// Test 1: ZUGFeRD v1 extraction performance
const zugferdV1Performance = await performanceTracker.measureAsync(
'zugferd-v1-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/ZUGFeRDv1/**/*.pdf');
const einvoice = new EInvoice();
const results = {
fileCount: 0,
extractionTimes: [],
fileSizes: [],
successCount: 0,
failureCount: 0,
bytesPerMs: []
};
// Process ZUGFeRD v1 PDFs
const sampleFiles = files.slice(0, 20);
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
const fileSize = pdfBuffer.length;
results.fileSizes.push(fileSize);
results.fileCount++;
// Measure extraction time
const startTime = process.hrtime.bigint();
const extractedXml = await einvoice.extractFromPDF(pdfBuffer);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.extractionTimes.push(duration);
if (extractedXml) {
results.successCount++;
results.bytesPerMs.push(fileSize / duration);
} else {
results.failureCount++;
}
} catch (error) {
results.failureCount++;
}
}
// Calculate statistics
if (results.extractionTimes.length > 0) {
results.extractionTimes.sort((a, b) => a - b);
const stats = {
min: results.extractionTimes[0],
max: results.extractionTimes[results.extractionTimes.length - 1],
avg: results.extractionTimes.reduce((a, b) => a + b, 0) / results.extractionTimes.length,
median: results.extractionTimes[Math.floor(results.extractionTimes.length / 2)],
avgFileSize: results.fileSizes.reduce((a, b) => a + b, 0) / results.fileSizes.length / 1024, // KB
avgBytesPerMs: results.bytesPerMs.length > 0 ?
results.bytesPerMs.reduce((a, b) => a + b, 0) / results.bytesPerMs.length / 1024 : 0 // KB/ms
};
return { ...results, stats };
}
return results;
}
);
// Test 2: ZUGFeRD v2/Factur-X extraction performance
const facturXPerformance = await performanceTracker.measureAsync(
'facturx-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/ZUGFeRDv2/**/*.pdf');
const einvoice = new EInvoice();
const results = {
profiles: new Map<string, { count: number; totalTime: number }>(),
extractionTimes: [],
xmlSizes: [],
largestFile: { path: '', size: 0, time: 0 },
smallestFile: { path: '', size: Infinity, time: 0 }
};
// Process Factur-X PDFs
const sampleFiles = files.slice(0, 30);
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
const fileSize = pdfBuffer.length;
// Measure extraction
const startTime = process.hrtime.bigint();
const extractedXml = await einvoice.extractFromPDF(pdfBuffer);
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
results.extractionTimes.push(duration);
if (extractedXml) {
const xmlSize = Buffer.byteLength(extractedXml, 'utf-8');
results.xmlSizes.push(xmlSize);
// Detect profile from filename or content
const profile = file.includes('BASIC') ? 'BASIC' :
file.includes('COMFORT') ? 'COMFORT' :
file.includes('EXTENDED') ? 'EXTENDED' : 'UNKNOWN';
if (!results.profiles.has(profile)) {
results.profiles.set(profile, { count: 0, totalTime: 0 });
}
const profileStats = results.profiles.get(profile)!;
profileStats.count++;
profileStats.totalTime += duration;
// Track largest/smallest
if (fileSize > results.largestFile.size) {
results.largestFile = { path: file, size: fileSize, time: duration };
}
if (fileSize < results.smallestFile.size) {
results.smallestFile = { path: file, size: fileSize, time: duration };
}
}
} catch (error) {
// Skip failed extractions
}
}
// Calculate profile statistics
const profileStats = Array.from(results.profiles.entries()).map(([profile, data]) => ({
profile,
count: data.count,
avgTime: data.count > 0 ? (data.totalTime / data.count).toFixed(3) : 'N/A'
}));
return {
totalFiles: sampleFiles.length,
successfulExtractions: results.extractionTimes.length,
avgExtractionTime: results.extractionTimes.length > 0 ?
(results.extractionTimes.reduce((a, b) => a + b, 0) / results.extractionTimes.length).toFixed(3) : 'N/A',
avgXmlSize: results.xmlSizes.length > 0 ?
(results.xmlSizes.reduce((a, b) => a + b, 0) / results.xmlSizes.length / 1024).toFixed(2) : 'N/A',
profileStats,
largestFile: {
...results.largestFile,
sizeKB: (results.largestFile.size / 1024).toFixed(2),
timeMs: results.largestFile.time.toFixed(3)
},
smallestFile: {
...results.smallestFile,
sizeKB: (results.smallestFile.size / 1024).toFixed(2),
timeMs: results.smallestFile.time.toFixed(3)
}
};
}
);
// Test 3: Large PDF extraction performance
const largePDFPerformance = await performanceTracker.measureAsync(
'large-pdf-extraction',
async () => {
const einvoice = new EInvoice();
const results = [];
// Create synthetic large PDFs with embedded XML
const pdfSizes = [
{ name: '1MB', size: 1024 * 1024, xmlSize: 50 * 1024 },
{ name: '5MB', size: 5 * 1024 * 1024, xmlSize: 100 * 1024 },
{ name: '10MB', size: 10 * 1024 * 1024, xmlSize: 200 * 1024 },
{ name: '20MB', size: 20 * 1024 * 1024, xmlSize: 500 * 1024 }
];
for (const pdfSpec of pdfSizes) {
// Simulate PDF content (in real scenario, would use actual PDF library)
const mockPdfBuffer = Buffer.alloc(pdfSpec.size);
// Fill with some pattern to simulate real PDF
for (let i = 0; i < mockPdfBuffer.length; i += 1024) {
mockPdfBuffer.write('%PDF-1.4\n', i);
}
// Embed mock XML at a known location
const mockXml = `<?xml version="1.0"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID>LARGE-PDF-TEST</ram:ID>
${' '.repeat(pdfSpec.xmlSize - 200)}
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
// Measure extraction time
const times = [];
const iterations = 5;
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
try {
// Simulate extraction (would use real PDF library)
await new Promise(resolve => setTimeout(resolve, pdfSpec.size / (50 * 1024 * 1024))); // Simulate 50MB/s extraction
const endTime = process.hrtime.bigint();
const duration = Number(endTime - startTime) / 1_000_000;
times.push(duration);
} catch (error) {
// Extraction failed
}
}
if (times.length > 0) {
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.push({
size: pdfSpec.name,
sizeBytes: pdfSpec.size,
avgExtractionTime: avgTime.toFixed(3),
throughputMBps: (pdfSpec.size / avgTime / 1024).toFixed(2)
});
}
}
return results;
}
);
// Test 4: Concurrent PDF extraction
const concurrentExtraction = await performanceTracker.measureAsync(
'concurrent-pdf-extraction',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.pdf');
const einvoice = new EInvoice();
const results = [];
// Select sample PDFs
const samplePDFs = files.slice(0, 10);
if (samplePDFs.length === 0) {
return { error: 'No PDF files found for testing' };
}
// Test different concurrency levels
const concurrencyLevels = [1, 2, 5, 10];
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
let successCount = 0;
// Create extraction tasks
const tasks = [];
for (let i = 0; i < concurrency; i++) {
const pdfFile = samplePDFs[i % samplePDFs.length];
tasks.push(
plugins.fs.readFile(pdfFile)
.then(buffer => einvoice.extractFromPDF(buffer))
.then(xml => xml ? successCount++ : null)
.catch(() => null)
);
}
await Promise.all(tasks);
const duration = Date.now() - startTime;
results.push({
concurrency,
duration,
successCount,
throughput: (successCount / (duration / 1000)).toFixed(2),
avgTimePerExtraction: (duration / concurrency).toFixed(3)
});
}
return results;
}
);
// Test 5: Memory efficiency during extraction
const memoryEfficiency = await performanceTracker.measureAsync(
'extraction-memory-efficiency',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.pdf');
const einvoice = new EInvoice();
const results = {
memorySnapshots: [],
peakMemoryUsage: 0,
avgMemoryPerExtraction: 0
};
// Force garbage collection if available
if (global.gc) global.gc();
const baselineMemory = process.memoryUsage();
// Process PDFs and monitor memory
const sampleFiles = files.slice(0, 20);
let extractionCount = 0;
for (const file of sampleFiles) {
try {
const pdfBuffer = await plugins.fs.readFile(file);
// Memory before extraction
const beforeMemory = process.memoryUsage();
// Extract XML
const xml = await einvoice.extractFromPDF(pdfBuffer);
// Memory after extraction
const afterMemory = process.memoryUsage();
if (xml) {
extractionCount++;
const memoryIncrease = {
heapUsed: (afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024,
external: (afterMemory.external - beforeMemory.external) / 1024 / 1024,
fileSize: pdfBuffer.length / 1024 / 1024
};
results.memorySnapshots.push(memoryIncrease);
if (afterMemory.heapUsed > results.peakMemoryUsage) {
results.peakMemoryUsage = afterMemory.heapUsed;
}
}
} catch (error) {
// Skip failed extractions
}
}
// Calculate statistics
if (results.memorySnapshots.length > 0) {
const totalMemoryIncrease = results.memorySnapshots
.reduce((sum, snap) => sum + snap.heapUsed, 0);
results.avgMemoryPerExtraction = totalMemoryIncrease / results.memorySnapshots.length;
}
// Force garbage collection and measure final state
if (global.gc) global.gc();
const finalMemory = process.memoryUsage();
return {
extractionsProcessed: extractionCount,
peakMemoryMB: ((results.peakMemoryUsage - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2),
avgMemoryPerExtractionMB: results.avgMemoryPerExtraction.toFixed(2),
memoryLeakDetected: (finalMemory.heapUsed - baselineMemory.heapUsed) > 50 * 1024 * 1024,
finalMemoryIncreaseMB: ((finalMemory.heapUsed - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2)
};
}
);
// Summary
t.comment('\n=== PERF-03: PDF Extraction Speed Test Summary ===');
if (zugferdV1Performance.result.stats) {
t.comment('\nZUGFeRD v1 Extraction Performance:');
t.comment(` Files processed: ${zugferdV1Performance.result.fileCount}`);
t.comment(` Success rate: ${(zugferdV1Performance.result.successCount / zugferdV1Performance.result.fileCount * 100).toFixed(1)}%`);
t.comment(` Extraction times:`);
t.comment(` - Min: ${zugferdV1Performance.result.stats.min.toFixed(3)}ms`);
t.comment(` - Max: ${zugferdV1Performance.result.stats.max.toFixed(3)}ms`);
t.comment(` - Avg: ${zugferdV1Performance.result.stats.avg.toFixed(3)}ms`);
t.comment(` - Median: ${zugferdV1Performance.result.stats.median.toFixed(3)}ms`);
t.comment(` Average file size: ${zugferdV1Performance.result.stats.avgFileSize.toFixed(2)}KB`);
t.comment(` Throughput: ${zugferdV1Performance.result.stats.avgBytesPerMs.toFixed(2)}KB/ms`);
}
t.comment('\nFactur-X/ZUGFeRD v2 Extraction Performance:');
t.comment(` Files processed: ${facturXPerformance.result.totalFiles}`);
t.comment(` Successful extractions: ${facturXPerformance.result.successfulExtractions}`);
t.comment(` Average extraction time: ${facturXPerformance.result.avgExtractionTime}ms`);
t.comment(` Average XML size: ${facturXPerformance.result.avgXmlSize}KB`);
t.comment(' By profile:');
facturXPerformance.result.profileStats.forEach(stat => {
t.comment(` - ${stat.profile}: ${stat.count} files, avg ${stat.avgTime}ms`);
});
t.comment(` Largest file: ${facturXPerformance.result.largestFile.sizeKB}KB in ${facturXPerformance.result.largestFile.timeMs}ms`);
t.comment(` Smallest file: ${facturXPerformance.result.smallestFile.sizeKB}KB in ${facturXPerformance.result.smallestFile.timeMs}ms`);
t.comment('\nLarge PDF Extraction Performance:');
largePDFPerformance.result.forEach(result => {
t.comment(` ${result.size}: ${result.avgExtractionTime}ms (${result.throughputMBps}MB/s)`);
});
t.comment('\nConcurrent Extraction Performance:');
concurrentExtraction.result.forEach(result => {
if (!result.error) {
t.comment(` ${result.concurrency} concurrent: ${result.duration}ms total, ${result.throughput} extractions/sec`);
}
});
t.comment('\nMemory Efficiency:');
t.comment(` Extractions processed: ${memoryEfficiency.result.extractionsProcessed}`);
t.comment(` Peak memory usage: ${memoryEfficiency.result.peakMemoryMB}MB`);
t.comment(` Avg memory per extraction: ${memoryEfficiency.result.avgMemoryPerExtractionMB}MB`);
t.comment(` Memory leak detected: ${memoryEfficiency.result.memoryLeakDetected ? 'YES ⚠️' : 'NO ✅'}`);
t.comment(` Final memory increase: ${memoryEfficiency.result.finalMemoryIncreaseMB}MB`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgExtractionTime = parseFloat(facturXPerformance.result.avgExtractionTime) || 0;
const targetTime = 500; // Target: <500ms for PDF extraction
if (avgExtractionTime > 0 && avgExtractionTime < targetTime) {
t.comment(`✅ PDF extraction meets target: ${avgExtractionTime}ms < ${targetTime}ms`);
} else if (avgExtractionTime > 0) {
t.comment(`⚠️ PDF extraction exceeds target: ${avgExtractionTime}ms > ${targetTime}ms`);
}
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,583 @@
/**
* @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();

View File

@ -0,0 +1,569 @@
/**
* @file test.perf-05.memory-usage.ts
* @description Performance tests for memory usage profiling
*/
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-05: Memory Usage Profiling');
tap.test('PERF-05: Memory Usage Profiling - should maintain efficient memory usage patterns', async (t) => {
// Test 1: Baseline memory usage for different operations
const baselineMemoryUsage = await performanceTracker.measureAsync(
'baseline-memory-usage',
async () => {
const einvoice = new EInvoice();
const results = {
operations: [],
initialMemory: null,
finalMemory: null
};
// Force garbage collection if available
if (global.gc) global.gc();
results.initialMemory = process.memoryUsage();
// Test different operations
const operations = [
{
name: 'Format Detection',
fn: async () => {
const xml = '<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>TEST</ID></Invoice>';
for (let i = 0; i < 100; i++) {
await einvoice.detectFormat(xml);
}
}
},
{
name: 'XML Parsing',
fn: async () => {
const xml = `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>MEM-TEST</ID>
<IssueDate>2024-01-01</IssueDate>
${Array(10).fill('<InvoiceLine><ID>Line</ID></InvoiceLine>').join('\n')}
</Invoice>`;
for (let i = 0; i < 50; i++) {
await einvoice.parseInvoice(xml, 'ubl');
}
}
},
{
name: 'Validation',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'MEM-VAL-001',
issueDate: '2024-02-10',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 20 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 }
}
};
for (let i = 0; i < 30; i++) {
await einvoice.validateInvoice(invoice);
}
}
},
{
name: 'Format Conversion',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'MEM-CONV-001',
issueDate: '2024-02-10',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: '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 }
}
};
for (let i = 0; i < 20; i++) {
await einvoice.convertFormat(invoice, 'cii');
}
}
}
];
// Execute operations and measure memory
for (const operation of operations) {
if (global.gc) global.gc();
const beforeMemory = process.memoryUsage();
await operation.fn();
if (global.gc) global.gc();
const afterMemory = process.memoryUsage();
results.operations.push({
name: operation.name,
heapUsedBefore: (beforeMemory.heapUsed / 1024 / 1024).toFixed(2),
heapUsedAfter: (afterMemory.heapUsed / 1024 / 1024).toFixed(2),
heapIncrease: ((afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024).toFixed(2),
externalIncrease: ((afterMemory.external - beforeMemory.external) / 1024 / 1024).toFixed(2),
rssIncrease: ((afterMemory.rss - beforeMemory.rss) / 1024 / 1024).toFixed(2)
});
}
if (global.gc) global.gc();
results.finalMemory = process.memoryUsage();
return results;
}
);
// Test 2: Memory scaling with invoice complexity
const memoryScaling = await performanceTracker.measureAsync(
'memory-scaling',
async () => {
const einvoice = new EInvoice();
const results = {
scalingData: [],
memoryFormula: null
};
// Test with increasing invoice sizes
const itemCounts = [1, 10, 50, 100, 200, 500, 1000];
for (const itemCount of itemCounts) {
if (global.gc) global.gc();
const beforeMemory = process.memoryUsage();
// Create invoice with specified number of items
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `SCALE-${itemCount}`,
issueDate: '2024-02-10',
seller: {
name: 'Memory Test Seller Corporation Ltd.',
address: '123 Memory Lane, Suite 456',
city: 'Test City',
postalCode: '12345',
country: 'US',
taxId: 'US123456789'
},
buyer: {
name: 'Memory Test Buyer Enterprises Inc.',
address: '789 RAM Avenue, Floor 10',
city: 'Cache Town',
postalCode: '67890',
country: 'US',
taxId: 'US987654321'
},
items: Array.from({ length: itemCount }, (_, i) => ({
description: `Product Item Number ${i + 1} with detailed description and specifications`,
quantity: Math.floor(Math.random() * 100) + 1,
unitPrice: Math.random() * 1000,
vatRate: [5, 10, 15, 20][Math.floor(Math.random() * 4)],
lineTotal: 0,
itemId: `ITEM-${String(i + 1).padStart(6, '0')}`,
additionalInfo: {
weight: `${Math.random() * 10}kg`,
dimensions: `${Math.random() * 100}x${Math.random() * 100}x${Math.random() * 100}`,
notes: `Additional notes for item ${i + 1}`
}
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Calculate totals
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 invoice through multiple operations
const parsed = await einvoice.parseInvoice(JSON.stringify(invoice), 'json');
await einvoice.validateInvoice(parsed);
await einvoice.convertFormat(parsed, 'cii');
if (global.gc) global.gc();
const afterMemory = process.memoryUsage();
const memoryUsed = (afterMemory.heapUsed - beforeMemory.heapUsed) / 1024 / 1024;
const invoiceSize = JSON.stringify(invoice).length / 1024; // KB
results.scalingData.push({
itemCount,
invoiceSizeKB: invoiceSize.toFixed(2),
memoryUsedMB: memoryUsed.toFixed(2),
memoryPerItemKB: ((memoryUsed * 1024) / itemCount).toFixed(2),
memoryEfficiency: (invoiceSize / (memoryUsed * 1024)).toFixed(3)
});
}
// Calculate memory scaling formula (linear regression)
if (results.scalingData.length > 2) {
const n = results.scalingData.length;
const sumX = results.scalingData.reduce((sum, d) => sum + d.itemCount, 0);
const sumY = results.scalingData.reduce((sum, d) => sum + parseFloat(d.memoryUsedMB), 0);
const sumXY = results.scalingData.reduce((sum, d) => sum + d.itemCount * parseFloat(d.memoryUsedMB), 0);
const sumX2 = results.scalingData.reduce((sum, d) => sum + d.itemCount * d.itemCount, 0);
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
results.memoryFormula = {
slope: slope.toFixed(4),
intercept: intercept.toFixed(4),
formula: `Memory(MB) = ${slope.toFixed(4)} * items + ${intercept.toFixed(4)}`
};
}
return results;
}
);
// Test 3: Memory leak detection
const memoryLeakDetection = await performanceTracker.measureAsync(
'memory-leak-detection',
async () => {
const einvoice = new EInvoice();
const results = {
iterations: 100,
memorySnapshots: [],
leakDetected: false,
leakRate: 0
};
// Test invoice for repeated operations
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'LEAK-TEST-001',
issueDate: '2024-02-10',
seller: { name: 'Leak Test Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Leak Test Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 10 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 }
}
};
// Take memory snapshots during repeated operations
for (let i = 0; i < results.iterations; i++) {
if (i % 10 === 0) {
if (global.gc) global.gc();
const memory = process.memoryUsage();
results.memorySnapshots.push({
iteration: i,
heapUsedMB: memory.heapUsed / 1024 / 1024
});
}
// Perform operations that might leak memory
const xml = await einvoice.generateXML(testInvoice);
const parsed = await einvoice.parseInvoice(xml, 'ubl');
await einvoice.validateInvoice(parsed);
await einvoice.convertFormat(parsed, 'cii');
}
// Final snapshot
if (global.gc) global.gc();
const finalMemory = process.memoryUsage();
results.memorySnapshots.push({
iteration: results.iterations,
heapUsedMB: finalMemory.heapUsed / 1024 / 1024
});
// Analyze for memory leaks
if (results.memorySnapshots.length > 2) {
const firstSnapshot = results.memorySnapshots[0];
const lastSnapshot = results.memorySnapshots[results.memorySnapshots.length - 1];
const memoryIncrease = lastSnapshot.heapUsedMB - firstSnapshot.heapUsedMB;
results.leakRate = memoryIncrease / results.iterations; // MB per iteration
results.leakDetected = results.leakRate > 0.1; // Threshold: 0.1MB per iteration
// Calculate trend
const midpoint = Math.floor(results.memorySnapshots.length / 2);
const firstHalf = results.memorySnapshots.slice(0, midpoint);
const secondHalf = results.memorySnapshots.slice(midpoint);
const firstHalfAvg = firstHalf.reduce((sum, s) => sum + s.heapUsedMB, 0) / firstHalf.length;
const secondHalfAvg = secondHalf.reduce((sum, s) => sum + s.heapUsedMB, 0) / secondHalf.length;
results.trend = {
firstHalfAvgMB: firstHalfAvg.toFixed(2),
secondHalfAvgMB: secondHalfAvg.toFixed(2),
increasing: secondHalfAvg > firstHalfAvg * 1.1
};
}
return results;
}
);
// Test 4: Corpus processing memory profile
const corpusMemoryProfile = await performanceTracker.measureAsync(
'corpus-memory-profile',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
filesProcessed: 0,
memoryByFormat: new Map<string, { count: number; totalMemory: number }>(),
memoryBySize: {
small: { count: 0, avgMemory: 0, total: 0 },
medium: { count: 0, avgMemory: 0, total: 0 },
large: { count: 0, avgMemory: 0, total: 0 }
},
peakMemory: 0,
totalAllocated: 0
};
// Initial memory state
if (global.gc) global.gc();
const startMemory = process.memoryUsage();
// Process sample files
const sampleFiles = files.slice(0, 30);
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const fileSize = Buffer.byteLength(content, 'utf-8');
const sizeCategory = fileSize < 10240 ? 'small' :
fileSize < 102400 ? 'medium' : 'large';
const beforeProcess = process.memoryUsage();
// Process file
const format = await einvoice.detectFormat(content);
if (!format || format === 'unknown') continue;
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
const afterProcess = process.memoryUsage();
const memoryUsed = (afterProcess.heapUsed - beforeProcess.heapUsed) / 1024 / 1024;
// Update statistics
results.filesProcessed++;
results.totalAllocated += memoryUsed;
// By format
if (!results.memoryByFormat.has(format)) {
results.memoryByFormat.set(format, { count: 0, totalMemory: 0 });
}
const formatStats = results.memoryByFormat.get(format)!;
formatStats.count++;
formatStats.totalMemory += memoryUsed;
// By size
results.memoryBySize[sizeCategory].count++;
results.memoryBySize[sizeCategory].total += memoryUsed;
// Track peak
if (afterProcess.heapUsed > results.peakMemory) {
results.peakMemory = afterProcess.heapUsed;
}
} catch (error) {
// Skip failed files
}
}
// Calculate averages
for (const category of Object.keys(results.memoryBySize)) {
const stats = results.memoryBySize[category];
if (stats.count > 0) {
stats.avgMemory = stats.total / stats.count;
}
}
// Format statistics
const formatStats = Array.from(results.memoryByFormat.entries()).map(([format, stats]) => ({
format,
count: stats.count,
avgMemoryMB: (stats.totalMemory / stats.count).toFixed(2)
}));
return {
filesProcessed: results.filesProcessed,
totalAllocatedMB: results.totalAllocated.toFixed(2),
peakMemoryMB: ((results.peakMemory - startMemory.heapUsed) / 1024 / 1024).toFixed(2),
avgMemoryPerFileMB: (results.totalAllocated / results.filesProcessed).toFixed(2),
formatStats,
sizeStats: {
small: { ...results.memoryBySize.small, avgMemory: results.memoryBySize.small.avgMemory.toFixed(2) },
medium: { ...results.memoryBySize.medium, avgMemory: results.memoryBySize.medium.avgMemory.toFixed(2) },
large: { ...results.memoryBySize.large, avgMemory: results.memoryBySize.large.avgMemory.toFixed(2) }
}
};
}
);
// Test 5: Garbage collection impact
const gcImpact = await performanceTracker.measureAsync(
'gc-impact',
async () => {
const einvoice = new EInvoice();
const results = {
withManualGC: { times: [], avgTime: 0 },
withoutGC: { times: [], avgTime: 0 },
gcOverhead: 0
};
// Test invoice
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'GC-TEST-001',
issueDate: '2024-02-10',
seller: { name: 'GC Test Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'GC Test Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 50 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 5000, vatAmount: 500, grossAmount: 5500 }
}
};
// Test with manual GC
if (global.gc) {
for (let i = 0; i < 20; i++) {
global.gc();
const start = process.hrtime.bigint();
await einvoice.parseInvoice(JSON.stringify(testInvoice), 'json');
await einvoice.validateInvoice(testInvoice);
await einvoice.convertFormat(testInvoice, 'cii');
const end = process.hrtime.bigint();
results.withManualGC.times.push(Number(end - start) / 1_000_000);
}
}
// Test without manual GC
for (let i = 0; i < 20; i++) {
const start = process.hrtime.bigint();
await einvoice.parseInvoice(JSON.stringify(testInvoice), 'json');
await einvoice.validateInvoice(testInvoice);
await einvoice.convertFormat(testInvoice, 'cii');
const end = process.hrtime.bigint();
results.withoutGC.times.push(Number(end - start) / 1_000_000);
}
// Calculate averages
if (results.withManualGC.times.length > 0) {
results.withManualGC.avgTime = results.withManualGC.times.reduce((a, b) => a + b, 0) / results.withManualGC.times.length;
}
results.withoutGC.avgTime = results.withoutGC.times.reduce((a, b) => a + b, 0) / results.withoutGC.times.length;
if (results.withManualGC.avgTime > 0) {
results.gcOverhead = ((results.withManualGC.avgTime - results.withoutGC.avgTime) / results.withoutGC.avgTime * 100);
}
return results;
}
);
// Summary
t.comment('\n=== PERF-05: Memory Usage Profiling Test Summary ===');
t.comment('\nBaseline Memory Usage:');
baselineMemoryUsage.result.operations.forEach(op => {
t.comment(` ${op.name}:`);
t.comment(` - Heap before: ${op.heapUsedBefore}MB, after: ${op.heapUsedAfter}MB`);
t.comment(` - Heap increase: ${op.heapIncrease}MB`);
t.comment(` - RSS increase: ${op.rssIncrease}MB`);
});
t.comment('\nMemory Scaling with Invoice Complexity:');
t.comment(' Item Count | Invoice Size | Memory Used | Memory/Item | Efficiency');
t.comment(' -----------|--------------|-------------|-------------|------------');
memoryScaling.result.scalingData.forEach(data => {
t.comment(` ${String(data.itemCount).padEnd(10)} | ${data.invoiceSizeKB.padEnd(12)}KB | ${data.memoryUsedMB.padEnd(11)}MB | ${data.memoryPerItemKB.padEnd(11)}KB | ${data.memoryEfficiency}`);
});
if (memoryScaling.result.memoryFormula) {
t.comment(` Memory scaling formula: ${memoryScaling.result.memoryFormula.formula}`);
}
t.comment('\nMemory Leak Detection:');
t.comment(` Iterations: ${memoryLeakDetection.result.iterations}`);
t.comment(` Leak detected: ${memoryLeakDetection.result.leakDetected ? 'YES ⚠️' : 'NO ✅'}`);
t.comment(` Leak rate: ${(memoryLeakDetection.result.leakRate * 1000).toFixed(3)}KB per iteration`);
if (memoryLeakDetection.result.trend) {
t.comment(` Memory trend: ${memoryLeakDetection.result.trend.increasing ? 'INCREASING ⚠️' : 'STABLE ✅'}`);
t.comment(` - First half avg: ${memoryLeakDetection.result.trend.firstHalfAvgMB}MB`);
t.comment(` - Second half avg: ${memoryLeakDetection.result.trend.secondHalfAvgMB}MB`);
}
t.comment('\nCorpus Memory Profile:');
t.comment(` Files processed: ${corpusMemoryProfile.result.filesProcessed}`);
t.comment(` Total allocated: ${corpusMemoryProfile.result.totalAllocatedMB}MB`);
t.comment(` Peak memory: ${corpusMemoryProfile.result.peakMemoryMB}MB`);
t.comment(` Avg per file: ${corpusMemoryProfile.result.avgMemoryPerFileMB}MB`);
t.comment(' By format:');
corpusMemoryProfile.result.formatStats.forEach(stat => {
t.comment(` - ${stat.format}: ${stat.count} files, avg ${stat.avgMemoryMB}MB`);
});
t.comment(' By size:');
['small', 'medium', 'large'].forEach(size => {
const stats = corpusMemoryProfile.result.sizeStats[size];
if (stats.count > 0) {
t.comment(` - ${size}: ${stats.count} files, avg ${stats.avgMemory}MB`);
}
});
t.comment('\nGarbage Collection Impact:');
if (gcImpact.result.withManualGC.avgTime > 0) {
t.comment(` With manual GC: ${gcImpact.result.withManualGC.avgTime.toFixed(3)}ms avg`);
}
t.comment(` Without GC: ${gcImpact.result.withoutGC.avgTime.toFixed(3)}ms avg`);
if (gcImpact.result.gcOverhead !== 0) {
t.comment(` GC overhead: ${gcImpact.result.gcOverhead.toFixed(1)}%`);
}
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgMemoryPerInvoice = parseFloat(corpusMemoryProfile.result.avgMemoryPerFileMB);
const targetMemory = 100; // Target: <100MB per invoice
const leakDetected = memoryLeakDetection.result.leakDetected;
t.comment(`Memory usage: ${avgMemoryPerInvoice}MB ${avgMemoryPerInvoice < targetMemory ? '✅' : '⚠️'} (target: <${targetMemory}MB per invoice)`);
t.comment(`Memory leaks: ${leakDetected ? 'DETECTED ⚠️' : 'NONE ✅'}`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,669 @@
/**
* @file test.perf-06.cpu-utilization.ts
* @description Performance tests for CPU utilization monitoring
*/
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';
import * as os from 'os';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-06: CPU Utilization');
tap.test('PERF-06: CPU Utilization - should maintain efficient CPU usage patterns', async (t) => {
// Helper function to get CPU usage
const getCPUUsage = () => {
const cpus = os.cpus();
let user = 0;
let nice = 0;
let sys = 0;
let idle = 0;
let irq = 0;
for (const cpu of cpus) {
user += cpu.times.user;
nice += cpu.times.nice;
sys += cpu.times.sys;
idle += cpu.times.idle;
irq += cpu.times.irq;
}
const total = user + nice + sys + idle + irq;
return {
user: user / total,
system: sys / total,
idle: idle / total,
total: total
};
};
// Test 1: CPU usage baseline for operations
const cpuBaseline = await performanceTracker.measureAsync(
'cpu-usage-baseline',
async () => {
const einvoice = new EInvoice();
const results = {
operations: [],
cpuCount: os.cpus().length,
cpuModel: os.cpus()[0]?.model || 'Unknown'
};
// Operations to test
const operations = [
{
name: 'Idle baseline',
fn: async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
}
},
{
name: 'Format detection (100x)',
fn: async () => {
const xml = '<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>CPU-TEST</ID></Invoice>';
for (let i = 0; i < 100; i++) {
await einvoice.detectFormat(xml);
}
}
},
{
name: 'XML parsing (50x)',
fn: async () => {
const xml = `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CPU-PARSE</ID>
<IssueDate>2024-01-01</IssueDate>
${Array(20).fill('<InvoiceLine><ID>Line</ID></InvoiceLine>').join('\n')}
</Invoice>`;
for (let i = 0; i < 50; i++) {
await einvoice.parseInvoice(xml, 'ubl');
}
}
},
{
name: 'Validation (30x)',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'CPU-VAL-001',
issueDate: '2024-02-15',
seller: { name: 'CPU Test Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'CPU Test Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 20 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 }
}
};
for (let i = 0; i < 30; i++) {
await einvoice.validateInvoice(invoice);
}
}
},
{
name: 'Conversion (20x)',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'CPU-CONV-001',
issueDate: '2024-02-15',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 10 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 }
}
};
for (let i = 0; i < 20; i++) {
await einvoice.convertFormat(invoice, 'cii');
}
}
}
];
// Execute operations and measure CPU
for (const operation of operations) {
const startCPU = getCPUUsage();
const startTime = Date.now();
const startUsage = process.cpuUsage();
await operation.fn();
const endUsage = process.cpuUsage(startUsage);
const endTime = Date.now();
const endCPU = getCPUUsage();
const duration = endTime - startTime;
const userCPU = endUsage.user / 1000; // Convert to milliseconds
const systemCPU = endUsage.system / 1000;
results.operations.push({
name: operation.name,
duration,
userCPU: userCPU.toFixed(2),
systemCPU: systemCPU.toFixed(2),
totalCPU: (userCPU + systemCPU).toFixed(2),
cpuPercentage: ((userCPU + systemCPU) / duration * 100).toFixed(2),
efficiency: (duration / (userCPU + systemCPU)).toFixed(2)
});
}
return results;
}
);
// Test 2: Multi-core utilization
const multiCoreUtilization = await performanceTracker.measureAsync(
'multi-core-utilization',
async () => {
const einvoice = new EInvoice();
const results = {
coreCount: os.cpus().length,
parallelTests: []
};
// Test invoice batch
const invoices = Array.from({ length: 50 }, (_, i) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `MULTI-CORE-${i + 1}`,
issueDate: '2024-02-15',
seller: { name: `Seller ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i}` },
buyer: { name: `Buyer ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i + 1000}` },
items: Array.from({ length: 10 }, (_, j) => ({
description: `Item ${j + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 }
}
}));
// Test different parallelism levels
const parallelismLevels = [1, 2, 4, 8, results.coreCount];
for (const parallelism of parallelismLevels) {
if (parallelism > results.coreCount) continue;
const startUsage = process.cpuUsage();
const startTime = Date.now();
// Process invoices in parallel
const batchSize = Math.ceil(invoices.length / parallelism);
const promises = [];
for (let i = 0; i < parallelism; i++) {
const batch = invoices.slice(i * batchSize, (i + 1) * batchSize);
promises.push(
Promise.all(batch.map(async (invoice) => {
await einvoice.validateInvoice(invoice);
await einvoice.convertFormat(invoice, 'cii');
}))
);
}
await Promise.all(promises);
const endTime = Date.now();
const endUsage = process.cpuUsage(startUsage);
const duration = endTime - startTime;
const totalCPU = (endUsage.user + endUsage.system) / 1000;
const theoreticalSpeedup = parallelism;
const actualSpeedup = results.parallelTests.length > 0 ?
results.parallelTests[0].duration / duration : 1;
results.parallelTests.push({
parallelism,
duration,
totalCPU: totalCPU.toFixed(2),
cpuEfficiency: ((totalCPU / duration) * 100).toFixed(2),
theoreticalSpeedup,
actualSpeedup: actualSpeedup.toFixed(2),
efficiency: ((actualSpeedup / theoreticalSpeedup) * 100).toFixed(2)
});
}
return results;
}
);
// Test 3: CPU-intensive operations profiling
const cpuIntensiveOperations = await performanceTracker.measureAsync(
'cpu-intensive-operations',
async () => {
const einvoice = new EInvoice();
const results = {
operations: []
};
// Test scenarios
const scenarios = [
{
name: 'Complex validation',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'COMPLEX-VAL-001',
issueDate: '2024-02-15',
dueDate: '2024-03-15',
currency: 'EUR',
seller: {
name: 'Complex Validation Test Seller GmbH',
address: 'Hauptstraße 123',
city: 'Berlin',
postalCode: '10115',
country: 'DE',
taxId: 'DE123456789',
registrationNumber: 'HRB12345',
email: 'billing@seller.de',
phone: '+49 30 12345678'
},
buyer: {
name: 'Complex Validation Test Buyer Ltd',
address: 'Business Street 456',
city: 'Munich',
postalCode: '80331',
country: 'DE',
taxId: 'DE987654321',
email: 'ap@buyer.de'
},
items: Array.from({ length: 100 }, (_, i) => ({
description: `Complex Product ${i + 1} with detailed specifications and compliance requirements`,
quantity: Math.floor(Math.random() * 100) + 1,
unitPrice: Math.random() * 1000,
vatRate: [0, 7, 19][Math.floor(Math.random() * 3)],
lineTotal: 0,
itemId: `ITEM-${String(i + 1).padStart(5, '0')}`,
additionalCharges: Math.random() * 50,
discounts: Math.random() * 20
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Calculate totals
invoice.data.items.forEach(item => {
item.lineTotal = item.quantity * item.unitPrice + (item.additionalCharges || 0) - (item.discounts || 0);
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;
// Perform all validation levels
await einvoice.validateInvoice(invoice, { level: 'syntax' });
await einvoice.validateInvoice(invoice, { level: 'semantic' });
await einvoice.validateInvoice(invoice, { level: 'business' });
}
},
{
name: 'Large XML generation',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'LARGE-XML-001',
issueDate: '2024-02-15',
seller: { name: 'XML Generator Corp', address: 'XML Street', country: 'US', taxId: 'US123456789' },
buyer: { name: 'XML Consumer Inc', address: 'XML Avenue', country: 'US', taxId: 'US987654321' },
items: Array.from({ length: 200 }, (_, i) => ({
description: `Product ${i + 1} with very long description `.repeat(10),
quantity: Math.random() * 100,
unitPrice: Math.random() * 1000,
vatRate: Math.random() * 25,
lineTotal: 0
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Calculate totals
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;
await einvoice.generateXML(invoice);
}
},
{
name: 'Chain conversions',
fn: async () => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'CHAIN-CONV-001',
issueDate: '2024-02-15',
seller: { name: 'Chain Seller', address: 'Chain Street', country: 'US', taxId: 'US123' },
buyer: { name: 'Chain Buyer', address: 'Chain Avenue', country: 'US', taxId: 'US456' },
items: Array.from({ length: 50 }, (_, i) => ({
description: `Chain Item ${i + 1}`,
quantity: i + 1,
unitPrice: 100 + i * 10,
vatRate: 10,
lineTotal: (i + 1) * (100 + i * 10)
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Calculate totals
invoice.data.items.forEach(item => {
invoice.data.totals.netAmount += item.lineTotal;
invoice.data.totals.vatAmount += item.lineTotal * 0.1;
});
invoice.data.totals.grossAmount = invoice.data.totals.netAmount + invoice.data.totals.vatAmount;
// Chain conversions
let current = invoice;
const formats = ['cii', 'zugferd', 'xrechnung', 'ubl'];
for (const format of formats) {
current = await einvoice.convertFormat(current, format);
}
}
}
];
// Profile each scenario
for (const scenario of scenarios) {
const iterations = 5;
const measurements = [];
for (let i = 0; i < iterations; i++) {
const startUsage = process.cpuUsage();
const startTime = process.hrtime.bigint();
await scenario.fn();
const endTime = process.hrtime.bigint();
const endUsage = process.cpuUsage(startUsage);
const duration = Number(endTime - startTime) / 1_000_000;
const cpuTime = (endUsage.user + endUsage.system) / 1000;
measurements.push({
duration,
cpuTime,
efficiency: cpuTime / duration
});
}
// Calculate averages
const avgDuration = measurements.reduce((sum, m) => sum + m.duration, 0) / iterations;
const avgCpuTime = measurements.reduce((sum, m) => sum + m.cpuTime, 0) / iterations;
const avgEfficiency = measurements.reduce((sum, m) => sum + m.efficiency, 0) / iterations;
results.operations.push({
name: scenario.name,
iterations,
avgDuration: avgDuration.toFixed(2),
avgCpuTime: avgCpuTime.toFixed(2),
avgEfficiency: (avgEfficiency * 100).toFixed(2),
cpuIntensity: avgCpuTime > avgDuration * 0.8 ? 'HIGH' :
avgCpuTime > avgDuration * 0.5 ? 'MEDIUM' : 'LOW'
});
}
return results;
}
);
// Test 4: Corpus processing CPU profile
const corpusCPUProfile = await performanceTracker.measureAsync(
'corpus-cpu-profile',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
filesProcessed: 0,
totalCPUTime: 0,
totalWallTime: 0,
cpuByOperation: {
detection: { time: 0, count: 0 },
parsing: { time: 0, count: 0 },
validation: { time: 0, count: 0 },
conversion: { time: 0, count: 0 }
}
};
// Sample files
const sampleFiles = files.slice(0, 25);
const overallStart = Date.now();
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
// Format detection
let startUsage = process.cpuUsage();
const format = await einvoice.detectFormat(content);
let endUsage = process.cpuUsage(startUsage);
results.cpuByOperation.detection.time += (endUsage.user + endUsage.system) / 1000;
results.cpuByOperation.detection.count++;
if (!format || format === 'unknown') continue;
// Parsing
startUsage = process.cpuUsage();
const invoice = await einvoice.parseInvoice(content, format);
endUsage = process.cpuUsage(startUsage);
results.cpuByOperation.parsing.time += (endUsage.user + endUsage.system) / 1000;
results.cpuByOperation.parsing.count++;
// Validation
startUsage = process.cpuUsage();
await einvoice.validateInvoice(invoice);
endUsage = process.cpuUsage(startUsage);
results.cpuByOperation.validation.time += (endUsage.user + endUsage.system) / 1000;
results.cpuByOperation.validation.count++;
// Conversion
const targetFormat = format === 'ubl' ? 'cii' : 'ubl';
startUsage = process.cpuUsage();
await einvoice.convertFormat(invoice, targetFormat);
endUsage = process.cpuUsage(startUsage);
results.cpuByOperation.conversion.time += (endUsage.user + endUsage.system) / 1000;
results.cpuByOperation.conversion.count++;
results.filesProcessed++;
} catch (error) {
// Skip failed files
}
}
results.totalWallTime = Date.now() - overallStart;
// Calculate totals and averages
for (const op of Object.keys(results.cpuByOperation)) {
const opData = results.cpuByOperation[op];
results.totalCPUTime += opData.time;
}
return {
filesProcessed: results.filesProcessed,
totalWallTime: results.totalWallTime,
totalCPUTime: results.totalCPUTime.toFixed(2),
cpuEfficiency: ((results.totalCPUTime / results.totalWallTime) * 100).toFixed(2),
operations: Object.entries(results.cpuByOperation).map(([op, data]) => ({
operation: op,
totalTime: data.time.toFixed(2),
avgTime: data.count > 0 ? (data.time / data.count).toFixed(3) : 'N/A',
percentage: ((data.time / results.totalCPUTime) * 100).toFixed(1)
}))
};
}
);
// Test 5: Sustained CPU load test
const sustainedCPULoad = await performanceTracker.measureAsync(
'sustained-cpu-load',
async () => {
const einvoice = new EInvoice();
const testDuration = 5000; // 5 seconds
const results = {
samples: [],
avgCPUUsage: 0,
peakCPUUsage: 0,
consistency: 0
};
// Test invoice
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'SUSTAINED-CPU-001',
issueDate: '2024-02-15',
seller: { name: 'CPU Load Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'CPU Load Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 20 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 }
}
};
const startTime = Date.now();
let sampleCount = 0;
// Run sustained load
while (Date.now() - startTime < testDuration) {
const sampleStart = process.cpuUsage();
const sampleStartTime = Date.now();
// Perform operations
await einvoice.validateInvoice(testInvoice);
await einvoice.convertFormat(testInvoice, 'cii');
const sampleEndTime = Date.now();
const sampleEnd = process.cpuUsage(sampleStart);
const sampleDuration = sampleEndTime - sampleStartTime;
const cpuTime = (sampleEnd.user + sampleEnd.system) / 1000;
const cpuUsage = (cpuTime / sampleDuration) * 100;
results.samples.push(cpuUsage);
if (cpuUsage > results.peakCPUUsage) {
results.peakCPUUsage = cpuUsage;
}
sampleCount++;
}
// Calculate statistics
if (results.samples.length > 0) {
results.avgCPUUsage = results.samples.reduce((a, b) => a + b, 0) / results.samples.length;
// Calculate standard deviation for consistency
const variance = results.samples.reduce((sum, val) =>
sum + Math.pow(val - results.avgCPUUsage, 2), 0) / results.samples.length;
const stdDev = Math.sqrt(variance);
results.consistency = 100 - (stdDev / results.avgCPUUsage * 100);
}
return {
duration: Date.now() - startTime,
samples: results.samples.length,
avgCPUUsage: results.avgCPUUsage.toFixed(2),
peakCPUUsage: results.peakCPUUsage.toFixed(2),
consistency: results.consistency.toFixed(2),
stable: results.consistency > 80
};
}
);
// Summary
t.comment('\n=== PERF-06: CPU Utilization Test Summary ===');
t.comment('\nCPU Baseline:');
t.comment(` System: ${cpuBaseline.result.cpuCount} cores, ${cpuBaseline.result.cpuModel}`);
t.comment(' Operation benchmarks:');
cpuBaseline.result.operations.forEach(op => {
t.comment(` ${op.name}:`);
t.comment(` - Duration: ${op.duration}ms`);
t.comment(` - CPU time: ${op.totalCPU}ms (user: ${op.userCPU}ms, system: ${op.systemCPU}ms)`);
t.comment(` - CPU usage: ${op.cpuPercentage}%`);
t.comment(` - Efficiency: ${op.efficiency}x`);
});
t.comment('\nMulti-Core Utilization:');
t.comment(' Parallelism | Duration | CPU Time | Efficiency | Speedup | Scaling');
t.comment(' ------------|----------|----------|------------|---------|--------');
multiCoreUtilization.result.parallelTests.forEach(test => {
t.comment(` ${String(test.parallelism).padEnd(11)} | ${String(test.duration + 'ms').padEnd(8)} | ${test.totalCPU.padEnd(8)}ms | ${test.cpuEfficiency.padEnd(10)}% | ${test.actualSpeedup.padEnd(7)}x | ${test.efficiency}%`);
});
t.comment('\nCPU-Intensive Operations:');
cpuIntensiveOperations.result.operations.forEach(op => {
t.comment(` ${op.name}:`);
t.comment(` - Avg duration: ${op.avgDuration}ms`);
t.comment(` - Avg CPU time: ${op.avgCpuTime}ms`);
t.comment(` - CPU efficiency: ${op.avgEfficiency}%`);
t.comment(` - Intensity: ${op.cpuIntensity}`);
});
t.comment('\nCorpus CPU Profile:');
t.comment(` Files processed: ${corpusCPUProfile.result.filesProcessed}`);
t.comment(` Total wall time: ${corpusCPUProfile.result.totalWallTime}ms`);
t.comment(` Total CPU time: ${corpusCPUProfile.result.totalCPUTime}ms`);
t.comment(` CPU efficiency: ${corpusCPUProfile.result.cpuEfficiency}%`);
t.comment(' By operation:');
corpusCPUProfile.result.operations.forEach(op => {
t.comment(` - ${op.operation}: ${op.totalTime}ms (${op.percentage}%), avg ${op.avgTime}ms`);
});
t.comment('\nSustained CPU Load (5 seconds):');
t.comment(` Samples: ${sustainedCPULoad.result.samples}`);
t.comment(` Average CPU usage: ${sustainedCPULoad.result.avgCPUUsage}%`);
t.comment(` Peak CPU usage: ${sustainedCPULoad.result.peakCPUUsage}%`);
t.comment(` Consistency: ${sustainedCPULoad.result.consistency}%`);
t.comment(` Stable performance: ${sustainedCPULoad.result.stable ? 'YES ✅' : 'NO ⚠️'}`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const avgCPUEfficiency = parseFloat(corpusCPUProfile.result.cpuEfficiency);
const cpuStability = sustainedCPULoad.result.stable;
t.comment(`CPU efficiency: ${avgCPUEfficiency}% ${avgCPUEfficiency > 50 ? '✅' : '⚠️'} (target: >50%)`);
t.comment(`CPU stability: ${cpuStability ? 'STABLE ✅' : 'UNSTABLE ⚠️'}`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,663 @@
/**
* @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';
import { CorpusLoader } from '../../suite/corpus.loader.js';
import { PerformanceTracker } from '../../suite/performance.tracker.js';
import * as os from 'os';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-07: Concurrent Processing');
tap.test('PERF-07: Concurrent Processing - should handle concurrent operations efficiently', async (t) => {
// Test 1: Concurrent format detection
const concurrentDetection = await performanceTracker.measureAsync(
'concurrent-format-detection',
async () => {
const einvoice = new EInvoice();
const results = {
concurrencyLevels: [],
optimalConcurrency: 0,
maxThroughput: 0
};
// 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
const levels = [1, 2, 4, 8, 16, 32, 64];
for (const concurrency of levels) {
const startTime = Date.now();
let completed = 0;
let correct = 0;
// Process in batches
const batchSize = concurrency;
const batches = [];
for (let i = 0; i < testData.length; i += batchSize) {
batches.push(testData.slice(i, i + batchSize));
}
for (const batch of batches) {
const promises = batch.map(async (item) => {
const format = await einvoice.detectFormat(item.content);
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));
const result = {
concurrency,
duration,
completed,
correct,
accuracy: ((correct / completed) * 100).toFixed(2),
throughput: throughput.toFixed(2),
avgLatency: (duration / completed).toFixed(2)
};
results.concurrencyLevels.push(result);
if (throughput > results.maxThroughput) {
results.maxThroughput = throughput;
results.optimalConcurrency = concurrency;
}
}
return results;
}
);
// Test 2: Concurrent validation
const concurrentValidation = await performanceTracker.measureAsync(
'concurrent-validation',
async () => {
const einvoice = new EInvoice();
const results = {
scenarios: [],
resourceContention: null
};
// Create test invoices with varying complexity
const createInvoice = (id: number, complexity: 'simple' | 'medium' | 'complex') => {
const itemCount = complexity === 'simple' ? 5 : complexity === 'medium' ? 20 : 50;
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `CONC-VAL-${complexity}-${id}`,
issueDate: '2024-02-20',
seller: { name: `Seller ${id}`, address: 'Address', country: 'US', taxId: `US${id}` },
buyer: { name: `Buyer ${id}`, address: 'Address', country: 'US', taxId: `US${id + 1000}` },
items: Array.from({ length: itemCount }, (_, i) => ({
description: `Item ${i + 1} for invoice ${id}`,
quantity: Math.random() * 10,
unitPrice: Math.random() * 100,
vatRate: [5, 10, 15, 20][Math.floor(Math.random() * 4)],
lineTotal: 0
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
};
// Calculate totals
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;
return invoice;
};
// Test scenarios
const scenarios = [
{ name: 'All simple', distribution: { simple: 30, medium: 0, complex: 0 } },
{ name: 'Mixed load', distribution: { simple: 10, medium: 15, complex: 5 } },
{ name: 'All complex', distribution: { simple: 0, medium: 0, complex: 30 } }
];
for (const scenario of scenarios) {
const invoices = [];
let id = 0;
// Create invoices according to distribution
for (const [complexity, count] of Object.entries(scenario.distribution)) {
for (let i = 0; i < count; i++) {
invoices.push(createInvoice(id++, complexity as any));
}
}
// Test with optimal concurrency from previous test
const concurrency = concurrentDetection.result.optimalConcurrency || 8;
const startTime = Date.now();
const startCPU = process.cpuUsage();
// Process concurrently
const results = [];
for (let i = 0; i < invoices.length; i += concurrency) {
const batch = invoices.slice(i, i + concurrency);
const batchResults = await Promise.all(
batch.map(async (invoice) => {
const start = Date.now();
const result = await einvoice.validateInvoice(invoice);
return {
duration: Date.now() - start,
valid: result.isValid,
errors: result.errors?.length || 0
};
})
);
results.push(...batchResults);
}
const totalDuration = Date.now() - startTime;
const cpuUsage = process.cpuUsage(startCPU);
// Analyze results
const validCount = results.filter(r => r.valid).length;
const avgDuration = results.reduce((sum, r) => sum + r.duration, 0) / results.length;
const maxDuration = Math.max(...results.map(r => r.duration));
results.scenarios.push({
name: scenario.name,
invoiceCount: invoices.length,
concurrency,
totalDuration,
throughput: (invoices.length / (totalDuration / 1000)).toFixed(2),
validCount,
validationRate: ((validCount / invoices.length) * 100).toFixed(2),
avgLatency: avgDuration.toFixed(2),
maxLatency: maxDuration,
cpuTime: ((cpuUsage.user + cpuUsage.system) / 1000).toFixed(2),
cpuEfficiency: (((cpuUsage.user + cpuUsage.system) / 1000) / totalDuration * 100).toFixed(2)
});
}
// Test resource contention
const contentionTest = async () => {
const invoice = createInvoice(9999, 'medium');
const concurrencyLevels = [1, 10, 50, 100];
const results = [];
for (const level of concurrencyLevels) {
const start = Date.now();
const promises = Array(level).fill(null).map(() =>
einvoice.validateInvoice(invoice)
);
await Promise.all(promises);
const duration = Date.now() - start;
results.push({
concurrency: level,
totalTime: duration,
avgTime: (duration / level).toFixed(2),
throughput: (level / (duration / 1000)).toFixed(2)
});
}
return results;
};
results.resourceContention = await contentionTest();
return results;
}
);
// Test 3: Concurrent file processing
const concurrentFileProcessing = await performanceTracker.measureAsync(
'concurrent-file-processing',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
fileCount: 0,
processedCount: 0,
concurrencyTests: [],
errorRates: new Map<number, number>()
};
// Sample files
const sampleFiles = files.slice(0, 50);
results.fileCount = sampleFiles.length;
// Test different concurrency strategies
const strategies = [
{ name: 'Sequential', concurrency: 1 },
{ name: 'Conservative', concurrency: 4 },
{ name: 'Moderate', concurrency: 8 },
{ name: 'Aggressive', concurrency: 16 },
{ name: 'Max', concurrency: os.cpus().length * 2 }
];
for (const strategy of strategies) {
const startTime = Date.now();
const startMemory = process.memoryUsage();
let processed = 0;
let errors = 0;
// Process files with specified concurrency
const queue = [...sampleFiles];
const activePromises = new Set();
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');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
processed++;
}
} catch (error) {
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;
const endMemory = process.memoryUsage();
results.concurrencyTests.push({
strategy: strategy.name,
concurrency: strategy.concurrency,
duration,
processed,
errors,
throughput: (processed / (duration / 1000)).toFixed(2),
avgFileTime: (duration / sampleFiles.length).toFixed(2),
memoryIncrease: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024).toFixed(2),
errorRate: ((errors / sampleFiles.length) * 100).toFixed(2)
});
results.errorRates.set(strategy.concurrency, errors);
results.processedCount = Math.max(results.processedCount, processed);
}
return results;
}
);
// Test 4: Mixed operation concurrency
const mixedOperationConcurrency = await performanceTracker.measureAsync(
'mixed-operation-concurrency',
async () => {
const einvoice = new EInvoice();
const results = {
operations: [],
contentionAnalysis: null
};
// Define mixed operations
const operations = [
{
name: 'detect',
fn: async (id: number) => {
const xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>MIXED-${id}</ID></Invoice>`;
return await einvoice.detectFormat(xml);
}
},
{
name: 'parse',
fn: async (id: number) => {
const xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>PARSE-${id}</ID><IssueDate>2024-01-01</IssueDate></Invoice>`;
return await einvoice.parseInvoice(xml, 'ubl');
}
},
{
name: 'validate',
fn: async (id: number) => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `VAL-${id}`,
issueDate: '2024-02-20',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: '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 }
}
};
return await einvoice.validateInvoice(invoice);
}
},
{
name: 'convert',
fn: async (id: number) => {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `CONV-${id}`,
issueDate: '2024-02-20',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: '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 }
}
};
return await einvoice.convertFormat(invoice, 'cii');
}
}
];
// Test mixed workload
const totalOperations = 200;
const operationMix = Array.from({ length: totalOperations }, (_, i) => ({
operation: operations[i % operations.length],
id: i
}));
// Shuffle to simulate real-world mix
for (let i = operationMix.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[operationMix[i], operationMix[j]] = [operationMix[j], operationMix[i]];
}
// Test with different concurrency levels
const concurrencyLevels = [1, 5, 10, 20];
for (const concurrency of concurrencyLevels) {
const startTime = Date.now();
const operationStats = new Map(operations.map(op => [op.name, { count: 0, totalTime: 0, errors: 0 }]));
// Process operations
for (let i = 0; i < operationMix.length; i += concurrency) {
const batch = operationMix.slice(i, i + concurrency);
await Promise.all(batch.map(async ({ operation, id }) => {
const opStart = Date.now();
try {
await operation.fn(id);
operationStats.get(operation.name)!.count++;
} catch {
operationStats.get(operation.name)!.errors++;
}
operationStats.get(operation.name)!.totalTime += Date.now() - opStart;
}));
}
const totalDuration = Date.now() - startTime;
results.operations.push({
concurrency,
totalDuration,
throughput: (totalOperations / (totalDuration / 1000)).toFixed(2),
operationBreakdown: Array.from(operationStats.entries()).map(([name, stats]) => ({
operation: name,
count: stats.count,
avgTime: stats.count > 0 ? (stats.totalTime / stats.count).toFixed(2) : 'N/A',
errorRate: ((stats.errors / (stats.count + stats.errors)) * 100).toFixed(2)
}))
});
}
// Analyze operation contention
const contentionTest = async () => {
const promises = [];
const contentionResults = [];
// Run all operations concurrently
for (let i = 0; i < 10; i++) {
for (const op of operations) {
promises.push(
(async () => {
const start = Date.now();
await op.fn(1000 + i);
return { operation: op.name, duration: Date.now() - start };
})()
);
}
}
const results = await Promise.all(promises);
// Group by operation
const grouped = results.reduce((acc, r) => {
if (!acc[r.operation]) acc[r.operation] = [];
acc[r.operation].push(r.duration);
return acc;
}, {} as Record<string, number[]>);
for (const [op, durations] of Object.entries(grouped)) {
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const min = Math.min(...durations);
const max = Math.max(...durations);
contentionResults.push({
operation: op,
avgDuration: avg.toFixed(2),
minDuration: min,
maxDuration: max,
variance: ((max - min) / avg * 100).toFixed(2)
});
}
return contentionResults;
};
results.contentionAnalysis = await contentionTest();
return results;
}
);
// Test 5: Concurrent corpus processing
const concurrentCorpusProcessing = await performanceTracker.measureAsync(
'concurrent-corpus-processing',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
totalFiles: files.length,
processedFiles: 0,
formatDistribution: new Map<string, number>(),
performanceMetrics: {
startTime: Date.now(),
endTime: 0,
peakConcurrency: 0,
avgResponseTime: 0,
throughputOverTime: []
}
};
// Process entire corpus with optimal concurrency
const optimalConcurrency = concurrentDetection.result.optimalConcurrency || 16;
const queue = [...files];
const activeOperations = new Map<string, { start: number; format?: string }>();
const responseTimes = [];
// Track throughput over time
const throughputInterval = setInterval(() => {
const elapsed = (Date.now() - results.performanceMetrics.startTime) / 1000;
const current = results.processedFiles;
results.performanceMetrics.throughputOverTime.push({
time: elapsed,
throughput: current / elapsed
});
}, 1000);
while (queue.length > 0 || activeOperations.size > 0) {
// Start new operations
while (activeOperations.size < optimalConcurrency && queue.length > 0) {
const file = queue.shift()!;
const operationId = `op-${Date.now()}-${Math.random()}`;
activeOperations.set(operationId, { start: Date.now() });
(async () => {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
activeOperations.get(operationId)!.format = format;
results.formatDistribution.set(format,
(results.formatDistribution.get(format) || 0) + 1
);
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
results.processedFiles++;
}
const duration = Date.now() - activeOperations.get(operationId)!.start;
responseTimes.push(duration);
} catch (error) {
// Skip failed files
} finally {
activeOperations.delete(operationId);
}
})();
if (activeOperations.size > results.performanceMetrics.peakConcurrency) {
results.performanceMetrics.peakConcurrency = activeOperations.size;
}
}
// Wait for some to complete
if (activeOperations.size > 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
clearInterval(throughputInterval);
results.performanceMetrics.endTime = Date.now();
// Calculate final metrics
const totalDuration = results.performanceMetrics.endTime - results.performanceMetrics.startTime;
results.performanceMetrics.avgResponseTime = responseTimes.length > 0 ?
responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0;
return {
totalFiles: results.totalFiles,
processedFiles: results.processedFiles,
successRate: ((results.processedFiles / results.totalFiles) * 100).toFixed(2),
totalDuration: totalDuration,
overallThroughput: (results.processedFiles / (totalDuration / 1000)).toFixed(2),
avgResponseTime: results.performanceMetrics.avgResponseTime.toFixed(2),
peakConcurrency: results.performanceMetrics.peakConcurrency,
formatDistribution: Array.from(results.formatDistribution.entries()),
throughputProgression: results.performanceMetrics.throughputOverTime.slice(-5)
};
}
);
// Summary
t.comment('\n=== PERF-07: Concurrent Processing Test Summary ===');
t.comment('\nConcurrent Format Detection:');
t.comment(' Concurrency | Duration | Throughput | Accuracy | Avg Latency');
t.comment(' ------------|----------|------------|----------|------------');
concurrentDetection.result.concurrencyLevels.forEach(level => {
t.comment(` ${String(level.concurrency).padEnd(11)} | ${String(level.duration + 'ms').padEnd(8)} | ${level.throughput.padEnd(10)}/s | ${level.accuracy.padEnd(8)}% | ${level.avgLatency}ms`);
});
t.comment(` Optimal concurrency: ${concurrentDetection.result.optimalConcurrency} (${concurrentDetection.result.maxThroughput.toFixed(2)} ops/sec)`);
t.comment('\nConcurrent Validation Scenarios:');
concurrentValidation.result.scenarios.forEach(scenario => {
t.comment(` ${scenario.name}:`);
t.comment(` - Invoices: ${scenario.invoiceCount}, Concurrency: ${scenario.concurrency}`);
t.comment(` - Duration: ${scenario.totalDuration}ms, Throughput: ${scenario.throughput}/sec`);
t.comment(` - Validation rate: ${scenario.validationRate}%`);
t.comment(` - Avg latency: ${scenario.avgLatency}ms, Max: ${scenario.maxLatency}ms`);
t.comment(` - CPU efficiency: ${scenario.cpuEfficiency}%`);
});
t.comment('\nConcurrent File Processing:');
t.comment(' Strategy | Concur. | Duration | Processed | Throughput | Errors | Memory');
t.comment(' ------------|---------|----------|-----------|------------|--------|-------');
concurrentFileProcessing.result.concurrencyTests.forEach(test => {
t.comment(` ${test.strategy.padEnd(11)} | ${String(test.concurrency).padEnd(7)} | ${String(test.duration + 'ms').padEnd(8)} | ${String(test.processed).padEnd(9)} | ${test.throughput.padEnd(10)}/s | ${test.errorRate.padEnd(6)}% | ${test.memoryIncrease}MB`);
});
t.comment('\nMixed Operation Concurrency:');
mixedOperationConcurrency.result.operations.forEach(test => {
t.comment(` Concurrency ${test.concurrency}: ${test.throughput} ops/sec`);
test.operationBreakdown.forEach(op => {
t.comment(` - ${op.operation}: ${op.count} ops, avg ${op.avgTime}ms, ${op.errorRate}% errors`);
});
});
t.comment('\nOperation Contention Analysis:');
mixedOperationConcurrency.result.contentionAnalysis.forEach(op => {
t.comment(` ${op.operation}: avg ${op.avgDuration}ms (${op.minDuration}-${op.maxDuration}ms), variance ${op.variance}%`);
});
t.comment('\nCorpus Concurrent Processing:');
t.comment(` Total files: ${concurrentCorpusProcessing.result.totalFiles}`);
t.comment(` Processed: ${concurrentCorpusProcessing.result.processedFiles}`);
t.comment(` Success rate: ${concurrentCorpusProcessing.result.successRate}%`);
t.comment(` Duration: ${(concurrentCorpusProcessing.result.totalDuration / 1000).toFixed(2)}s`);
t.comment(` Throughput: ${concurrentCorpusProcessing.result.overallThroughput} files/sec`);
t.comment(` Avg response time: ${concurrentCorpusProcessing.result.avgResponseTime}ms`);
t.comment(` Peak concurrency: ${concurrentCorpusProcessing.result.peakConcurrency}`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const targetConcurrency = 100; // Target: >100 concurrent ops/sec
const achievedThroughput = parseFloat(concurrentDetection.result.maxThroughput.toFixed(2));
t.comment(`Concurrent throughput: ${achievedThroughput} ops/sec ${achievedThroughput > targetConcurrency ? '✅' : '⚠️'} (target: >${targetConcurrency}/sec)`);
t.comment(`Optimal concurrency: ${concurrentDetection.result.optimalConcurrency} threads`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,680 @@
/**
* @file test.perf-08.large-files.ts
* @description Performance tests for large file processing
*/
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-08: Large File Processing');
tap.test('PERF-08: Large File Processing - should handle large files efficiently', async (t) => {
// Test 1: Large PEPPOL file processing
const largePEPPOLProcessing = await performanceTracker.measureAsync(
'large-peppol-processing',
async () => {
const files = await corpusLoader.getFilesByPattern('**/PEPPOL/**/*.xml');
const einvoice = new EInvoice();
const results = {
files: [],
memoryProfile: {
baseline: 0,
peak: 0,
increments: []
}
};
// Get baseline memory
if (global.gc) global.gc();
const baselineMemory = process.memoryUsage();
results.memoryProfile.baseline = baselineMemory.heapUsed / 1024 / 1024;
// Process PEPPOL files (known to be large)
for (const file of files) {
try {
const startTime = Date.now();
const startMemory = process.memoryUsage();
// Read file
const content = await plugins.fs.readFile(file, 'utf-8');
const fileSize = Buffer.byteLength(content, 'utf-8');
// Process file
const format = await einvoice.detectFormat(content);
const parseStart = Date.now();
const invoice = await einvoice.parseInvoice(content, format || 'ubl');
const parseEnd = Date.now();
const validationStart = Date.now();
const validationResult = await einvoice.validateInvoice(invoice);
const validationEnd = Date.now();
const endMemory = process.memoryUsage();
const totalTime = Date.now() - startTime;
const memoryUsed = (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024;
if (endMemory.heapUsed > results.memoryProfile.peak) {
results.memoryProfile.peak = endMemory.heapUsed / 1024 / 1024;
}
results.files.push({
path: file,
sizeKB: (fileSize / 1024).toFixed(2),
sizeMB: (fileSize / 1024 / 1024).toFixed(2),
format,
processingTime: totalTime,
parseTime: parseEnd - parseStart,
validationTime: validationEnd - validationStart,
memoryUsedMB: memoryUsed.toFixed(2),
throughputMBps: ((fileSize / 1024 / 1024) / (totalTime / 1000)).toFixed(2),
itemCount: invoice.data.items?.length || 0,
valid: validationResult.isValid
});
results.memoryProfile.increments.push(memoryUsed);
} catch (error) {
results.files.push({
path: file,
error: error.message
});
}
}
return results;
}
);
// Test 2: Synthetic large file generation and processing
const syntheticLargeFiles = await performanceTracker.measureAsync(
'synthetic-large-files',
async () => {
const einvoice = new EInvoice();
const results = {
tests: [],
scalingAnalysis: null
};
// Generate invoices of increasing size
const sizes = [
{ items: 100, name: '100 items' },
{ items: 500, name: '500 items' },
{ items: 1000, name: '1K items' },
{ items: 5000, name: '5K items' },
{ items: 10000, name: '10K items' }
];
for (const size of sizes) {
// Generate large invoice
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `LARGE-${size.items}`,
issueDate: '2024-02-25',
dueDate: '2024-03-25',
currency: 'EUR',
seller: {
name: 'Large File Test Seller Corporation International GmbH',
address: 'Hauptstraße 123-125, Building A, Floor 5',
city: 'Berlin',
postalCode: '10115',
country: 'DE',
taxId: 'DE123456789',
registrationNumber: 'HRB123456',
email: 'invoicing@largetest.de',
phone: '+49 30 123456789',
bankAccount: {
iban: 'DE89370400440532013000',
bic: 'COBADEFFXXX',
bankName: 'Commerzbank AG'
}
},
buyer: {
name: 'Large File Test Buyer Enterprises Ltd.',
address: '456 Commerce Boulevard, Suite 789',
city: 'Munich',
postalCode: '80331',
country: 'DE',
taxId: 'DE987654321',
registrationNumber: 'HRB654321',
email: 'ap@largebuyer.de',
phone: '+49 89 987654321'
},
items: Array.from({ length: size.items }, (_, i) => ({
itemId: `ITEM-${String(i + 1).padStart(6, '0')}`,
description: `Product Item Number ${i + 1} - Detailed description with technical specifications, compliance information, country of origin, weight, dimensions, and special handling instructions. This is a very detailed description to simulate real-world invoice data with comprehensive product information.`,
quantity: Math.floor(Math.random() * 100) + 1,
unitPrice: Math.random() * 1000,
vatRate: [0, 7, 19][Math.floor(Math.random() * 3)],
lineTotal: 0,
additionalInfo: {
weight: `${(Math.random() * 50).toFixed(2)}kg`,
dimensions: `${Math.floor(Math.random() * 100)}x${Math.floor(Math.random() * 100)}x${Math.floor(Math.random() * 100)}cm`,
countryOfOrigin: ['DE', 'FR', 'IT', 'CN', 'US'][Math.floor(Math.random() * 5)],
customsCode: `${Math.floor(Math.random() * 9000000000) + 1000000000}`,
serialNumber: `SN-${Date.now()}-${i}`,
batchNumber: `BATCH-${Math.floor(i / 100)}`
}
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 },
notes: 'This is a large invoice generated for performance testing purposes. ' +
'It contains a significant number of line items to test the system\'s ability ' +
'to handle large documents efficiently.'
}
};
// Calculate totals
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;
// Measure processing
if (global.gc) global.gc();
const startMemory = process.memoryUsage();
const startTime = Date.now();
// Generate XML
const xmlStart = Date.now();
const xml = await einvoice.generateXML(invoice);
const xmlEnd = Date.now();
const xmlSize = Buffer.byteLength(xml, 'utf-8');
// Parse back
const parseStart = Date.now();
const parsed = await einvoice.parseInvoice(xml, 'ubl');
const parseEnd = Date.now();
// Validate
const validateStart = Date.now();
const validation = await einvoice.validateInvoice(parsed);
const validateEnd = Date.now();
// Convert
const convertStart = Date.now();
const converted = await einvoice.convertFormat(parsed, 'cii');
const convertEnd = Date.now();
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.tests.push({
size: size.name,
items: size.items,
xmlSizeMB: (xmlSize / 1024 / 1024).toFixed(2),
totalTime: endTime - startTime,
xmlGeneration: xmlEnd - xmlStart,
parsing: parseEnd - parseStart,
validation: validateEnd - validateStart,
conversion: convertEnd - convertStart,
memoryUsedMB: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024).toFixed(2),
memoryPerItemKB: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / size.items).toFixed(2),
throughputMBps: ((xmlSize / 1024 / 1024) / ((endTime - startTime) / 1000)).toFixed(2),
valid: validation.isValid
});
}
// Analyze scaling
if (results.tests.length >= 3) {
const points = results.tests.map(t => ({
x: t.items,
y: t.totalTime
}));
// Simple linear regression
const n = points.length;
const sumX = points.reduce((sum, p) => sum + p.x, 0);
const sumY = points.reduce((sum, p) => sum + p.y, 0);
const sumXY = points.reduce((sum, p) => sum + p.x * p.y, 0);
const sumX2 = points.reduce((sum, p) => sum + p.x * p.x, 0);
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;
results.scalingAnalysis = {
type: slope < 0.5 ? 'Sub-linear' : slope <= 1.5 ? 'Linear' : 'Super-linear',
formula: `Time(ms) = ${slope.toFixed(3)} * items + ${intercept.toFixed(2)}`,
msPerItem: slope.toFixed(3)
};
}
return results;
}
);
// Test 3: Memory-efficient large file streaming
const streamingLargeFiles = await performanceTracker.measureAsync(
'streaming-large-files',
async () => {
const einvoice = new EInvoice();
const results = {
streamingSupported: false,
chunkProcessing: [],
memoryEfficiency: null
};
// Simulate large file processing in chunks
const totalItems = 10000;
const chunkSizes = [100, 500, 1000, 2000];
for (const chunkSize of chunkSizes) {
const chunks = Math.ceil(totalItems / chunkSize);
const startTime = Date.now();
const startMemory = process.memoryUsage();
let peakMemory = startMemory.heapUsed;
// Process in chunks
const chunkResults = [];
for (let chunk = 0; chunk < chunks; chunk++) {
const startItem = chunk * chunkSize;
const endItem = Math.min(startItem + chunkSize, totalItems);
// Create chunk invoice
const chunkInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `CHUNK-${chunk}`,
issueDate: '2024-02-25',
seller: { name: 'Chunk Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Chunk Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: endItem - startItem }, (_, i) => ({
description: `Chunk ${chunk} Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 19,
lineTotal: 100
})),
totals: {
netAmount: (endItem - startItem) * 100,
vatAmount: (endItem - startItem) * 19,
grossAmount: (endItem - startItem) * 119
}
}
};
// Process chunk
const chunkStart = Date.now();
await einvoice.validateInvoice(chunkInvoice);
const chunkEnd = Date.now();
chunkResults.push({
chunk,
items: endItem - startItem,
duration: chunkEnd - chunkStart
});
// Track peak memory
const currentMemory = process.memoryUsage();
if (currentMemory.heapUsed > peakMemory) {
peakMemory = currentMemory.heapUsed;
}
// Simulate cleanup between chunks
if (global.gc) global.gc();
}
const totalDuration = Date.now() - startTime;
const memoryIncrease = (peakMemory - startMemory.heapUsed) / 1024 / 1024;
results.chunkProcessing.push({
chunkSize,
chunks,
totalItems,
totalDuration,
avgChunkTime: chunkResults.reduce((sum, r) => sum + r.duration, 0) / chunkResults.length,
throughput: (totalItems / (totalDuration / 1000)).toFixed(2),
peakMemoryMB: (peakMemory / 1024 / 1024).toFixed(2),
memoryIncreaseMB: memoryIncrease.toFixed(2),
memoryPerItemKB: ((memoryIncrease * 1024) / totalItems).toFixed(3)
});
}
// Analyze memory efficiency
if (results.chunkProcessing.length > 0) {
const smallChunk = results.chunkProcessing[0];
const largeChunk = results.chunkProcessing[results.chunkProcessing.length - 1];
results.memoryEfficiency = {
smallChunkMemory: smallChunk.memoryIncreaseMB,
largeChunkMemory: largeChunk.memoryIncreaseMB,
memoryScaling: (parseFloat(largeChunk.memoryIncreaseMB) / parseFloat(smallChunk.memoryIncreaseMB)).toFixed(2),
recommendation: parseFloat(largeChunk.memoryIncreaseMB) < parseFloat(smallChunk.memoryIncreaseMB) * 2 ?
'Use larger chunks for better memory efficiency' :
'Use smaller chunks to reduce memory usage'
};
}
return results;
}
);
// Test 4: Corpus large file analysis
const corpusLargeFiles = await performanceTracker.measureAsync(
'corpus-large-file-analysis',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
totalFiles: 0,
largeFiles: [],
sizeDistribution: {
tiny: { count: 0, maxSize: 10 * 1024 }, // < 10KB
small: { count: 0, maxSize: 100 * 1024 }, // < 100KB
medium: { count: 0, maxSize: 1024 * 1024 }, // < 1MB
large: { count: 0, maxSize: 10 * 1024 * 1024 }, // < 10MB
huge: { count: 0, maxSize: Infinity } // >= 10MB
},
processingStats: {
avgTimePerKB: 0,
avgMemoryPerKB: 0
}
};
// Analyze all files
const fileSizes = [];
const processingMetrics = [];
for (const file of files) {
try {
const stats = await plugins.fs.stat(file);
const fileSize = stats.size;
results.totalFiles++;
// Categorize by size
if (fileSize < results.sizeDistribution.tiny.maxSize) {
results.sizeDistribution.tiny.count++;
} else if (fileSize < results.sizeDistribution.small.maxSize) {
results.sizeDistribution.small.count++;
} else if (fileSize < results.sizeDistribution.medium.maxSize) {
results.sizeDistribution.medium.count++;
} else if (fileSize < results.sizeDistribution.large.maxSize) {
results.sizeDistribution.large.count++;
} else {
results.sizeDistribution.huge.count++;
}
// Process large files
if (fileSize > 100 * 1024) { // Process files > 100KB
const content = await plugins.fs.readFile(file, 'utf-8');
const startTime = Date.now();
const startMemory = process.memoryUsage();
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
}
const endTime = Date.now();
const endMemory = process.memoryUsage();
const processingTime = endTime - startTime;
const memoryUsed = (endMemory.heapUsed - startMemory.heapUsed) / 1024; // KB
results.largeFiles.push({
path: file,
sizeKB: (fileSize / 1024).toFixed(2),
format,
processingTime,
memoryUsedKB: memoryUsed.toFixed(2),
timePerKB: (processingTime / (fileSize / 1024)).toFixed(3),
throughputKBps: ((fileSize / 1024) / (processingTime / 1000)).toFixed(2)
});
processingMetrics.push({
size: fileSize,
time: processingTime,
memory: memoryUsed
});
}
fileSizes.push(fileSize);
} catch (error) {
// Skip files that can't be processed
}
}
// Calculate statistics
if (processingMetrics.length > 0) {
const totalSize = processingMetrics.reduce((sum, m) => sum + m.size, 0);
const totalTime = processingMetrics.reduce((sum, m) => sum + m.time, 0);
const totalMemory = processingMetrics.reduce((sum, m) => sum + m.memory, 0);
results.processingStats.avgTimePerKB = (totalTime / (totalSize / 1024)).toFixed(3);
results.processingStats.avgMemoryPerKB = (totalMemory / (totalSize / 1024)).toFixed(3);
}
// Sort large files by size
results.largeFiles.sort((a, b) => parseFloat(b.sizeKB) - parseFloat(a.sizeKB));
return {
...results,
largeFiles: results.largeFiles.slice(0, 10), // Top 10 largest
avgFileSizeKB: fileSizes.length > 0 ?
(fileSizes.reduce((a, b) => a + b, 0) / fileSizes.length / 1024).toFixed(2) : 0
};
}
);
// Test 5: Stress test with extreme sizes
const extremeSizeStressTest = await performanceTracker.measureAsync(
'extreme-size-stress-test',
async () => {
const einvoice = new EInvoice();
const results = {
tests: [],
limits: {
maxItemsProcessed: 0,
maxSizeProcessedMB: 0,
failurePoint: null
}
};
// Test extreme scenarios
const extremeScenarios = [
{
name: 'Wide invoice (many items)',
generator: (count: number) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `EXTREME-WIDE-${count}`,
issueDate: '2024-02-25',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: count }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 10,
vatRate: 10,
lineTotal: 10
})),
totals: { netAmount: count * 10, vatAmount: count, grossAmount: count * 11 }
}
})
},
{
name: 'Deep invoice (long descriptions)',
generator: (size: number) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `EXTREME-DEEP-${size}`,
issueDate: '2024-02-25',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: [{
description: 'A'.repeat(size * 1024), // Size in KB
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
}],
totals: { netAmount: 100, vatAmount: 10, grossAmount: 110 }
}
})
}
];
// Test each scenario
for (const scenario of extremeScenarios) {
const testResults = {
scenario: scenario.name,
tests: []
};
// Test increasing sizes
const sizes = scenario.name.includes('Wide') ?
[1000, 5000, 10000, 20000, 50000] :
[100, 500, 1000, 2000, 5000]; // KB
for (const size of sizes) {
try {
const invoice = scenario.generator(size);
const startTime = Date.now();
const startMemory = process.memoryUsage();
// Try to process
const xml = await einvoice.generateXML(invoice);
const xmlSize = Buffer.byteLength(xml, 'utf-8') / 1024 / 1024; // MB
const parsed = await einvoice.parseInvoice(xml, invoice.format);
await einvoice.validateInvoice(parsed);
const endTime = Date.now();
const endMemory = process.memoryUsage();
testResults.tests.push({
size: scenario.name.includes('Wide') ? `${size} items` : `${size}KB text`,
success: true,
time: endTime - startTime,
memoryMB: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024).toFixed(2),
xmlSizeMB: xmlSize.toFixed(2)
});
// Update limits
if (scenario.name.includes('Wide') && size > results.limits.maxItemsProcessed) {
results.limits.maxItemsProcessed = size;
}
if (xmlSize > results.limits.maxSizeProcessedMB) {
results.limits.maxSizeProcessedMB = xmlSize;
}
} catch (error) {
testResults.tests.push({
size: scenario.name.includes('Wide') ? `${size} items` : `${size}KB text`,
success: false,
error: error.message
});
if (!results.limits.failurePoint) {
results.limits.failurePoint = {
scenario: scenario.name,
size,
error: error.message
};
}
break; // Stop testing larger sizes after failure
}
}
results.tests.push(testResults);
}
return results;
}
);
// Summary
t.comment('\n=== PERF-08: Large File Processing Test Summary ===');
if (largePEPPOLProcessing.result.files.length > 0) {
t.comment('\nLarge PEPPOL File Processing:');
largePEPPOLProcessing.result.files.forEach(file => {
if (!file.error) {
t.comment(` ${file.path.split('/').pop()}:`);
t.comment(` - Size: ${file.sizeMB}MB, Items: ${file.itemCount}`);
t.comment(` - Processing: ${file.processingTime}ms (parse: ${file.parseTime}ms, validate: ${file.validationTime}ms)`);
t.comment(` - Throughput: ${file.throughputMBps}MB/s`);
t.comment(` - Memory used: ${file.memoryUsedMB}MB`);
}
});
t.comment(` Peak memory: ${largePEPPOLProcessing.result.memoryProfile.peak.toFixed(2)}MB`);
}
t.comment('\nSynthetic Large File Scaling:');
t.comment(' Size | XML Size | Total Time | Parse | Validate | Convert | Memory | Throughput');
t.comment(' ----------|----------|------------|--------|----------|---------|--------|----------');
syntheticLargeFiles.result.tests.forEach(test => {
t.comment(` ${test.size.padEnd(9)} | ${test.xmlSizeMB.padEnd(8)}MB | ${String(test.totalTime + 'ms').padEnd(10)} | ${String(test.parsing + 'ms').padEnd(6)} | ${String(test.validation + 'ms').padEnd(8)} | ${String(test.conversion + 'ms').padEnd(7)} | ${test.memoryUsedMB.padEnd(6)}MB | ${test.throughputMBps}MB/s`);
});
if (syntheticLargeFiles.result.scalingAnalysis) {
t.comment(` Scaling: ${syntheticLargeFiles.result.scalingAnalysis.type}`);
t.comment(` Formula: ${syntheticLargeFiles.result.scalingAnalysis.formula}`);
}
t.comment('\nChunked Processing Efficiency:');
t.comment(' Chunk Size | Chunks | Duration | Throughput | Peak Memory | Memory/Item');
t.comment(' -----------|--------|----------|------------|-------------|------------');
streamingLargeFiles.result.chunkProcessing.forEach(chunk => {
t.comment(` ${String(chunk.chunkSize).padEnd(10)} | ${String(chunk.chunks).padEnd(6)} | ${String(chunk.totalDuration + 'ms').padEnd(8)} | ${chunk.throughput.padEnd(10)}/s | ${chunk.peakMemoryMB.padEnd(11)}MB | ${chunk.memoryPerItemKB}KB`);
});
if (streamingLargeFiles.result.memoryEfficiency) {
t.comment(` Recommendation: ${streamingLargeFiles.result.memoryEfficiency.recommendation}`);
}
t.comment('\nCorpus Large File Analysis:');
t.comment(` Total files: ${corpusLargeFiles.result.totalFiles}`);
t.comment(` Size distribution:`);
Object.entries(corpusLargeFiles.result.sizeDistribution).forEach(([size, data]: [string, any]) => {
t.comment(` - ${size}: ${data.count} files`);
});
t.comment(` Largest processed files:`);
corpusLargeFiles.result.largeFiles.slice(0, 5).forEach(file => {
t.comment(` - ${file.path.split('/').pop()}: ${file.sizeKB}KB, ${file.processingTime}ms, ${file.throughputKBps}KB/s`);
});
t.comment(` Average processing: ${corpusLargeFiles.result.processingStats.avgTimePerKB}ms/KB`);
t.comment('\nExtreme Size Stress Test:');
extremeSizeStressTest.result.tests.forEach(scenario => {
t.comment(` ${scenario.scenario}:`);
scenario.tests.forEach(test => {
t.comment(` - ${test.size}: ${test.success ? `${test.time}ms, ${test.xmlSizeMB}MB XML` : `${test.error}`}`);
});
});
t.comment(` Limits:`);
t.comment(` - Max items processed: ${extremeSizeStressTest.result.limits.maxItemsProcessed}`);
t.comment(` - Max size processed: ${extremeSizeStressTest.result.limits.maxSizeProcessedMB.toFixed(2)}MB`);
if (extremeSizeStressTest.result.limits.failurePoint) {
t.comment(` - Failure point: ${extremeSizeStressTest.result.limits.failurePoint.scenario} at ${extremeSizeStressTest.result.limits.failurePoint.size}`);
}
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const largeFileThroughput = syntheticLargeFiles.result.tests.length > 0 ?
parseFloat(syntheticLargeFiles.result.tests[syntheticLargeFiles.result.tests.length - 1].throughputMBps) : 0;
const targetThroughput = 1; // Target: >1MB/s for large files
t.comment(`Large file throughput: ${largeFileThroughput}MB/s ${largeFileThroughput > targetThroughput ? '✅' : '⚠️'} (target: >${targetThroughput}MB/s)`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,813 @@
/**
* @file test.perf-09.streaming.ts
* @description Performance tests for streaming operations
*/
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';
import { Readable, Writable, Transform } from 'stream';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-09: Streaming Performance');
tap.test('PERF-09: Streaming Performance - should handle streaming operations efficiently', async (t) => {
// Test 1: Streaming XML parsing
const streamingXMLParsing = await performanceTracker.measureAsync(
'streaming-xml-parsing',
async () => {
const einvoice = new EInvoice();
const results = {
tests: [],
memoryEfficiency: null
};
// Create test XML streams of different sizes
const createXMLStream = (itemCount: number): Readable => {
let currentItem = 0;
let headerSent = false;
let itemsSent = false;
return new Readable({
read() {
if (!headerSent) {
this.push(`<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>STREAM-${itemCount}</ID>
<IssueDate>2024-03-01</IssueDate>
<AccountingSupplierParty>
<Party>
<PartyName><Name>Streaming Supplier</Name></PartyName>
</Party>
</AccountingSupplierParty>
<AccountingCustomerParty>
<Party>
<PartyName><Name>Streaming Customer</Name></PartyName>
</Party>
</AccountingCustomerParty>
<InvoiceLine>`);
headerSent = true;
} else if (currentItem < itemCount) {
// Send items in chunks
const chunkSize = Math.min(10, itemCount - currentItem);
let chunk = '';
for (let i = 0; i < chunkSize; i++) {
chunk += `
<InvoiceLine>
<ID>${currentItem + i + 1}</ID>
<InvoicedQuantity>1</InvoicedQuantity>
<LineExtensionAmount>100.00</LineExtensionAmount>
<Item>
<Description>Streaming Item ${currentItem + i + 1}</Description>
</Item>
</InvoiceLine>`;
}
this.push(chunk);
currentItem += chunkSize;
// Simulate streaming delay
setTimeout(() => this.read(), 1);
} else if (!itemsSent) {
this.push(`
</InvoiceLine>
</Invoice>`);
itemsSent = true;
} else {
this.push(null); // End stream
}
}
});
};
// Test different stream sizes
const streamSizes = [
{ items: 10, name: 'Small stream' },
{ items: 100, name: 'Medium stream' },
{ items: 1000, name: 'Large stream' },
{ items: 5000, name: 'Very large stream' }
];
for (const size of streamSizes) {
const startTime = Date.now();
const startMemory = process.memoryUsage();
const memorySnapshots = [];
// Create monitoring interval
const monitorInterval = setInterval(() => {
memorySnapshots.push(process.memoryUsage().heapUsed / 1024 / 1024);
}, 100);
try {
// Simulate streaming parsing
const stream = createXMLStream(size.items);
const chunks = [];
let totalBytes = 0;
await new Promise((resolve, reject) => {
stream.on('data', (chunk) => {
chunks.push(chunk);
totalBytes += chunk.length;
});
stream.on('end', async () => {
clearInterval(monitorInterval);
// Parse accumulated XML
const xml = chunks.join('');
const format = await einvoice.detectFormat(xml);
const invoice = await einvoice.parseInvoice(xml, format || 'ubl');
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.tests.push({
size: size.name,
items: size.items,
totalBytes: (totalBytes / 1024).toFixed(2),
duration: endTime - startTime,
memoryUsed: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024).toFixed(2),
peakMemory: Math.max(...memorySnapshots).toFixed(2),
avgMemory: (memorySnapshots.reduce((a, b) => a + b, 0) / memorySnapshots.length).toFixed(2),
throughput: ((totalBytes / 1024) / ((endTime - startTime) / 1000)).toFixed(2),
itemsProcessed: invoice.data.items?.length || 0
});
resolve(null);
});
stream.on('error', reject);
});
} catch (error) {
clearInterval(monitorInterval);
results.tests.push({
size: size.name,
error: error.message
});
}
}
// Analyze memory efficiency
if (results.tests.length >= 2) {
const small = results.tests[0];
const large = results.tests[results.tests.length - 1];
if (!small.error && !large.error) {
results.memoryEfficiency = {
smallStreamMemory: small.memoryUsed,
largeStreamMemory: large.memoryUsed,
memoryScaling: (parseFloat(large.memoryUsed) / parseFloat(small.memoryUsed)).toFixed(2),
itemScaling: large.items / small.items,
efficient: parseFloat(large.memoryUsed) < parseFloat(small.memoryUsed) * (large.items / small.items)
};
}
}
return results;
}
);
// Test 2: Stream transformation pipeline
const streamTransformation = await performanceTracker.measureAsync(
'stream-transformation-pipeline',
async () => {
const einvoice = new EInvoice();
const results = {
pipelines: [],
transformationStats: null
};
// Create transformation streams
class FormatDetectionStream extends Transform {
constructor(private einvoice: EInvoice) {
super({ objectMode: true });
}
async _transform(chunk: any, encoding: string, callback: Function) {
try {
const format = await this.einvoice.detectFormat(chunk.content);
this.push({ ...chunk, format });
callback();
} catch (error) {
callback(error);
}
}
}
class ValidationStream extends Transform {
constructor(private einvoice: EInvoice) {
super({ objectMode: true });
}
async _transform(chunk: any, encoding: string, callback: Function) {
try {
if (chunk.format && chunk.format !== 'unknown') {
const invoice = await this.einvoice.parseInvoice(chunk.content, chunk.format);
const validation = await this.einvoice.validateInvoice(invoice);
this.push({ ...chunk, valid: validation.isValid, errors: validation.errors?.length || 0 });
} else {
this.push({ ...chunk, valid: false, errors: -1 });
}
callback();
} catch (error) {
callback(error);
}
}
}
// Test different pipeline configurations
const pipelineConfigs = [
{
name: 'Simple pipeline',
batchSize: 10,
stages: ['detect', 'validate']
},
{
name: 'Parallel pipeline',
batchSize: 50,
stages: ['detect', 'validate'],
parallel: true
},
{
name: 'Complex pipeline',
batchSize: 100,
stages: ['detect', 'parse', 'validate', 'convert']
}
];
// Create test data
const testInvoices = Array.from({ length: 100 }, (_, i) => ({
id: i,
content: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>PIPELINE-${i}</ID>
<IssueDate>2024-03-01</IssueDate>
<AccountingSupplierParty><Party><PartyName><Name>Supplier ${i}</Name></PartyName></Party></AccountingSupplierParty>
<AccountingCustomerParty><Party><PartyName><Name>Customer ${i}</Name></PartyName></Party></AccountingCustomerParty>
<InvoiceLine>
<ID>1</ID>
<InvoicedQuantity>1</InvoicedQuantity>
<LineExtensionAmount>${100 + i}</LineExtensionAmount>
</InvoiceLine>
</Invoice>`
}));
for (const config of pipelineConfigs) {
const startTime = Date.now();
const processedItems = [];
try {
// Create pipeline
const inputStream = new Readable({
objectMode: true,
read() {
const item = testInvoices.shift();
if (item) {
this.push(item);
} else {
this.push(null);
}
}
});
const outputStream = new Writable({
objectMode: true,
write(chunk, encoding, callback) {
processedItems.push(chunk);
callback();
}
});
// Build pipeline
let pipeline = inputStream;
if (config.stages.includes('detect')) {
pipeline = pipeline.pipe(new FormatDetectionStream(einvoice));
}
if (config.stages.includes('validate')) {
pipeline = pipeline.pipe(new ValidationStream(einvoice));
}
// Process
await new Promise((resolve, reject) => {
pipeline.pipe(outputStream)
.on('finish', resolve)
.on('error', reject);
});
const endTime = Date.now();
const duration = endTime - startTime;
results.pipelines.push({
name: config.name,
batchSize: config.batchSize,
stages: config.stages.length,
itemsProcessed: processedItems.length,
duration,
throughput: (processedItems.length / (duration / 1000)).toFixed(2),
avgLatency: (duration / processedItems.length).toFixed(2),
validItems: processedItems.filter(i => i.valid).length,
errorItems: processedItems.filter(i => !i.valid).length
});
} catch (error) {
results.pipelines.push({
name: config.name,
error: error.message
});
}
}
// Analyze transformation efficiency
if (results.pipelines.length > 0) {
const validPipelines = results.pipelines.filter(p => !p.error);
if (validPipelines.length > 0) {
const avgThroughput = validPipelines.reduce((sum, p) => sum + parseFloat(p.throughput), 0) / validPipelines.length;
const bestPipeline = validPipelines.reduce((best, p) =>
parseFloat(p.throughput) > parseFloat(best.throughput) ? p : best
);
results.transformationStats = {
avgThroughput: avgThroughput.toFixed(2),
bestPipeline: bestPipeline.name,
bestThroughput: bestPipeline.throughput
};
}
}
return results;
}
);
// Test 3: Backpressure handling
const backpressureHandling = await performanceTracker.measureAsync(
'backpressure-handling',
async () => {
const einvoice = new EInvoice();
const results = {
scenarios: [],
backpressureStats: null
};
// Test scenarios with different processing speeds
const scenarios = [
{
name: 'Fast producer, slow consumer',
producerDelay: 1,
consumerDelay: 10,
bufferSize: 100
},
{
name: 'Slow producer, fast consumer',
producerDelay: 10,
consumerDelay: 1,
bufferSize: 100
},
{
name: 'Balanced pipeline',
producerDelay: 5,
consumerDelay: 5,
bufferSize: 100
},
{
name: 'High volume burst',
producerDelay: 0,
consumerDelay: 5,
bufferSize: 1000
}
];
for (const scenario of scenarios) {
const startTime = Date.now();
const metrics = {
produced: 0,
consumed: 0,
buffered: 0,
maxBuffered: 0,
backpressureEvents: 0
};
try {
// Create producer stream
const producer = new Readable({
objectMode: true,
highWaterMark: scenario.bufferSize,
read() {
if (metrics.produced < 100) {
setTimeout(() => {
this.push({
id: metrics.produced++,
content: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>BP-${metrics.produced}</ID></Invoice>`
});
metrics.buffered = metrics.produced - metrics.consumed;
if (metrics.buffered > metrics.maxBuffered) {
metrics.maxBuffered = metrics.buffered;
}
}, scenario.producerDelay);
} else {
this.push(null);
}
}
});
// Create consumer stream with processing
const consumer = new Writable({
objectMode: true,
highWaterMark: scenario.bufferSize,
async write(chunk, encoding, callback) {
// Simulate processing
await new Promise(resolve => setTimeout(resolve, scenario.consumerDelay));
// Process invoice
const format = await einvoice.detectFormat(chunk.content);
metrics.consumed++;
metrics.buffered = metrics.produced - metrics.consumed;
callback();
}
});
// Monitor backpressure
producer.on('pause', () => metrics.backpressureEvents++);
// Process
await new Promise((resolve, reject) => {
producer.pipe(consumer)
.on('finish', resolve)
.on('error', reject);
});
const endTime = Date.now();
const duration = endTime - startTime;
results.scenarios.push({
name: scenario.name,
duration,
produced: metrics.produced,
consumed: metrics.consumed,
maxBuffered: metrics.maxBuffered,
backpressureEvents: metrics.backpressureEvents,
throughput: (metrics.consumed / (duration / 1000)).toFixed(2),
efficiency: ((metrics.consumed / metrics.produced) * 100).toFixed(2),
avgBufferUtilization: ((metrics.maxBuffered / scenario.bufferSize) * 100).toFixed(2)
});
} catch (error) {
results.scenarios.push({
name: scenario.name,
error: error.message
});
}
}
// Analyze backpressure handling
const validScenarios = results.scenarios.filter(s => !s.error);
if (validScenarios.length > 0) {
results.backpressureStats = {
avgBackpressureEvents: (validScenarios.reduce((sum, s) => sum + s.backpressureEvents, 0) / validScenarios.length).toFixed(2),
maxBufferUtilization: Math.max(...validScenarios.map(s => parseFloat(s.avgBufferUtilization))).toFixed(2),
recommendation: validScenarios.some(s => s.backpressureEvents > 10) ?
'Consider increasing buffer sizes or optimizing processing speed' :
'Backpressure handling is adequate'
};
}
return results;
}
);
// Test 4: Corpus streaming analysis
const corpusStreaming = await performanceTracker.measureAsync(
'corpus-streaming-analysis',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
streamableFiles: 0,
nonStreamableFiles: 0,
processingStats: {
streamed: [],
traditional: []
},
comparison: null
};
// Process sample files both ways
const sampleFiles = files.slice(0, 20);
for (const file of sampleFiles) {
try {
const stats = await plugins.fs.stat(file);
const fileSize = stats.size;
// Traditional processing
const traditionalStart = Date.now();
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
}
const traditionalEnd = Date.now();
results.processingStats.traditional.push({
size: fileSize,
time: traditionalEnd - traditionalStart
});
// Simulated streaming (chunked reading)
const streamingStart = Date.now();
const chunkSize = 64 * 1024; // 64KB chunks
const chunks = [];
// Read in chunks
const fd = await plugins.fs.open(file, 'r');
const buffer = Buffer.alloc(chunkSize);
let position = 0;
while (true) {
const { bytesRead } = await fd.read(buffer, 0, chunkSize, position);
if (bytesRead === 0) break;
chunks.push(buffer.slice(0, bytesRead).toString('utf-8'));
position += bytesRead;
}
await fd.close();
// Process accumulated content
const streamedContent = chunks.join('');
const streamedFormat = await einvoice.detectFormat(streamedContent);
if (streamedFormat && streamedFormat !== 'unknown') {
const invoice = await einvoice.parseInvoice(streamedContent, streamedFormat);
await einvoice.validateInvoice(invoice);
}
const streamingEnd = Date.now();
results.processingStats.streamed.push({
size: fileSize,
time: streamingEnd - streamingStart,
chunks: chunks.length
});
// Determine if file benefits from streaming
if (fileSize > 100 * 1024) { // Files > 100KB
results.streamableFiles++;
} else {
results.nonStreamableFiles++;
}
} catch (error) {
// Skip files that can't be processed
}
}
// Compare approaches
if (results.processingStats.traditional.length > 0 && results.processingStats.streamed.length > 0) {
const avgTraditional = results.processingStats.traditional.reduce((sum, s) => sum + s.time, 0) /
results.processingStats.traditional.length;
const avgStreamed = results.processingStats.streamed.reduce((sum, s) => sum + s.time, 0) /
results.processingStats.streamed.length;
const largeFiles = results.processingStats.traditional.filter(s => s.size > 100 * 1024);
const avgTraditionalLarge = largeFiles.length > 0 ?
largeFiles.reduce((sum, s) => sum + s.time, 0) / largeFiles.length : 0;
const largeStreamed = results.processingStats.streamed.filter(s => s.size > 100 * 1024);
const avgStreamedLarge = largeStreamed.length > 0 ?
largeStreamed.reduce((sum, s) => sum + s.time, 0) / largeStreamed.length : 0;
results.comparison = {
avgTraditionalTime: avgTraditional.toFixed(2),
avgStreamedTime: avgStreamed.toFixed(2),
overheadPercent: ((avgStreamed - avgTraditional) / avgTraditional * 100).toFixed(2),
largeFileImprovement: avgTraditionalLarge > 0 && avgStreamedLarge > 0 ?
((avgTraditionalLarge - avgStreamedLarge) / avgTraditionalLarge * 100).toFixed(2) : 'N/A',
recommendation: avgStreamed < avgTraditional * 1.1 ?
'Streaming provides benefits for this workload' :
'Traditional processing is more efficient for this workload'
};
}
return results;
}
);
// Test 5: Real-time streaming performance
const realtimeStreaming = await performanceTracker.measureAsync(
'realtime-streaming',
async () => {
const einvoice = new EInvoice();
const results = {
latencyTests: [],
jitterAnalysis: null
};
// Test real-time processing with different arrival rates
const arrivalRates = [
{ name: 'Low rate', invoicesPerSecond: 10 },
{ name: 'Medium rate', invoicesPerSecond: 50 },
{ name: 'High rate', invoicesPerSecond: 100 },
{ name: 'Burst rate', invoicesPerSecond: 200 }
];
for (const rate of arrivalRates) {
const testDuration = 5000; // 5 seconds
const interval = 1000 / rate.invoicesPerSecond;
const latencies = [];
let processed = 0;
let dropped = 0;
const startTime = Date.now();
// Create processing queue
const queue = [];
let processing = false;
const processNext = async () => {
if (processing || queue.length === 0) return;
processing = true;
const item = queue.shift();
try {
const processStart = Date.now();
const format = await einvoice.detectFormat(item.content);
const invoice = await einvoice.parseInvoice(item.content, format || 'ubl');
await einvoice.validateInvoice(invoice);
const latency = Date.now() - item.arrivalTime;
latencies.push(latency);
processed++;
} catch (error) {
dropped++;
}
processing = false;
if (queue.length > 0) {
setImmediate(processNext);
}
};
// Generate invoices at specified rate
const generator = setInterval(() => {
const invoice = {
arrivalTime: Date.now(),
content: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>RT-${Date.now()}</ID><IssueDate>2024-03-01</IssueDate></Invoice>`
};
// Apply backpressure - drop if queue is too large
if (queue.length < 100) {
queue.push(invoice);
processNext();
} else {
dropped++;
}
}, interval);
// Run test
await new Promise(resolve => setTimeout(resolve, testDuration));
clearInterval(generator);
// Process remaining items
while (queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
// Calculate statistics
if (latencies.length > 0) {
latencies.sort((a, b) => a - b);
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
const p50 = latencies[Math.floor(latencies.length * 0.5)];
const p95 = latencies[Math.floor(latencies.length * 0.95)];
const p99 = latencies[Math.floor(latencies.length * 0.99)];
// Calculate jitter
const jitters = [];
for (let i = 1; i < latencies.length; i++) {
jitters.push(Math.abs(latencies[i] - latencies[i - 1]));
}
const avgJitter = jitters.length > 0 ?
jitters.reduce((a, b) => a + b, 0) / jitters.length : 0;
results.latencyTests.push({
rate: rate.name,
targetRate: rate.invoicesPerSecond,
processed,
dropped,
actualRate: (processed / (testDuration / 1000)).toFixed(2),
avgLatency: avgLatency.toFixed(2),
p50Latency: p50,
p95Latency: p95,
p99Latency: p99,
avgJitter: avgJitter.toFixed(2),
dropRate: ((dropped / (processed + dropped)) * 100).toFixed(2)
});
}
}
// Analyze jitter and stability
if (results.latencyTests.length > 0) {
const avgJitters = results.latencyTests.map(t => parseFloat(t.avgJitter));
const avgDropRates = results.latencyTests.map(t => parseFloat(t.dropRate));
results.jitterAnalysis = {
avgJitter: (avgJitters.reduce((a, b) => a + b, 0) / avgJitters.length).toFixed(2),
maxJitter: Math.max(...avgJitters).toFixed(2),
avgDropRate: (avgDropRates.reduce((a, b) => a + b, 0) / avgDropRates.length).toFixed(2),
stable: Math.max(...avgJitters) < 50 && Math.max(...avgDropRates) < 5,
recommendation: Math.max(...avgDropRates) > 10 ?
'System cannot handle high arrival rates - consider scaling or optimization' :
'System handles real-time streaming adequately'
};
}
return results;
}
);
// Summary
t.comment('\n=== PERF-09: Streaming Performance Test Summary ===');
t.comment('\nStreaming XML Parsing:');
t.comment(' Stream Size | Items | Data | Duration | Memory | Peak | Throughput');
t.comment(' ------------|-------|---------|----------|--------|--------|----------');
streamingXMLParsing.result.tests.forEach(test => {
if (!test.error) {
t.comment(` ${test.size.padEnd(11)} | ${String(test.items).padEnd(5)} | ${test.totalBytes.padEnd(7)}KB | ${String(test.duration + 'ms').padEnd(8)} | ${test.memoryUsed.padEnd(6)}MB | ${test.peakMemory.padEnd(6)}MB | ${test.throughput}KB/s`);
}
});
if (streamingXMLParsing.result.memoryEfficiency) {
t.comment(` Memory efficiency: ${streamingXMLParsing.result.memoryEfficiency.efficient ? 'GOOD ✅' : 'POOR ⚠️'}`);
t.comment(` Scaling: ${streamingXMLParsing.result.memoryEfficiency.memoryScaling}x memory for ${streamingXMLParsing.result.memoryEfficiency.itemScaling}x items`);
}
t.comment('\nStream Transformation Pipeline:');
streamTransformation.result.pipelines.forEach(pipeline => {
if (!pipeline.error) {
t.comment(` ${pipeline.name}:`);
t.comment(` - Stages: ${pipeline.stages}, Items: ${pipeline.itemsProcessed}`);
t.comment(` - Duration: ${pipeline.duration}ms, Throughput: ${pipeline.throughput}/s`);
t.comment(` - Valid: ${pipeline.validItems}, Errors: ${pipeline.errorItems}`);
}
});
if (streamTransformation.result.transformationStats) {
t.comment(` Best pipeline: ${streamTransformation.result.transformationStats.bestPipeline} (${streamTransformation.result.transformationStats.bestThroughput}/s)`);
}
t.comment('\nBackpressure Handling:');
t.comment(' Scenario | Duration | Produced | Consumed | Max Buffer | BP Events | Efficiency');
t.comment(' ----------------------------|----------|----------|----------|------------|-----------|----------');
backpressureHandling.result.scenarios.forEach(scenario => {
if (!scenario.error) {
t.comment(` ${scenario.name.padEnd(27)} | ${String(scenario.duration + 'ms').padEnd(8)} | ${String(scenario.produced).padEnd(8)} | ${String(scenario.consumed).padEnd(8)} | ${String(scenario.maxBuffered).padEnd(10)} | ${String(scenario.backpressureEvents).padEnd(9)} | ${scenario.efficiency}%`);
}
});
if (backpressureHandling.result.backpressureStats) {
t.comment(` ${backpressureHandling.result.backpressureStats.recommendation}`);
}
t.comment('\nCorpus Streaming Analysis:');
t.comment(` Streamable files: ${corpusStreaming.result.streamableFiles}`);
t.comment(` Non-streamable files: ${corpusStreaming.result.nonStreamableFiles}`);
if (corpusStreaming.result.comparison) {
t.comment(` Traditional avg: ${corpusStreaming.result.comparison.avgTraditionalTime}ms`);
t.comment(` Streamed avg: ${corpusStreaming.result.comparison.avgStreamedTime}ms`);
t.comment(` Overhead: ${corpusStreaming.result.comparison.overheadPercent}%`);
t.comment(` Large file improvement: ${corpusStreaming.result.comparison.largeFileImprovement}%`);
t.comment(` ${corpusStreaming.result.comparison.recommendation}`);
}
t.comment('\nReal-time Streaming:');
t.comment(' Rate | Target | Actual | Processed | Dropped | Avg Latency | P95 | Jitter');
t.comment(' ------------|--------|--------|-----------|---------|-------------|--------|-------');
realtimeStreaming.result.latencyTests.forEach(test => {
t.comment(` ${test.rate.padEnd(11)} | ${String(test.targetRate).padEnd(6)} | ${test.actualRate.padEnd(6)} | ${String(test.processed).padEnd(9)} | ${test.dropRate.padEnd(7)}% | ${test.avgLatency.padEnd(11)}ms | ${String(test.p95Latency).padEnd(6)}ms | ${test.avgJitter}ms`);
});
if (realtimeStreaming.result.jitterAnalysis) {
t.comment(` System stability: ${realtimeStreaming.result.jitterAnalysis.stable ? 'STABLE ✅' : 'UNSTABLE ⚠️'}`);
t.comment(` ${realtimeStreaming.result.jitterAnalysis.recommendation}`);
}
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const streamingEfficient = streamingXMLParsing.result.memoryEfficiency?.efficient || false;
const realtimeStable = realtimeStreaming.result.jitterAnalysis?.stable || false;
t.comment(`Streaming memory efficiency: ${streamingEfficient ? 'EFFICIENT ✅' : 'INEFFICIENT ⚠️'}`);
t.comment(`Real-time stability: ${realtimeStable ? 'STABLE ✅' : 'UNSTABLE ⚠️'}`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,719 @@
/**
* @file test.perf-10.cache-efficiency.ts
* @description Performance tests for cache efficiency and optimization
*/
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-10: Cache Efficiency');
tap.test('PERF-10: Cache Efficiency - should demonstrate effective caching strategies', async (t) => {
// Test 1: Format detection cache
const formatDetectionCache = await performanceTracker.measureAsync(
'format-detection-cache',
async () => {
const einvoice = new EInvoice();
const results = {
withoutCache: {
iterations: 0,
totalTime: 0,
avgTime: 0
},
withCache: {
iterations: 0,
totalTime: 0,
avgTime: 0,
cacheHits: 0,
cacheMisses: 0
},
improvement: null
};
// Test data
const testDocuments = [
{
id: 'ubl-1',
content: '<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>UBL-001</ID></Invoice>'
},
{
id: 'cii-1',
content: '<?xml version="1.0"?><rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"><ID>CII-001</ID></rsm:CrossIndustryInvoice>'
},
{
id: 'unknown-1',
content: '<?xml version="1.0"?><UnknownFormat><ID>UNKNOWN-001</ID></UnknownFormat>'
}
];
// Test without cache (baseline)
const iterations = 100;
const startWithoutCache = Date.now();
for (let i = 0; i < iterations; i++) {
for (const doc of testDocuments) {
await einvoice.detectFormat(doc.content);
results.withoutCache.iterations++;
}
}
results.withoutCache.totalTime = Date.now() - startWithoutCache;
results.withoutCache.avgTime = results.withoutCache.totalTime / results.withoutCache.iterations;
// Implement simple cache
const formatCache = new Map<string, { format: string; timestamp: number }>();
const cacheMaxAge = 60000; // 1 minute
const detectFormatWithCache = async (content: string) => {
// Create cache key from content hash
const hash = Buffer.from(content).toString('base64').slice(0, 20);
// Check cache
const cached = formatCache.get(hash);
if (cached && Date.now() - cached.timestamp < cacheMaxAge) {
results.withCache.cacheHits++;
return cached.format;
}
// Cache miss
results.withCache.cacheMisses++;
const format = await einvoice.detectFormat(content);
// Store in cache
formatCache.set(hash, { format: format || 'unknown', timestamp: Date.now() });
return format;
};
// Test with cache
const startWithCache = Date.now();
for (let i = 0; i < iterations; i++) {
for (const doc of testDocuments) {
await detectFormatWithCache(doc.content);
results.withCache.iterations++;
}
}
results.withCache.totalTime = Date.now() - startWithCache;
results.withCache.avgTime = results.withCache.totalTime / results.withCache.iterations;
// Calculate improvement
results.improvement = {
speedup: (results.withoutCache.avgTime / results.withCache.avgTime).toFixed(2),
timeReduction: ((results.withoutCache.totalTime - results.withCache.totalTime) / results.withoutCache.totalTime * 100).toFixed(2),
hitRate: ((results.withCache.cacheHits / results.withCache.iterations) * 100).toFixed(2),
efficiency: results.withCache.cacheHits > 0 ?
((results.withCache.cacheHits / (results.withCache.cacheHits + results.withCache.cacheMisses)) * 100).toFixed(2) : '0'
};
return results;
}
);
// Test 2: Validation cache
const validationCache = await performanceTracker.measureAsync(
'validation-cache',
async () => {
const einvoice = new EInvoice();
const results = {
cacheStrategies: [],
optimalStrategy: null
};
// Test invoice
const testInvoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: 'CACHE-VAL-001',
issueDate: '2024-03-05',
seller: { name: 'Cache Test Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Cache Test Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 20 }, (_, i) => ({
description: `Item ${i + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 2000, vatAmount: 200, grossAmount: 2200 }
}
};
// Cache strategies to test
const strategies = [
{
name: 'No cache',
cacheSize: 0,
ttl: 0
},
{
name: 'Small cache',
cacheSize: 10,
ttl: 30000
},
{
name: 'Medium cache',
cacheSize: 100,
ttl: 60000
},
{
name: 'Large cache',
cacheSize: 1000,
ttl: 300000
},
{
name: 'LRU cache',
cacheSize: 50,
ttl: 120000,
lru: true
}
];
for (const strategy of strategies) {
const cache = new Map<string, { result: any; timestamp: number; accessCount: number }>();
let cacheHits = 0;
let cacheMisses = 0;
const validateWithCache = async (invoice: any) => {
const key = JSON.stringify(invoice).slice(0, 50); // Simple key generation
// Check cache
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < strategy.ttl) {
cacheHits++;
cached.accessCount++;
return cached.result;
}
// Cache miss
cacheMisses++;
const result = await einvoice.validateInvoice(invoice);
// Cache management
if (strategy.cacheSize > 0) {
if (cache.size >= strategy.cacheSize) {
if (strategy.lru) {
// Remove least recently used
let lruKey = '';
let minAccess = Infinity;
for (const [k, v] of cache.entries()) {
if (v.accessCount < minAccess) {
minAccess = v.accessCount;
lruKey = k;
}
}
cache.delete(lruKey);
} else {
// Remove oldest
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
}
cache.set(key, { result, timestamp: Date.now(), accessCount: 1 });
}
return result;
};
// Test with mixed workload
const workload = [];
// Repeated validations of same invoice
for (let i = 0; i < 50; i++) {
workload.push(testInvoice);
}
// Variations of the invoice
for (let i = 0; i < 30; i++) {
const variation = JSON.parse(JSON.stringify(testInvoice));
variation.data.invoiceNumber = `CACHE-VAL-${i + 2}`;
workload.push(variation);
}
// Repeat some variations
for (let i = 0; i < 20; i++) {
const variation = JSON.parse(JSON.stringify(testInvoice));
variation.data.invoiceNumber = `CACHE-VAL-${(i % 10) + 2}`;
workload.push(variation);
}
// Process workload
const startTime = Date.now();
for (const invoice of workload) {
await validateWithCache(invoice);
}
const totalTime = Date.now() - startTime;
results.cacheStrategies.push({
name: strategy.name,
cacheSize: strategy.cacheSize,
ttl: strategy.ttl,
lru: strategy.lru || false,
totalRequests: workload.length,
cacheHits,
cacheMisses,
hitRate: ((cacheHits / workload.length) * 100).toFixed(2),
totalTime,
avgTime: (totalTime / workload.length).toFixed(2),
finalCacheSize: cache.size,
memoryUsage: (cache.size * 1024).toFixed(0) // Rough estimate in bytes
});
}
// Find optimal strategy
const validStrategies = results.cacheStrategies.filter(s => s.cacheSize > 0);
if (validStrategies.length > 0) {
results.optimalStrategy = validStrategies.reduce((best, current) => {
const bestScore = parseFloat(best.hitRate) / (parseFloat(best.avgTime) + 1);
const currentScore = parseFloat(current.hitRate) / (parseFloat(current.avgTime) + 1);
return currentScore > bestScore ? current : best;
});
}
return results;
}
);
// Test 3: Schema cache efficiency
const schemaCache = await performanceTracker.measureAsync(
'schema-cache-efficiency',
async () => {
const einvoice = new EInvoice();
const results = {
schemaCaching: {
enabled: false,
tests: []
},
improvement: null
};
// Simulate schema validation with and without caching
const schemas = {
ubl: { size: 1024 * 50, parseTime: 50 }, // 50KB, 50ms parse time
cii: { size: 1024 * 60, parseTime: 60 }, // 60KB, 60ms parse time
zugferd: { size: 1024 * 80, parseTime: 80 }, // 80KB, 80ms parse time
xrechnung: { size: 1024 * 70, parseTime: 70 } // 70KB, 70ms parse time
};
const schemaCache = new Map<string, { schema: any; loadTime: number }>();
const loadSchemaWithoutCache = async (format: string) => {
const schema = schemas[format];
if (schema) {
await new Promise(resolve => setTimeout(resolve, schema.parseTime));
return { format, size: schema.size };
}
throw new Error(`Unknown schema format: ${format}`);
};
const loadSchemaWithCache = async (format: string) => {
const cached = schemaCache.get(format);
if (cached) {
results.schemaCaching.enabled = true;
return cached.schema;
}
const schema = await loadSchemaWithoutCache(format);
schemaCache.set(format, { schema, loadTime: Date.now() });
return schema;
};
// Test workload
const workload = [];
const formats = Object.keys(schemas);
// Initial load of each schema
for (const format of formats) {
workload.push(format);
}
// Repeated use of schemas
for (let i = 0; i < 100; i++) {
workload.push(formats[i % formats.length]);
}
// Test without cache
const startWithoutCache = Date.now();
for (const format of workload) {
await loadSchemaWithoutCache(format);
}
const timeWithoutCache = Date.now() - startWithoutCache;
// Test with cache
const startWithCache = Date.now();
for (const format of workload) {
await loadSchemaWithCache(format);
}
const timeWithCache = Date.now() - startWithCache;
// Calculate memory usage
let totalCachedSize = 0;
for (const format of schemaCache.keys()) {
totalCachedSize += schemas[format].size;
}
results.improvement = {
timeWithoutCache,
timeWithCache,
speedup: (timeWithoutCache / timeWithCache).toFixed(2),
timeReduction: ((timeWithoutCache - timeWithCache) / timeWithoutCache * 100).toFixed(2),
memoryCost: (totalCachedSize / 1024).toFixed(2), // KB
schemasLoaded: workload.length,
uniqueSchemas: schemaCache.size
};
return results;
}
);
// Test 4: Corpus cache analysis
const corpusCacheAnalysis = await performanceTracker.measureAsync(
'corpus-cache-analysis',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
cacheableOperations: {
formatDetection: { count: 0, duplicates: 0 },
parsing: { count: 0, duplicates: 0 },
validation: { count: 0, duplicates: 0 }
},
potentialSavings: null
};
// Track unique content hashes
const contentHashes = new Map<string, number>();
const formatResults = new Map<string, string>();
// Sample corpus files
const sampleFiles = files.slice(0, 100);
for (const file of sampleFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const hash = Buffer.from(content).toString('base64').slice(0, 32);
// Track content duplicates
const count = contentHashes.get(hash) || 0;
contentHashes.set(hash, count + 1);
if (count > 0) {
results.cacheableOperations.formatDetection.duplicates++;
results.cacheableOperations.parsing.duplicates++;
results.cacheableOperations.validation.duplicates++;
}
// Perform operations
const format = await einvoice.detectFormat(content);
results.cacheableOperations.formatDetection.count++;
if (format && format !== 'unknown') {
formatResults.set(hash, format);
const invoice = await einvoice.parseInvoice(content, format);
results.cacheableOperations.parsing.count++;
await einvoice.validateInvoice(invoice);
results.cacheableOperations.validation.count++;
}
} catch (error) {
// Skip failed files
}
}
// Calculate potential savings
const avgFormatDetectionTime = 5; // ms
const avgParsingTime = 20; // ms
const avgValidationTime = 50; // ms
results.potentialSavings = {
formatDetection: {
duplicateRatio: (results.cacheableOperations.formatDetection.duplicates /
results.cacheableOperations.formatDetection.count * 100).toFixed(2),
timeSavings: results.cacheableOperations.formatDetection.duplicates * avgFormatDetectionTime
},
parsing: {
duplicateRatio: (results.cacheableOperations.parsing.duplicates /
results.cacheableOperations.parsing.count * 100).toFixed(2),
timeSavings: results.cacheableOperations.parsing.duplicates * avgParsingTime
},
validation: {
duplicateRatio: (results.cacheableOperations.validation.duplicates /
results.cacheableOperations.validation.count * 100).toFixed(2),
timeSavings: results.cacheableOperations.validation.duplicates * avgValidationTime
},
totalTimeSavings: results.cacheableOperations.formatDetection.duplicates * avgFormatDetectionTime +
results.cacheableOperations.parsing.duplicates * avgParsingTime +
results.cacheableOperations.validation.duplicates * avgValidationTime,
memoryCost: contentHashes.size * 100 // Rough estimate: 100 bytes per cached item
};
return results;
}
);
// Test 5: Cache invalidation strategies
const cacheInvalidation = await performanceTracker.measureAsync(
'cache-invalidation-strategies',
async () => {
const einvoice = new EInvoice();
const results = {
strategies: [],
bestStrategy: null
};
// Test different invalidation strategies
const strategies = [
{
name: 'TTL only',
ttl: 60000,
maxSize: Infinity,
policy: 'ttl'
},
{
name: 'Size limited',
ttl: Infinity,
maxSize: 50,
policy: 'fifo'
},
{
name: 'LRU with TTL',
ttl: 120000,
maxSize: 100,
policy: 'lru'
},
{
name: 'Adaptive',
ttl: 60000,
maxSize: 100,
policy: 'adaptive'
}
];
for (const strategy of strategies) {
const cache = new Map<string, {
data: any;
timestamp: number;
accessCount: number;
lastAccess: number;
size: number;
}>();
let hits = 0;
let misses = 0;
let evictions = 0;
const cacheGet = (key: string) => {
const entry = cache.get(key);
if (!entry) {
misses++;
return null;
}
// Check TTL
if (strategy.ttl !== Infinity && Date.now() - entry.timestamp > strategy.ttl) {
cache.delete(key);
evictions++;
misses++;
return null;
}
// Update access info
entry.accessCount++;
entry.lastAccess = Date.now();
hits++;
return entry.data;
};
const cacheSet = (key: string, data: any, size: number = 1) => {
// Check size limit
if (cache.size >= strategy.maxSize) {
let keyToEvict = '';
switch (strategy.policy) {
case 'fifo':
keyToEvict = cache.keys().next().value;
break;
case 'lru':
let oldestAccess = Infinity;
for (const [k, v] of cache.entries()) {
if (v.lastAccess < oldestAccess) {
oldestAccess = v.lastAccess;
keyToEvict = k;
}
}
break;
case 'adaptive':
// Evict based on access frequency and age
let lowestScore = Infinity;
for (const [k, v] of cache.entries()) {
const age = Date.now() - v.timestamp;
const score = v.accessCount / (age / 1000);
if (score < lowestScore) {
lowestScore = score;
keyToEvict = k;
}
}
break;
}
if (keyToEvict) {
cache.delete(keyToEvict);
evictions++;
}
}
cache.set(key, {
data,
timestamp: Date.now(),
accessCount: 0,
lastAccess: Date.now(),
size
});
};
// Simulate workload with temporal locality
const workloadSize = 500;
const uniqueItems = 200;
const workload = [];
// Generate workload with patterns
for (let i = 0; i < workloadSize; i++) {
if (i < 100) {
// Initial unique accesses
workload.push(`item-${i % uniqueItems}`);
} else if (i < 300) {
// Repeated access to popular items
workload.push(`item-${Math.floor(Math.random() * 20)}`);
} else {
// Mixed access pattern
if (Math.random() < 0.3) {
// Access recent item
workload.push(`item-${Math.floor(Math.random() * 50)}`);
} else {
// Access any item
workload.push(`item-${Math.floor(Math.random() * uniqueItems)}`);
}
}
}
// Process workload
const startTime = Date.now();
for (const key of workload) {
const cached = cacheGet(key);
if (!cached) {
// Simulate data generation
const data = { key, value: Math.random() };
cacheSet(key, data);
}
}
const totalTime = Date.now() - startTime;
results.strategies.push({
name: strategy.name,
policy: strategy.policy,
ttl: strategy.ttl,
maxSize: strategy.maxSize,
hits,
misses,
hitRate: ((hits / (hits + misses)) * 100).toFixed(2),
evictions,
evictionRate: ((evictions / workloadSize) * 100).toFixed(2),
finalCacheSize: cache.size,
totalTime,
avgAccessTime: (totalTime / workloadSize).toFixed(2)
});
}
// Find best strategy
results.bestStrategy = results.strategies.reduce((best, current) => {
const bestScore = parseFloat(best.hitRate) - parseFloat(best.evictionRate);
const currentScore = parseFloat(current.hitRate) - parseFloat(current.evictionRate);
return currentScore > bestScore ? current : best;
});
return results;
}
);
// Summary
t.comment('\n=== PERF-10: Cache Efficiency Test Summary ===');
t.comment('\nFormat Detection Cache:');
t.comment(` Without cache: ${formatDetectionCache.result.withoutCache.totalTime}ms for ${formatDetectionCache.result.withoutCache.iterations} ops`);
t.comment(` With cache: ${formatDetectionCache.result.withCache.totalTime}ms for ${formatDetectionCache.result.withCache.iterations} ops`);
t.comment(` Cache hits: ${formatDetectionCache.result.withCache.cacheHits}, misses: ${formatDetectionCache.result.withCache.cacheMisses}`);
t.comment(` Speedup: ${formatDetectionCache.result.improvement.speedup}x`);
t.comment(` Hit rate: ${formatDetectionCache.result.improvement.hitRate}%`);
t.comment(` Time reduction: ${formatDetectionCache.result.improvement.timeReduction}%`);
t.comment('\nValidation Cache Strategies:');
t.comment(' Strategy | Size | TTL | Requests | Hits | Hit Rate | Avg Time | Memory');
t.comment(' -------------|------|--------|----------|------|----------|----------|--------');
validationCache.result.cacheStrategies.forEach(strategy => {
t.comment(` ${strategy.name.padEnd(12)} | ${String(strategy.cacheSize).padEnd(4)} | ${String(strategy.ttl).padEnd(6)} | ${String(strategy.totalRequests).padEnd(8)} | ${String(strategy.cacheHits).padEnd(4)} | ${strategy.hitRate.padEnd(8)}% | ${strategy.avgTime.padEnd(8)}ms | ${strategy.memoryUsage}B`);
});
if (validationCache.result.optimalStrategy) {
t.comment(` Optimal strategy: ${validationCache.result.optimalStrategy.name}`);
}
t.comment('\nSchema Cache Efficiency:');
t.comment(` Without cache: ${schemaCache.result.improvement.timeWithoutCache}ms`);
t.comment(` With cache: ${schemaCache.result.improvement.timeWithCache}ms`);
t.comment(` Speedup: ${schemaCache.result.improvement.speedup}x`);
t.comment(` Time reduction: ${schemaCache.result.improvement.timeReduction}%`);
t.comment(` Memory cost: ${schemaCache.result.improvement.memoryCost}KB`);
t.comment(` Schemas loaded: ${schemaCache.result.improvement.schemasLoaded}, unique: ${schemaCache.result.improvement.uniqueSchemas}`);
t.comment('\nCorpus Cache Analysis:');
t.comment(' Operation | Count | Duplicates | Ratio | Time Savings');
t.comment(' -----------------|-------|------------|--------|-------------');
['formatDetection', 'parsing', 'validation'].forEach(op => {
const stats = corpusCacheAnalysis.result.cacheableOperations[op];
const savings = corpusCacheAnalysis.result.potentialSavings[op];
t.comment(` ${op.padEnd(16)} | ${String(stats.count).padEnd(5)} | ${String(stats.duplicates).padEnd(10)} | ${savings.duplicateRatio.padEnd(6)}% | ${savings.timeSavings}ms`);
});
t.comment(` Total potential time savings: ${corpusCacheAnalysis.result.potentialSavings.totalTimeSavings}ms`);
t.comment(` Estimated memory cost: ${(corpusCacheAnalysis.result.potentialSavings.memoryCost / 1024).toFixed(2)}KB`);
t.comment('\nCache Invalidation Strategies:');
t.comment(' Strategy | Policy | Hits | Hit Rate | Evictions | Final Size');
t.comment(' --------------|----------|------|----------|-----------|------------');
cacheInvalidation.result.strategies.forEach(strategy => {
t.comment(` ${strategy.name.padEnd(13)} | ${strategy.policy.padEnd(8)} | ${String(strategy.hits).padEnd(4)} | ${strategy.hitRate.padEnd(8)}% | ${String(strategy.evictions).padEnd(9)} | ${strategy.finalCacheSize}`);
});
if (cacheInvalidation.result.bestStrategy) {
t.comment(` Best strategy: ${cacheInvalidation.result.bestStrategy.name} (${cacheInvalidation.result.bestStrategy.hitRate}% hit rate)`);
}
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const cacheSpeedup = parseFloat(formatDetectionCache.result.improvement.speedup);
const targetSpeedup = 2; // Target: >2x speedup with caching
t.comment(`Cache speedup: ${cacheSpeedup}x ${cacheSpeedup > targetSpeedup ? '✅' : '⚠️'} (target: >${targetSpeedup}x)`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,685 @@
/**
* @file test.perf-11.batch-processing.ts
* @description Performance tests for batch processing operations
*/
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';
import * as os from 'os';
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-11: Batch Processing');
tap.test('PERF-11: Batch Processing - should handle batch operations efficiently', async (t) => {
// Test 1: Batch size optimization
const batchSizeOptimization = await performanceTracker.measureAsync(
'batch-size-optimization',
async () => {
const einvoice = new EInvoice();
const results = {
batchSizes: [],
optimalBatchSize: 0,
maxThroughput: 0
};
// Create test invoices
const totalInvoices = 500;
const testInvoices = Array.from({ length: totalInvoices }, (_, i) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `BATCH-${i + 1}`,
issueDate: '2024-03-10',
seller: { name: `Seller ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i}` },
buyer: { name: `Buyer ${i + 1}`, address: 'Address', country: 'US', taxId: `US${i + 1000}` },
items: Array.from({ length: 10 }, (_, j) => ({
description: `Item ${j + 1}`,
quantity: 1,
unitPrice: 100,
vatRate: 10,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 100, grossAmount: 1100 }
}
}));
// Test different batch sizes
const batchSizes = [1, 5, 10, 20, 50, 100, 200];
for (const batchSize of batchSizes) {
const startTime = Date.now();
let processed = 0;
let errors = 0;
// Process in batches
for (let i = 0; i < testInvoices.length; i += batchSize) {
const batch = testInvoices.slice(i, Math.min(i + batchSize, testInvoices.length));
// Process batch
const batchPromises = batch.map(async (invoice) => {
try {
await einvoice.validateInvoice(invoice);
await einvoice.convertFormat(invoice, 'cii');
processed++;
return true;
} catch (error) {
errors++;
return false;
}
});
await Promise.all(batchPromises);
}
const totalTime = Date.now() - startTime;
const throughput = (processed / (totalTime / 1000));
const result = {
batchSize,
totalTime,
processed,
errors,
throughput: throughput.toFixed(2),
avgTimePerInvoice: (totalTime / processed).toFixed(2),
avgTimePerBatch: (totalTime / Math.ceil(totalInvoices / batchSize)).toFixed(2)
};
results.batchSizes.push(result);
if (throughput > results.maxThroughput) {
results.maxThroughput = throughput;
results.optimalBatchSize = batchSize;
}
}
return results;
}
);
// Test 2: Batch operation types
const batchOperationTypes = await performanceTracker.measureAsync(
'batch-operation-types',
async () => {
const einvoice = new EInvoice();
const results = {
operations: []
};
// Create test data
const batchSize = 50;
const testBatch = Array.from({ length: batchSize }, (_, i) => ({
xml: `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>BATCH-OP-${i}</ID><IssueDate>2024-03-10</IssueDate></Invoice>`,
invoice: {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `BATCH-OP-${i}`,
issueDate: '2024-03-10',
seller: { name: 'Batch Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'Batch 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 }
}
}
}));
// Test different batch operations
const operations = [
{
name: 'Batch format detection',
fn: async (batch: any[]) => {
const promises = batch.map(item => einvoice.detectFormat(item.xml));
return await Promise.all(promises);
}
},
{
name: 'Batch parsing',
fn: async (batch: any[]) => {
const promises = batch.map(item => einvoice.parseInvoice(item.xml, 'ubl'));
return await Promise.all(promises);
}
},
{
name: 'Batch validation',
fn: async (batch: any[]) => {
const promises = batch.map(item => einvoice.validateInvoice(item.invoice));
return await Promise.all(promises);
}
},
{
name: 'Batch conversion',
fn: async (batch: any[]) => {
const promises = batch.map(item => einvoice.convertFormat(item.invoice, 'cii'));
return await Promise.all(promises);
}
},
{
name: 'Batch pipeline',
fn: async (batch: any[]) => {
const promises = batch.map(async (item) => {
const format = await einvoice.detectFormat(item.xml);
const parsed = await einvoice.parseInvoice(item.xml, format || 'ubl');
const validated = await einvoice.validateInvoice(parsed);
const converted = await einvoice.convertFormat(parsed, 'cii');
return { format, validated: validated.isValid, converted: !!converted };
});
return await Promise.all(promises);
}
}
];
for (const operation of operations) {
const iterations = 10;
const times = [];
for (let i = 0; i < iterations; i++) {
const startTime = Date.now();
await operation.fn(testBatch);
const endTime = Date.now();
times.push(endTime - startTime);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
results.operations.push({
name: operation.name,
batchSize,
avgTime: avgTime.toFixed(2),
minTime,
maxTime,
throughput: (batchSize / (avgTime / 1000)).toFixed(2),
avgPerItem: (avgTime / batchSize).toFixed(2)
});
}
return results;
}
);
// Test 3: Batch error handling
const batchErrorHandling = await performanceTracker.measureAsync(
'batch-error-handling',
async () => {
const einvoice = new EInvoice();
const results = {
strategies: [],
recommendation: null
};
// Create batch with some invalid invoices
const batchSize = 100;
const errorRate = 0.2; // 20% errors
const testBatch = Array.from({ length: batchSize }, (_, i) => {
const hasError = Math.random() < errorRate;
if (hasError) {
return {
id: i,
invoice: {
format: 'ubl' as const,
data: {
// Invalid invoice - missing required fields
invoiceNumber: `ERROR-${i}`,
items: []
}
}
};
}
return {
id: i,
invoice: {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `VALID-${i}`,
issueDate: '2024-03-10',
seller: { name: 'Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: '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 }
}
}
};
});
// Test different error handling strategies
const strategies = [
{
name: 'Fail fast',
fn: async (batch: any[]) => {
const startTime = Date.now();
const results = [];
try {
for (const item of batch) {
const result = await einvoice.validateInvoice(item.invoice);
if (!result.isValid) {
throw new Error(`Validation failed for invoice ${item.id}`);
}
results.push({ id: item.id, success: true });
}
} catch (error) {
return {
time: Date.now() - startTime,
processed: results.length,
failed: batch.length - results.length,
results
};
}
return {
time: Date.now() - startTime,
processed: results.length,
failed: 0,
results
};
}
},
{
name: 'Continue on error',
fn: async (batch: any[]) => {
const startTime = Date.now();
const results = [];
let failed = 0;
for (const item of batch) {
try {
const result = await einvoice.validateInvoice(item.invoice);
results.push({ id: item.id, success: result.isValid });
if (!result.isValid) failed++;
} catch (error) {
results.push({ id: item.id, success: false, error: error.message });
failed++;
}
}
return {
time: Date.now() - startTime,
processed: results.length,
failed,
results
};
}
},
{
name: 'Parallel with error collection',
fn: async (batch: any[]) => {
const startTime = Date.now();
const promises = batch.map(async (item) => {
try {
const result = await einvoice.validateInvoice(item.invoice);
return { id: item.id, success: result.isValid };
} catch (error) {
return { id: item.id, success: false, error: error.message };
}
});
const results = await Promise.allSettled(promises);
const processed = results.filter(r => r.status === 'fulfilled').map(r => (r as any).value);
const failed = processed.filter(r => !r.success).length;
return {
time: Date.now() - startTime,
processed: processed.length,
failed,
results: processed
};
}
}
];
for (const strategy of strategies) {
const result = await strategy.fn(testBatch);
results.strategies.push({
name: strategy.name,
time: result.time,
processed: result.processed,
failed: result.failed,
successRate: ((result.processed - result.failed) / result.processed * 100).toFixed(2),
throughput: (result.processed / (result.time / 1000)).toFixed(2)
});
}
// Determine best strategy
results.recommendation = results.strategies.reduce((best, current) => {
// Balance between completion and speed
const bestScore = parseFloat(best.successRate) * parseFloat(best.throughput);
const currentScore = parseFloat(current.successRate) * parseFloat(current.throughput);
return currentScore > bestScore ? current.name : best.name;
}, results.strategies[0].name);
return results;
}
);
// Test 4: Memory-efficient batch processing
const memoryEfficientBatch = await performanceTracker.measureAsync(
'memory-efficient-batch',
async () => {
const einvoice = new EInvoice();
const results = {
approaches: [],
memoryProfile: null
};
// Create large dataset
const totalItems = 1000;
const createInvoice = (id: number) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `MEM-BATCH-${id}`,
issueDate: '2024-03-10',
seller: { name: `Memory Test Seller ${id}`, address: 'Long Address '.repeat(10), country: 'US', taxId: `US${id}` },
buyer: { name: `Memory Test Buyer ${id}`, address: 'Long Address '.repeat(10), country: 'US', taxId: `US${id + 10000}` },
items: Array.from({ length: 20 }, (_, j) => ({
description: `Detailed product description for item ${j + 1} with lots of text `.repeat(5),
quantity: j + 1,
unitPrice: 100 + j,
vatRate: 19,
lineTotal: (j + 1) * (100 + j)
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
});
// Approach 1: Load all in memory
const approach1 = async () => {
if (global.gc) global.gc();
const startMemory = process.memoryUsage();
const startTime = Date.now();
// Create all invoices
const allInvoices = Array.from({ length: totalItems }, (_, i) => createInvoice(i));
// Process all
const results = await Promise.all(
allInvoices.map(invoice => einvoice.validateInvoice(invoice))
);
const endTime = Date.now();
const endMemory = process.memoryUsage();
return {
approach: 'Load all in memory',
time: endTime - startTime,
peakMemory: (endMemory.heapUsed - startMemory.heapUsed) / 1024 / 1024,
processed: results.length,
memoryPerItem: ((endMemory.heapUsed - startMemory.heapUsed) / 1024 / totalItems).toFixed(2)
};
};
// Approach 2: Streaming with chunks
const approach2 = async () => {
if (global.gc) global.gc();
const startMemory = process.memoryUsage();
const startTime = Date.now();
const chunkSize = 50;
let processed = 0;
let peakMemory = 0;
for (let i = 0; i < totalItems; i += chunkSize) {
// Create chunk on demand
const chunk = Array.from(
{ length: Math.min(chunkSize, totalItems - i) },
(_, j) => createInvoice(i + j)
);
// Process chunk
await Promise.all(chunk.map(invoice => einvoice.validateInvoice(invoice)));
processed += chunk.length;
// Track memory
const currentMemory = process.memoryUsage();
const memoryUsed = currentMemory.heapUsed - startMemory.heapUsed;
if (memoryUsed > peakMemory) {
peakMemory = memoryUsed;
}
// Allow GC between chunks
if (global.gc && i % 200 === 0) global.gc();
}
const endTime = Date.now();
return {
approach: 'Streaming chunks',
time: endTime - startTime,
peakMemory: peakMemory / 1024 / 1024,
processed,
memoryPerItem: (peakMemory / 1024 / processed).toFixed(2)
};
};
// Approach 3: Generator-based processing
const approach3 = async () => {
if (global.gc) global.gc();
const startMemory = process.memoryUsage();
const startTime = Date.now();
let processed = 0;
let peakMemory = 0;
// Invoice generator
function* invoiceGenerator() {
for (let i = 0; i < totalItems; i++) {
yield createInvoice(i);
}
}
// Process using generator
const batchSize = 20;
const batch = [];
for (const invoice of invoiceGenerator()) {
batch.push(einvoice.validateInvoice(invoice));
if (batch.length >= batchSize) {
await Promise.all(batch);
processed += batch.length;
batch.length = 0;
// Track memory
const currentMemory = process.memoryUsage();
const memoryUsed = currentMemory.heapUsed - startMemory.heapUsed;
if (memoryUsed > peakMemory) {
peakMemory = memoryUsed;
}
}
}
// Process remaining
if (batch.length > 0) {
await Promise.all(batch);
processed += batch.length;
}
const endTime = Date.now();
return {
approach: 'Generator-based',
time: endTime - startTime,
peakMemory: peakMemory / 1024 / 1024,
processed,
memoryPerItem: (peakMemory / 1024 / processed).toFixed(2)
};
};
// Execute approaches
results.approaches.push(await approach1());
results.approaches.push(await approach2());
results.approaches.push(await approach3());
// Analyze memory efficiency
const sortedByMemory = [...results.approaches].sort((a, b) => a.peakMemory - b.peakMemory);
const sortedBySpeed = [...results.approaches].sort((a, b) => a.time - b.time);
results.memoryProfile = {
mostMemoryEfficient: sortedByMemory[0].approach,
fastest: sortedBySpeed[0].approach,
recommendation: sortedByMemory[0].peakMemory < sortedBySpeed[0].peakMemory * 0.5 ?
'Use memory-efficient approach for large datasets' :
'Use fastest approach if memory is not constrained'
};
return results;
}
);
// Test 5: Corpus batch processing
const corpusBatchProcessing = await performanceTracker.measureAsync(
'corpus-batch-processing',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
totalFiles: files.length,
batchResults: [],
overallStats: {
totalProcessed: 0,
totalTime: 0,
failures: 0,
avgBatchTime: 0
}
};
// Process corpus in batches
const batchSize = 20;
const maxBatches = 5; // Limit for testing
const startTime = Date.now();
for (let batchNum = 0; batchNum < maxBatches && batchNum * batchSize < files.length; batchNum++) {
const batchStart = batchNum * batchSize;
const batchFiles = files.slice(batchStart, batchStart + batchSize);
const batchStartTime = Date.now();
const batchResults = {
batchNumber: batchNum + 1,
filesInBatch: batchFiles.length,
processed: 0,
formats: new Map<string, number>(),
errors: 0
};
// Process batch in parallel
const promises = batchFiles.map(async (file) => {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
batchResults.formats.set(format, (batchResults.formats.get(format) || 0) + 1);
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
batchResults.processed++;
return { success: true, format };
} else {
batchResults.errors++;
return { success: false };
}
} catch (error) {
batchResults.errors++;
return { success: false, error: error.message };
}
});
await Promise.all(promises);
const batchEndTime = Date.now();
batchResults.batchTime = batchEndTime - batchStartTime;
batchResults.throughput = (batchResults.processed / (batchResults.batchTime / 1000)).toFixed(2);
results.batchResults.push({
...batchResults,
formats: Array.from(batchResults.formats.entries())
});
results.overallStats.totalProcessed += batchResults.processed;
results.overallStats.failures += batchResults.errors;
}
results.overallStats.totalTime = Date.now() - startTime;
results.overallStats.avgBatchTime = results.batchResults.length > 0 ?
results.batchResults.reduce((sum, b) => sum + b.batchTime, 0) / results.batchResults.length : 0;
return results;
}
);
// Summary
t.comment('\n=== PERF-11: Batch Processing Test Summary ===');
t.comment('\nBatch Size Optimization:');
t.comment(' Batch Size | Total Time | Processed | Throughput | Avg/Invoice | Avg/Batch');
t.comment(' -----------|------------|-----------|------------|-------------|----------');
batchSizeOptimization.result.batchSizes.forEach(size => {
t.comment(` ${String(size.batchSize).padEnd(10)} | ${String(size.totalTime + 'ms').padEnd(10)} | ${String(size.processed).padEnd(9)} | ${size.throughput.padEnd(10)}/s | ${size.avgTimePerInvoice.padEnd(11)}ms | ${size.avgTimePerBatch}ms`);
});
t.comment(` Optimal batch size: ${batchSizeOptimization.result.optimalBatchSize} (${batchSizeOptimization.result.maxThroughput.toFixed(2)} ops/sec)`);
t.comment('\nBatch Operation Types:');
batchOperationTypes.result.operations.forEach(op => {
t.comment(` ${op.name}:`);
t.comment(` - Avg time: ${op.avgTime}ms (${op.minTime}-${op.maxTime}ms)`);
t.comment(` - Throughput: ${op.throughput} ops/sec`);
t.comment(` - Per item: ${op.avgPerItem}ms`);
});
t.comment('\nBatch Error Handling Strategies:');
t.comment(' Strategy | Time | Processed | Failed | Success Rate | Throughput');
t.comment(' --------------------------|--------|-----------|--------|--------------|----------');
batchErrorHandling.result.strategies.forEach(strategy => {
t.comment(` ${strategy.name.padEnd(25)} | ${String(strategy.time + 'ms').padEnd(6)} | ${String(strategy.processed).padEnd(9)} | ${String(strategy.failed).padEnd(6)} | ${strategy.successRate.padEnd(12)}% | ${strategy.throughput}/s`);
});
t.comment(` Recommended strategy: ${batchErrorHandling.result.recommendation}`);
t.comment('\nMemory-Efficient Batch Processing:');
t.comment(' Approach | Time | Peak Memory | Processed | Memory/Item');
t.comment(' -------------------|---------|-------------|-----------|------------');
memoryEfficientBatch.result.approaches.forEach(approach => {
t.comment(` ${approach.approach.padEnd(18)} | ${String(approach.time + 'ms').padEnd(7)} | ${approach.peakMemory.toFixed(2).padEnd(11)}MB | ${String(approach.processed).padEnd(9)} | ${approach.memoryPerItem}KB`);
});
t.comment(` Most memory efficient: ${memoryEfficientBatch.result.memoryProfile.mostMemoryEfficient}`);
t.comment(` Fastest: ${memoryEfficientBatch.result.memoryProfile.fastest}`);
t.comment(` ${memoryEfficientBatch.result.memoryProfile.recommendation}`);
t.comment('\nCorpus Batch Processing:');
t.comment(` Total files: ${corpusBatchProcessing.result.totalFiles}`);
t.comment(` Batches processed: ${corpusBatchProcessing.result.batchResults.length}`);
t.comment(' Batch # | Files | Processed | Errors | Time | Throughput');
t.comment(' --------|-------|-----------|--------|---------|----------');
corpusBatchProcessing.result.batchResults.forEach(batch => {
t.comment(` ${String(batch.batchNumber).padEnd(7)} | ${String(batch.filesInBatch).padEnd(5)} | ${String(batch.processed).padEnd(9)} | ${String(batch.errors).padEnd(6)} | ${String(batch.batchTime + 'ms').padEnd(7)} | ${batch.throughput}/s`);
});
t.comment(` Overall:`);
t.comment(` - Total processed: ${corpusBatchProcessing.result.overallStats.totalProcessed}`);
t.comment(` - Total failures: ${corpusBatchProcessing.result.overallStats.failures}`);
t.comment(` - Total time: ${corpusBatchProcessing.result.overallStats.totalTime}ms`);
t.comment(` - Avg batch time: ${corpusBatchProcessing.result.overallStats.avgBatchTime.toFixed(2)}ms`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const optimalThroughput = batchSizeOptimization.result.maxThroughput;
const targetThroughput = 50; // Target: >50 ops/sec for batch processing
t.comment(`Batch throughput: ${optimalThroughput.toFixed(2)} ops/sec ${optimalThroughput > targetThroughput ? '✅' : '⚠️'} (target: >${targetThroughput} ops/sec)`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();

View File

@ -0,0 +1,688 @@
/**
* @file test.perf-12.resource-cleanup.ts
* @description Performance tests for resource cleanup and management
*/
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';
import * as os from 'os';
const corpusLoader = new CorpusLoader();
const performanceTracker = new PerformanceTracker('PERF-12: Resource Cleanup');
tap.test('PERF-12: Resource Cleanup - should properly manage and cleanup resources', async (t) => {
// Test 1: Memory cleanup after operations
const memoryCleanup = await performanceTracker.measureAsync(
'memory-cleanup-after-operations',
async () => {
const einvoice = new EInvoice();
const results = {
operations: [],
cleanupEfficiency: null
};
// Force initial GC to get baseline
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
const baselineMemory = process.memoryUsage();
// Test operations
const operations = [
{
name: 'Large invoice processing',
fn: async () => {
const largeInvoices = Array.from({ length: 100 }, (_, i) => ({
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `CLEANUP-${i}`,
issueDate: '2024-03-15',
seller: {
name: 'Large Data Seller ' + 'X'.repeat(1000),
address: 'Long Address ' + 'Y'.repeat(1000),
country: 'US',
taxId: 'US123456789'
},
buyer: {
name: 'Large Data Buyer ' + 'Z'.repeat(1000),
address: 'Long Address ' + 'W'.repeat(1000),
country: 'US',
taxId: 'US987654321'
},
items: Array.from({ length: 50 }, (_, j) => ({
description: `Item ${j} with very long description `.repeat(20),
quantity: Math.random() * 100,
unitPrice: Math.random() * 1000,
vatRate: 19,
lineTotal: 0
})),
totals: { netAmount: 0, vatAmount: 0, grossAmount: 0 }
}
}));
// Process all invoices
for (const invoice of largeInvoices) {
await einvoice.validateInvoice(invoice);
await einvoice.convertFormat(invoice, 'cii');
}
}
},
{
name: 'XML generation and parsing',
fn: async () => {
const xmlBuffers = [];
for (let i = 0; i < 50; i++) {
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `XML-GEN-${i}`,
issueDate: '2024-03-15',
seller: { name: 'XML Seller', address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: 'XML Buyer', address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 100 }, (_, j) => ({
description: `XML Item ${j}`,
quantity: 1,
unitPrice: 100,
vatRate: 19,
lineTotal: 100
})),
totals: { netAmount: 10000, vatAmount: 1900, grossAmount: 11900 }
}
};
const xml = await einvoice.generateXML(invoice);
xmlBuffers.push(Buffer.from(xml));
// Parse it back
await einvoice.parseInvoice(xml, 'ubl');
}
}
},
{
name: 'Concurrent operations',
fn: async () => {
const promises = [];
for (let i = 0; i < 200; i++) {
promises.push((async () => {
const xml = `<?xml version="1.0"?><Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"><ID>CONCURRENT-${i}</ID></Invoice>`;
const format = await einvoice.detectFormat(xml);
const parsed = await einvoice.parseInvoice(xml, format || 'ubl');
await einvoice.validateInvoice(parsed);
})());
}
await Promise.all(promises);
}
}
];
// Execute operations and measure cleanup
for (const operation of operations) {
// Memory before operation
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
const beforeOperation = process.memoryUsage();
// Execute operation
await operation.fn();
// Memory after operation (before cleanup)
const afterOperation = process.memoryUsage();
// Force cleanup
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
// Memory after cleanup
const afterCleanup = process.memoryUsage();
const memoryUsed = (afterOperation.heapUsed - beforeOperation.heapUsed) / 1024 / 1024;
const memoryRecovered = (afterOperation.heapUsed - afterCleanup.heapUsed) / 1024 / 1024;
const recoveryRate = memoryUsed > 0 ? (memoryRecovered / memoryUsed * 100) : 0;
results.operations.push({
name: operation.name,
memoryUsedMB: memoryUsed.toFixed(2),
memoryRecoveredMB: memoryRecovered.toFixed(2),
recoveryRate: recoveryRate.toFixed(2),
finalMemoryMB: ((afterCleanup.heapUsed - baselineMemory.heapUsed) / 1024 / 1024).toFixed(2),
externalMemoryMB: ((afterCleanup.external - baselineMemory.external) / 1024 / 1024).toFixed(2)
});
}
// Overall cleanup efficiency
const totalUsed = results.operations.reduce((sum, op) => sum + parseFloat(op.memoryUsedMB), 0);
const totalRecovered = results.operations.reduce((sum, op) => sum + parseFloat(op.memoryRecoveredMB), 0);
results.cleanupEfficiency = {
totalMemoryUsedMB: totalUsed.toFixed(2),
totalMemoryRecoveredMB: totalRecovered.toFixed(2),
overallRecoveryRate: totalUsed > 0 ? (totalRecovered / totalUsed * 100).toFixed(2) : '0',
memoryLeakDetected: results.operations.some(op => parseFloat(op.finalMemoryMB) > 10)
};
return results;
}
);
// Test 2: File handle cleanup
const fileHandleCleanup = await performanceTracker.measureAsync(
'file-handle-cleanup',
async () => {
const einvoice = new EInvoice();
const results = {
tests: [],
handleLeaks: false
};
// Monitor file handles (platform-specific)
const getOpenFiles = () => {
try {
if (process.platform === 'linux') {
const { execSync } = require('child_process');
const pid = process.pid;
const output = execSync(`ls /proc/${pid}/fd 2>/dev/null | wc -l`).toString();
return parseInt(output.trim());
}
return -1; // Not supported on this platform
} catch {
return -1;
}
};
const initialHandles = getOpenFiles();
// Test scenarios
const scenarios = [
{
name: 'Sequential file operations',
fn: async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const sampleFiles = files.slice(0, 20);
for (const file of sampleFiles) {
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
}
}
}
},
{
name: 'Concurrent file operations',
fn: async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const sampleFiles = files.slice(0, 20);
await Promise.all(sampleFiles.map(async (file) => {
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
}
}));
}
},
{
name: 'Large file streaming',
fn: async () => {
// Create temporary large file
const tempFile = '/tmp/einvoice-test-large.xml';
const largeContent = '<?xml version="1.0"?><Invoice>' + 'X'.repeat(1024 * 1024) + '</Invoice>';
await plugins.fs.writeFile(tempFile, largeContent);
try {
// Read in chunks
const chunkSize = 64 * 1024;
const fd = await plugins.fs.open(tempFile, 'r');
const buffer = Buffer.alloc(chunkSize);
let position = 0;
while (true) {
const { bytesRead } = await fd.read(buffer, 0, chunkSize, position);
if (bytesRead === 0) break;
position += bytesRead;
}
await fd.close();
} finally {
// Cleanup
await plugins.fs.unlink(tempFile).catch(() => {});
}
}
}
];
// Execute scenarios
for (const scenario of scenarios) {
const beforeHandles = getOpenFiles();
await scenario.fn();
// Allow time for handle cleanup
await new Promise(resolve => setTimeout(resolve, 100));
const afterHandles = getOpenFiles();
results.tests.push({
name: scenario.name,
beforeHandles: beforeHandles === -1 ? 'N/A' : beforeHandles,
afterHandles: afterHandles === -1 ? 'N/A' : afterHandles,
handleIncrease: beforeHandles === -1 || afterHandles === -1 ? 'N/A' : afterHandles - beforeHandles
});
}
// Check for handle leaks
const finalHandles = getOpenFiles();
if (initialHandles !== -1 && finalHandles !== -1) {
results.handleLeaks = finalHandles > initialHandles + 5; // Allow small variance
}
return results;
}
);
// Test 3: Event listener cleanup
const eventListenerCleanup = await performanceTracker.measureAsync(
'event-listener-cleanup',
async () => {
const einvoice = new EInvoice();
const results = {
listenerTests: [],
memoryLeaks: false
};
// Test event emitter scenarios
const EventEmitter = require('events');
const scenarios = [
{
name: 'Proper listener removal',
fn: async () => {
const emitter = new EventEmitter();
const listeners = [];
// Add listeners
for (let i = 0; i < 100; i++) {
const listener = () => {
// Process invoice event
einvoice.validateInvoice({
format: 'ubl',
data: { invoiceNumber: `EVENT-${i}` }
});
};
listeners.push(listener);
emitter.on('invoice', listener);
}
// Trigger events
for (let i = 0; i < 10; i++) {
emitter.emit('invoice');
}
// Remove listeners
for (const listener of listeners) {
emitter.removeListener('invoice', listener);
}
return {
listenersAdded: listeners.length,
listenersRemaining: emitter.listenerCount('invoice')
};
}
},
{
name: 'Once listeners',
fn: async () => {
const emitter = new EventEmitter();
let triggeredCount = 0;
// Add once listeners
for (let i = 0; i < 100; i++) {
emitter.once('process', () => {
triggeredCount++;
});
}
// Trigger event
emitter.emit('process');
return {
listenersAdded: 100,
triggered: triggeredCount,
listenersRemaining: emitter.listenerCount('process')
};
}
},
{
name: 'Memory pressure with listeners',
fn: async () => {
const emitter = new EventEmitter();
const startMemory = process.memoryUsage().heapUsed;
// Add many listeners with closures
for (let i = 0; i < 1000; i++) {
const largeData = Buffer.alloc(1024); // 1KB per listener
emitter.on('data', () => {
// Closure captures largeData
return largeData.length;
});
}
const afterAddMemory = process.memoryUsage().heapUsed;
// Remove all listeners
emitter.removeAllListeners('data');
// Force GC
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
const afterRemoveMemory = process.memoryUsage().heapUsed;
return {
memoryAddedMB: ((afterAddMemory - startMemory) / 1024 / 1024).toFixed(2),
memoryFreedMB: ((afterAddMemory - afterRemoveMemory) / 1024 / 1024).toFixed(2),
listenersRemaining: emitter.listenerCount('data')
};
}
}
];
// Execute scenarios
for (const scenario of scenarios) {
const result = await scenario.fn();
results.listenerTests.push({
name: scenario.name,
...result
});
}
// Check for memory leaks
const memoryTest = results.listenerTests.find(t => t.name === 'Memory pressure with listeners');
if (memoryTest) {
const freed = parseFloat(memoryTest.memoryFreedMB);
const added = parseFloat(memoryTest.memoryAddedMB);
results.memoryLeaks = freed < added * 0.8; // Should free at least 80%
}
return results;
}
);
// Test 4: Long-running operation cleanup
const longRunningCleanup = await performanceTracker.measureAsync(
'long-running-cleanup',
async () => {
const einvoice = new EInvoice();
const results = {
iterations: 0,
memorySnapshots: [],
stabilized: false,
trend: null
};
// Simulate long-running process
const testDuration = 10000; // 10 seconds
const snapshotInterval = 1000; // Every second
const startTime = Date.now();
const startMemory = process.memoryUsage();
let iteration = 0;
const snapshotTimer = setInterval(() => {
const memory = process.memoryUsage();
results.memorySnapshots.push({
time: Date.now() - startTime,
heapUsedMB: (memory.heapUsed / 1024 / 1024).toFixed(2),
externalMB: (memory.external / 1024 / 1024).toFixed(2),
iteration
});
}, snapshotInterval);
// Continuous operations
while (Date.now() - startTime < testDuration) {
// Create and process invoice
const invoice = {
format: 'ubl' as const,
data: {
documentType: 'INVOICE',
invoiceNumber: `LONG-RUN-${iteration}`,
issueDate: '2024-03-15',
seller: { name: `Seller ${iteration}`, address: 'Address', country: 'US', taxId: 'US123' },
buyer: { name: `Buyer ${iteration}`, address: 'Address', country: 'US', taxId: 'US456' },
items: Array.from({ length: 10 }, (_, i) => ({
description: `Item ${i}`,
quantity: 1,
unitPrice: 100,
vatRate: 19,
lineTotal: 100
})),
totals: { netAmount: 1000, vatAmount: 190, grossAmount: 1190 }
}
};
await einvoice.validateInvoice(invoice);
await einvoice.convertFormat(invoice, 'cii');
iteration++;
results.iterations = iteration;
// Periodic cleanup
if (iteration % 50 === 0 && global.gc) {
global.gc();
}
// Small delay to prevent CPU saturation
await new Promise(resolve => setTimeout(resolve, 10));
}
clearInterval(snapshotTimer);
// Analyze memory trend
if (results.memorySnapshots.length >= 5) {
const firstHalf = results.memorySnapshots.slice(0, Math.floor(results.memorySnapshots.length / 2));
const secondHalf = results.memorySnapshots.slice(Math.floor(results.memorySnapshots.length / 2));
const avgFirstHalf = firstHalf.reduce((sum, s) => sum + parseFloat(s.heapUsedMB), 0) / firstHalf.length;
const avgSecondHalf = secondHalf.reduce((sum, s) => sum + parseFloat(s.heapUsedMB), 0) / secondHalf.length;
results.trend = {
firstHalfAvgMB: avgFirstHalf.toFixed(2),
secondHalfAvgMB: avgSecondHalf.toFixed(2),
increasing: avgSecondHalf > avgFirstHalf * 1.1,
stable: Math.abs(avgSecondHalf - avgFirstHalf) < avgFirstHalf * 0.1
};
results.stabilized = results.trend.stable;
}
return results;
}
);
// Test 5: Corpus cleanup verification
const corpusCleanupVerification = await performanceTracker.measureAsync(
'corpus-cleanup-verification',
async () => {
const files = await corpusLoader.getFilesByPattern('**/*.xml');
const einvoice = new EInvoice();
const results = {
phases: [],
overallCleanup: null
};
// Process corpus in phases
const phases = [
{ name: 'Initial batch', count: 50 },
{ name: 'Heavy processing', count: 100 },
{ name: 'Final batch', count: 50 }
];
if (global.gc) global.gc();
const initialMemory = process.memoryUsage();
for (const phase of phases) {
const phaseStart = process.memoryUsage();
const startTime = Date.now();
// Process files
const phaseFiles = files.slice(0, phase.count);
let processed = 0;
let errors = 0;
for (const file of phaseFiles) {
try {
const content = await plugins.fs.readFile(file, 'utf-8');
const format = await einvoice.detectFormat(content);
if (format && format !== 'unknown') {
const invoice = await einvoice.parseInvoice(content, format);
await einvoice.validateInvoice(invoice);
// Heavy processing for middle phase
if (phase.name === 'Heavy processing') {
await einvoice.convertFormat(invoice, 'cii');
await einvoice.generateXML(invoice);
}
processed++;
}
} catch (error) {
errors++;
}
}
const phaseEnd = process.memoryUsage();
// Cleanup between phases
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 200));
}
const afterCleanup = process.memoryUsage();
results.phases.push({
name: phase.name,
filesProcessed: processed,
errors,
duration: Date.now() - startTime,
memoryUsedMB: ((phaseEnd.heapUsed - phaseStart.heapUsed) / 1024 / 1024).toFixed(2),
memoryAfterCleanupMB: ((afterCleanup.heapUsed - phaseStart.heapUsed) / 1024 / 1024).toFixed(2),
cleanupEfficiency: ((phaseEnd.heapUsed - afterCleanup.heapUsed) / (phaseEnd.heapUsed - phaseStart.heapUsed) * 100).toFixed(2)
});
}
// Final cleanup
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 500));
}
const finalMemory = process.memoryUsage();
results.overallCleanup = {
initialMemoryMB: (initialMemory.heapUsed / 1024 / 1024).toFixed(2),
finalMemoryMB: (finalMemory.heapUsed / 1024 / 1024).toFixed(2),
totalIncreaseMB: ((finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024).toFixed(2),
acceptableIncrease: (finalMemory.heapUsed - initialMemory.heapUsed) < 50 * 1024 * 1024 // Less than 50MB
};
return results;
}
);
// Summary
t.comment('\n=== PERF-12: Resource Cleanup Test Summary ===');
t.comment('\nMemory Cleanup After Operations:');
t.comment(' Operation | Used | Recovered | Recovery % | Final | External');
t.comment(' -------------------------|---------|-----------|------------|---------|----------');
memoryCleanup.result.operations.forEach(op => {
t.comment(` ${op.name.padEnd(24)} | ${op.memoryUsedMB.padEnd(7)}MB | ${op.memoryRecoveredMB.padEnd(9)}MB | ${op.recoveryRate.padEnd(10)}% | ${op.finalMemoryMB.padEnd(7)}MB | ${op.externalMemoryMB}MB`);
});
t.comment(` Overall efficiency:`);
t.comment(` - Total used: ${memoryCleanup.result.cleanupEfficiency.totalMemoryUsedMB}MB`);
t.comment(` - Total recovered: ${memoryCleanup.result.cleanupEfficiency.totalMemoryRecoveredMB}MB`);
t.comment(` - Recovery rate: ${memoryCleanup.result.cleanupEfficiency.overallRecoveryRate}%`);
t.comment(` - Memory leak detected: ${memoryCleanup.result.cleanupEfficiency.memoryLeakDetected ? 'YES ⚠️' : 'NO ✅'}`);
t.comment('\nFile Handle Cleanup:');
fileHandleCleanup.result.tests.forEach(test => {
t.comment(` ${test.name}:`);
t.comment(` - Before: ${test.beforeHandles}, After: ${test.afterHandles}`);
t.comment(` - Handle increase: ${test.handleIncrease}`);
});
t.comment(` Handle leaks detected: ${fileHandleCleanup.result.handleLeaks ? 'YES ⚠️' : 'NO ✅'}`);
t.comment('\nEvent Listener Cleanup:');
eventListenerCleanup.result.listenerTests.forEach(test => {
t.comment(` ${test.name}:`);
if (test.listenersAdded !== undefined) {
t.comment(` - Added: ${test.listenersAdded}, Remaining: ${test.listenersRemaining}`);
}
if (test.memoryAddedMB !== undefined) {
t.comment(` - Memory added: ${test.memoryAddedMB}MB, Freed: ${test.memoryFreedMB}MB`);
}
});
t.comment(` Memory leaks in listeners: ${eventListenerCleanup.result.memoryLeaks ? 'YES ⚠️' : 'NO ✅'}`);
t.comment('\nLong-Running Operation Cleanup:');
t.comment(` Iterations: ${longRunningCleanup.result.iterations}`);
t.comment(` Memory snapshots: ${longRunningCleanup.result.memorySnapshots.length}`);
if (longRunningCleanup.result.trend) {
t.comment(` Memory trend:`);
t.comment(` - First half avg: ${longRunningCleanup.result.trend.firstHalfAvgMB}MB`);
t.comment(` - Second half avg: ${longRunningCleanup.result.trend.secondHalfAvgMB}MB`);
t.comment(` - Trend: ${longRunningCleanup.result.trend.increasing ? 'INCREASING ⚠️' : longRunningCleanup.result.trend.stable ? 'STABLE ✅' : 'DECREASING ✅'}`);
}
t.comment(` Memory stabilized: ${longRunningCleanup.result.stabilized ? 'YES ✅' : 'NO ⚠️'}`);
t.comment('\nCorpus Cleanup Verification:');
t.comment(' Phase | Files | Duration | Memory Used | After Cleanup | Efficiency');
t.comment(' -------------------|-------|----------|-------------|---------------|------------');
corpusCleanupVerification.result.phases.forEach(phase => {
t.comment(` ${phase.name.padEnd(18)} | ${String(phase.filesProcessed).padEnd(5)} | ${String(phase.duration + 'ms').padEnd(8)} | ${phase.memoryUsedMB.padEnd(11)}MB | ${phase.memoryAfterCleanupMB.padEnd(13)}MB | ${phase.cleanupEfficiency}%`);
});
t.comment(` Overall cleanup:`);
t.comment(` - Initial memory: ${corpusCleanupVerification.result.overallCleanup.initialMemoryMB}MB`);
t.comment(` - Final memory: ${corpusCleanupVerification.result.overallCleanup.finalMemoryMB}MB`);
t.comment(` - Total increase: ${corpusCleanupVerification.result.overallCleanup.totalIncreaseMB}MB`);
t.comment(` - Acceptable increase: ${corpusCleanupVerification.result.overallCleanup.acceptableIncrease ? 'YES ✅' : 'NO ⚠️'}`);
// Performance targets check
t.comment('\n=== Performance Targets Check ===');
const memoryRecoveryRate = parseFloat(memoryCleanup.result.cleanupEfficiency.overallRecoveryRate);
const targetRecoveryRate = 80; // Target: >80% memory recovery
const noMemoryLeaks = !memoryCleanup.result.cleanupEfficiency.memoryLeakDetected &&
!fileHandleCleanup.result.handleLeaks &&
!eventListenerCleanup.result.memoryLeaks &&
longRunningCleanup.result.stabilized;
t.comment(`Memory recovery rate: ${memoryRecoveryRate}% ${memoryRecoveryRate > targetRecoveryRate ? '✅' : '⚠️'} (target: >${targetRecoveryRate}%)`);
t.comment(`Resource leak prevention: ${noMemoryLeaks ? 'PASSED ✅' : 'FAILED ⚠️'}`);
// Overall performance summary
t.comment('\n=== Overall Performance Summary ===');
performanceTracker.logSummary();
t.end();
});
tap.start();