dcrouter/test/suite/smtpclient_performance/test.cperf-06.caching-strategies.ts

769 lines
27 KiB
TypeScript
Raw Normal View History

2025-05-24 18:12:08 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from './plugins.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
tap.test('CPERF-06: should implement efficient caching strategies', async (tools) => {
const testId = 'CPERF-06-caching-strategies';
console.log(`\n${testId}: Testing caching strategies performance...`);
let scenarioCount = 0;
// Scenario 1: DNS resolution caching
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing DNS resolution caching`);
let dnsLookupCount = 0;
const dnsCache = new Map<string, { address: string; timestamp: number }>();
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 dns-cache.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-dns-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Simulate DNS lookup caching
const mockDnsLookup = (hostname: string) => {
const cached = dnsCache.get(hostname);
const now = Date.now();
if (cached && (now - cached.timestamp) < 300000) { // 5 minute cache
console.log(` [DNS] Cache hit for ${hostname}`);
return cached.address;
}
dnsLookupCount++;
console.log(` [DNS] Cache miss for ${hostname} (lookup #${dnsLookupCount})`);
const address = testServer.hostname; // Mock resolution
dnsCache.set(hostname, { address, timestamp: now });
return address;
};
// Test multiple connections to same host
const connectionCount = 10;
console.log(` Creating ${connectionCount} connections to test DNS caching...`);
const clients: any[] = [];
const startTime = Date.now();
for (let i = 0; i < connectionCount; i++) {
// Simulate DNS lookup
const resolvedHost = mockDnsLookup(testServer.hostname);
const client = createSmtpClient({
host: resolvedHost,
port: testServer.port,
secure: false
});
clients.push(client);
}
// Send emails through cached connections
const emails = clients.map((client, i) =>
new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: `DNS cache test ${i + 1}`,
text: `Testing DNS resolution caching - connection ${i + 1}`
})
);
await Promise.all(emails.map((email, i) => clients[i].sendMail(email)));
const totalTime = Date.now() - startTime;
const cacheHitRate = ((connectionCount - dnsLookupCount) / connectionCount) * 100;
console.log(` DNS lookups performed: ${dnsLookupCount}/${connectionCount}`);
console.log(` Cache hit rate: ${cacheHitRate.toFixed(1)}%`);
console.log(` Total time: ${totalTime}ms`);
console.log(` Time per connection: ${(totalTime / connectionCount).toFixed(1)}ms`);
// Close clients
await Promise.all(clients.map(client => {
if (client.close) {
return client.close();
}
return Promise.resolve();
}));
// DNS caching should reduce lookups
expect(dnsLookupCount).toBeLessThan(connectionCount);
expect(cacheHitRate).toBeGreaterThan(50); // At least 50% cache hit rate
await testServer.server.close();
})();
// Scenario 2: Connection pool caching
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing connection pool caching`);
let connectionCount = 0;
let connectionReuse = 0;
const connectionPool = new Map<string, { connection: any; lastUsed: number; messageCount: number }>();
const testServer = await createTestServer({
onConnection: async (socket) => {
connectionCount++;
const connId = connectionCount;
console.log(` [Server] New connection ${connId} created`);
socket.write('220 pool-cache.example.com ESMTP\r\n');
let messageCount = 0;
socket.on('close', () => {
console.log(` [Server] Connection ${connId} closed after ${messageCount} messages`);
});
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-pool-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
messageCount++;
socket.write(`250 OK: Message ${messageCount} on connection ${connId}\r\n`);
} else if (command === 'RSET') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Mock connection pool management
const getPooledConnection = (key: string) => {
const cached = connectionPool.get(key);
const now = Date.now();
if (cached && (now - cached.lastUsed) < 60000) { // 1 minute idle timeout
connectionReuse++;
cached.lastUsed = now;
cached.messageCount++;
console.log(` [Pool] Reusing connection for ${key} (reuse #${connectionReuse})`);
return cached.connection;
}
console.log(` [Pool] Creating new connection for ${key}`);
const newConnection = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 5,
maxMessages: 10
});
connectionPool.set(key, {
connection: newConnection,
lastUsed: now,
messageCount: 0
});
return newConnection;
};
// Test connection reuse with same destination
const destinations = [
'example.com',
'example.com', // Same as first (should reuse)
'example.com', // Same as first (should reuse)
'another.com',
'example.com', // Back to first (should reuse)
'another.com' // Same as fourth (should reuse)
];
console.log(` Sending emails to test connection pool caching...`);
for (let i = 0; i < destinations.length; i++) {
const destination = destinations[i];
const poolKey = destination;
const client = getPooledConnection(poolKey);
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@${destination}`],
subject: `Pool cache test ${i + 1}`,
text: `Testing connection pool caching - destination ${destination}`
});
await client.sendMail(email);
}
// Close all pooled connections
for (const [key, pooled] of connectionPool) {
if (pooled.connection.close) {
await pooled.connection.close();
}
}
const uniqueDestinations = new Set(destinations).size;
const poolEfficiency = (connectionReuse / destinations.length) * 100;
console.log(` Total emails sent: ${destinations.length}`);
console.log(` Unique destinations: ${uniqueDestinations}`);
console.log(` New connections: ${connectionCount}`);
console.log(` Connection reuses: ${connectionReuse}`);
console.log(` Pool efficiency: ${poolEfficiency.toFixed(1)}%`);
// Connection pool should reuse connections efficiently
expect(connectionCount).toBeLessThanOrEqual(uniqueDestinations + 1);
expect(connectionReuse).toBeGreaterThan(0);
expect(poolEfficiency).toBeGreaterThan(30); // At least 30% reuse
await testServer.server.close();
})();
// Scenario 3: Template and content caching
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing template and content caching`);
let templateCompilations = 0;
let cacheHits = 0;
const templateCache = new Map<string, { compiled: string; timestamp: number; uses: number }>();
const testServer = await createTestServer({
onConnection: async (socket) => {
socket.write('220 template-cache.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-template-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Mock template compilation and caching
const compileTemplate = (template: string, data: any) => {
const cacheKey = template;
const cached = templateCache.get(cacheKey);
const now = Date.now();
if (cached && (now - cached.timestamp) < 3600000) { // 1 hour cache
cacheHits++;
cached.uses++;
console.log(` [Template] Cache hit for template (use #${cached.uses})`);
return cached.compiled.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
}
templateCompilations++;
console.log(` [Template] Compiling template (compilation #${templateCompilations})`);
// Simulate template compilation overhead
const compiled = template.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
templateCache.set(cacheKey, {
compiled: template, // Store template for reuse
timestamp: now,
uses: 1
});
return compiled;
};
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test template caching with repeated templates
const templates = [
{
id: 'welcome',
subject: 'Welcome {{name}}!',
text: 'Hello {{name}}, welcome to our service!'
},
{
id: 'notification',
subject: 'Notification for {{name}}',
text: 'Dear {{name}}, you have a new notification.'
},
{
id: 'welcome', // Repeat of first template
subject: 'Welcome {{name}}!',
text: 'Hello {{name}}, welcome to our service!'
}
];
const users = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Charlie', email: 'charlie@example.com' },
{ name: 'Diana', email: 'diana@example.com' }
];
console.log(' Sending templated emails to test content caching...');
const startTime = Date.now();
for (const user of users) {
for (const template of templates) {
const compiledSubject = compileTemplate(template.subject, user);
const compiledText = compileTemplate(template.text, user);
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [user.email],
subject: compiledSubject,
text: compiledText
});
await smtpClient.sendMail(email);
}
}
const totalTime = Date.now() - startTime;
const totalTemplateUses = users.length * templates.length;
const uniqueTemplates = new Set(templates.map(t => t.id)).size;
const cacheEfficiency = (cacheHits / (templateCompilations + cacheHits)) * 100;
console.log(` Total template uses: ${totalTemplateUses}`);
console.log(` Unique templates: ${uniqueTemplates}`);
console.log(` Template compilations: ${templateCompilations}`);
console.log(` Cache hits: ${cacheHits}`);
console.log(` Cache efficiency: ${cacheEfficiency.toFixed(1)}%`);
console.log(` Average time per email: ${(totalTime / totalTemplateUses).toFixed(1)}ms`);
// Template caching should reduce compilation overhead
expect(templateCompilations).toBeLessThan(totalTemplateUses);
expect(cacheHits).toBeGreaterThan(0);
expect(cacheEfficiency).toBeGreaterThan(50); // At least 50% cache efficiency
await testServer.server.close();
})();
// Scenario 4: Message header caching
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing message header caching`);
let headerGenerations = 0;
let headerCacheHits = 0;
const headerCache = new Map<string, { headers: any; timestamp: number }>();
const testServer = await createTestServer({
onConnection: async (socket) => {
socket.write('220 header-cache.example.com ESMTP\r\n');
let messageCount = 0;
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-header-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
messageCount++;
socket.write(`250 OK: Message ${messageCount} with cached headers\r\n`);
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Mock header generation and caching
const generateHeaders = (from: string, subject: string, messageType: string) => {
const cacheKey = `${from}-${messageType}`;
const cached = headerCache.get(cacheKey);
const now = Date.now();
if (cached && (now - cached.timestamp) < 1800000) { // 30 minute cache
headerCacheHits++;
console.log(` [Headers] Cache hit for ${messageType} headers`);
return {
...cached.headers,
Subject: subject, // Subject is dynamic
Date: new Date().toISOString(),
'Message-ID': `<${Date.now()}-${Math.random()}@example.com>`
};
}
headerGenerations++;
console.log(` [Headers] Generating ${messageType} headers (generation #${headerGenerations})`);
// Simulate header generation overhead
const headers = {
From: from,
Subject: subject,
Date: new Date().toISOString(),
'Message-ID': `<${Date.now()}-${Math.random()}@example.com>`,
'X-Mailer': 'Test Mailer 1.0',
'MIME-Version': '1.0',
'Content-Type': messageType === 'html' ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8'
};
// Cache the static parts
const cacheableHeaders = {
From: from,
'X-Mailer': 'Test Mailer 1.0',
'MIME-Version': '1.0',
'Content-Type': headers['Content-Type']
};
headerCache.set(cacheKey, {
headers: cacheableHeaders,
timestamp: now
});
return headers;
};
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test header caching with similar message types
const messageTypes = ['text', 'html', 'text', 'html', 'text']; // Repeated types
const sender = 'sender@example.com';
console.log(' Sending emails to test header caching...');
for (let i = 0; i < messageTypes.length; i++) {
const messageType = messageTypes[i];
const headers = generateHeaders(sender, `Test ${i + 1}`, messageType);
const email = new plugins.smartmail.Email({
from: headers.From,
to: [`recipient${i + 1}@example.com`],
subject: headers.Subject,
text: messageType === 'text' ? 'Plain text message' : undefined,
html: messageType === 'html' ? '<p>HTML message</p>' : undefined,
headers: {
'X-Mailer': headers['X-Mailer'],
'Message-ID': headers['Message-ID']
}
});
await smtpClient.sendMail(email);
}
const uniqueMessageTypes = new Set(messageTypes).size;
const headerCacheEfficiency = (headerCacheHits / (headerGenerations + headerCacheHits)) * 100;
console.log(` Messages sent: ${messageTypes.length}`);
console.log(` Unique message types: ${uniqueMessageTypes}`);
console.log(` Header generations: ${headerGenerations}`);
console.log(` Header cache hits: ${headerCacheHits}`);
console.log(` Header cache efficiency: ${headerCacheEfficiency.toFixed(1)}%`);
// Header caching should reduce generation overhead
expect(headerGenerations).toBeLessThan(messageTypes.length);
expect(headerCacheHits).toBeGreaterThan(0);
await testServer.server.close();
})();
// Scenario 5: Attachment processing caching
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing attachment processing caching`);
let attachmentProcessing = 0;
let attachmentCacheHits = 0;
const attachmentCache = new Map<string, { processed: string; timestamp: number; size: number }>();
const testServer = await createTestServer({
onConnection: async (socket) => {
socket.write('220 attachment-cache.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-attachment-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Mock attachment processing with caching
const processAttachment = (filename: string, content: Buffer) => {
const contentHash = require('crypto').createHash('md5').update(content).digest('hex');
const cacheKey = `${filename}-${contentHash}`;
const cached = attachmentCache.get(cacheKey);
const now = Date.now();
if (cached && (now - cached.timestamp) < 7200000) { // 2 hour cache
attachmentCacheHits++;
console.log(` [Attachment] Cache hit for ${filename}`);
return cached.processed;
}
attachmentProcessing++;
console.log(` [Attachment] Processing ${filename} (processing #${attachmentProcessing})`);
// Simulate attachment processing (base64 encoding)
const processed = content.toString('base64');
attachmentCache.set(cacheKey, {
processed,
timestamp: now,
size: content.length
});
return processed;
};
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Create reusable attachment content
const commonAttachment = Buffer.from('This is a common attachment used in multiple emails.');
const uniqueAttachment = Buffer.from('This is a unique attachment.');
const emails = [
{
subject: 'Email 1 with common attachment',
attachments: [{ filename: 'common.txt', content: commonAttachment }]
},
{
subject: 'Email 2 with unique attachment',
attachments: [{ filename: 'unique.txt', content: uniqueAttachment }]
},
{
subject: 'Email 3 with common attachment again',
attachments: [{ filename: 'common.txt', content: commonAttachment }] // Same as first
},
{
subject: 'Email 4 with both attachments',
attachments: [
{ filename: 'common.txt', content: commonAttachment },
{ filename: 'unique.txt', content: uniqueAttachment }
]
}
];
console.log(' Sending emails with attachments to test caching...');
for (let i = 0; i < emails.length; i++) {
const emailData = emails[i];
// Process attachments (with caching)
const processedAttachments = emailData.attachments.map(att => ({
filename: att.filename,
content: processAttachment(att.filename, att.content)
}));
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: emailData.subject,
text: 'Email with attachments for caching test',
attachments: processedAttachments.map(att => ({
filename: att.filename,
content: att.content,
encoding: 'base64' as const
}))
});
await smtpClient.sendMail(email);
}
const totalAttachments = emails.reduce((sum, email) => sum + email.attachments.length, 0);
const attachmentCacheEfficiency = (attachmentCacheHits / (attachmentProcessing + attachmentCacheHits)) * 100;
console.log(` Total attachments sent: ${totalAttachments}`);
console.log(` Attachment processing operations: ${attachmentProcessing}`);
console.log(` Attachment cache hits: ${attachmentCacheHits}`);
console.log(` Attachment cache efficiency: ${attachmentCacheEfficiency.toFixed(1)}%`);
// Attachment caching should reduce processing overhead
expect(attachmentProcessing).toBeLessThan(totalAttachments);
expect(attachmentCacheHits).toBeGreaterThan(0);
expect(attachmentCacheEfficiency).toBeGreaterThan(30); // At least 30% cache efficiency
await testServer.server.close();
})();
// Scenario 6: Overall caching performance impact
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing overall caching performance impact`);
const testServer = await createTestServer({
onConnection: async (socket) => {
socket.write('220 performance-cache.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-performance-cache.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Test performance with caching enabled vs disabled
const emailCount = 20;
// Simulate no caching (always process)
console.log(' Testing performance without caching...');
const noCacheStart = Date.now();
let noCacheOperations = 0;
const noCacheClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
for (let i = 0; i < emailCount; i++) {
// Simulate processing overhead for each email
noCacheOperations += 3; // DNS lookup, header generation, template processing
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`nocache${i + 1}@example.com`],
subject: `No cache test ${i + 1}`,
text: `Testing performance without caching - email ${i + 1}`
});
await noCacheClient.sendMail(email);
}
const noCacheTime = Date.now() - noCacheStart;
// Simulate with caching (reduced processing)
console.log(' Testing performance with caching...');
const cacheStart = Date.now();
let cacheOperations = 5; // Initial setup, then reuse
const cacheClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2
});
for (let i = 0; i < emailCount; i++) {
// Simulate reduced operations due to caching
if (i < 5) {
cacheOperations += 1; // Some cache misses initially
}
// Most operations are cache hits (no additional operations)
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`cache${i + 1}@example.com`],
subject: `Cache test ${i + 1}`,
text: `Testing performance with caching - email ${i + 1}`
});
await cacheClient.sendMail(email);
}
await cacheClient.close();
const cacheTime = Date.now() - cacheStart;
// Calculate performance improvements
const timeImprovement = ((noCacheTime - cacheTime) / noCacheTime) * 100;
const operationReduction = ((noCacheOperations - cacheOperations) / noCacheOperations) * 100;
const throughputImprovement = (emailCount / cacheTime) / (emailCount / noCacheTime);
console.log(` Performance comparison (${emailCount} emails):`);
console.log(` Without caching: ${noCacheTime}ms, ${noCacheOperations} operations`);
console.log(` With caching: ${cacheTime}ms, ${cacheOperations} operations`);
console.log(` Time improvement: ${timeImprovement.toFixed(1)}%`);
console.log(` Operation reduction: ${operationReduction.toFixed(1)}%`);
console.log(` Throughput improvement: ${throughputImprovement.toFixed(2)}x`);
// Caching should improve performance
expect(cacheTime).toBeLessThan(noCacheTime);
expect(cacheOperations).toBeLessThan(noCacheOperations);
expect(timeImprovement).toBeGreaterThan(10); // At least 10% improvement
expect(throughputImprovement).toBeGreaterThan(1.1); // At least 10% better throughput
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} caching strategy scenarios tested ✓`);
});