This commit is contained in:
2025-05-24 14:39:48 +00:00
parent dc5c0b2584
commit 6e19e30f87
80 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,183 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
// import { createTestSmtpClient, sendConcurrentEmails, measureClientThroughput } from '../../helpers/smtp.client.js';
import { connectToSmtp, sendSmtpCommand, waitForGreeting, createMimeMessage, closeSmtpConnection } from '../../helpers/utils.js';
let testServer: ITestServer;
tap.test('setup - start SMTP server for performance testing', async () => {
testServer = await startTestServer({
port: 2531,
hostname: 'localhost',
maxConnections: 1000,
size: 50 * 1024 * 1024 // 50MB for performance testing
});
expect(testServer).toBeInstanceOf(Object);
});
// TODO: Enable these tests when the helper functions are implemented
/*
tap.test('PERF-01: Throughput Testing - measure emails per second', async () => {
const client = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
maxConnections: 10
});
try {
// Warm up the connection pool
console.log('🔥 Warming up connection pool...');
await sendConcurrentEmails(client, 5);
// Measure throughput for 10 seconds
console.log('📊 Measuring throughput for 10 seconds...');
const startTime = Date.now();
const testDuration = 10000; // 10 seconds
const result = await measureClientThroughput(client, testDuration, {
from: 'perf-test@example.com',
to: 'recipient@example.com',
subject: 'Performance Test Email',
text: 'This is a performance test email to measure throughput.'
});
const actualDuration = (Date.now() - startTime) / 1000;
console.log('📈 Throughput Test Results:');
console.log(` Total emails sent: ${result.totalSent}`);
console.log(` Successful: ${result.successCount}`);
console.log(` Failed: ${result.errorCount}`);
console.log(` Duration: ${actualDuration.toFixed(2)}s`);
console.log(` Throughput: ${result.throughput.toFixed(2)} emails/second`);
// Performance expectations
expect(result.throughput).toBeGreaterThan(10); // At least 10 emails/second
expect(result.errorCount).toBeLessThan(result.totalSent * 0.05); // Less than 5% errors
console.log('✅ Throughput test passed');
} finally {
if (client.close) {
await client.close();
}
}
});
tap.test('PERF-01: Burst throughput - handle sudden load spikes', async () => {
const client = createTestSmtpClient({
host: testServer.hostname,
port: testServer.port,
maxConnections: 20
});
try {
// Send burst of emails
const burstSize = 100;
console.log(`💥 Sending burst of ${burstSize} emails...`);
const startTime = Date.now();
const results = await sendConcurrentEmails(client, burstSize, {
from: 'burst-test@example.com',
to: 'recipient@example.com',
subject: 'Burst Test Email',
text: 'Testing burst performance.'
});
const duration = Date.now() - startTime;
const successCount = results.filter(r => r && !r.rejected).length;
const throughput = (successCount / duration) * 1000;
console.log(`✅ Burst completed in ${duration}ms`);
console.log(` Success rate: ${successCount}/${burstSize} (${(successCount/burstSize*100).toFixed(1)}%)`);
console.log(` Burst throughput: ${throughput.toFixed(2)} emails/second`);
expect(successCount).toBeGreaterThan(burstSize * 0.95); // 95% success rate
} finally {
if (client.close) {
await client.close();
}
}
});
*/
tap.test('PERF-01: Large message throughput - measure with varying sizes', async () => {
const messageSizes = [
{ size: 1024, label: '1KB' },
{ size: 100 * 1024, label: '100KB' },
{ size: 1024 * 1024, label: '1MB' },
{ size: 5 * 1024 * 1024, label: '5MB' }
];
for (const { size, label } of messageSizes) {
console.log(`\n📧 Testing throughput with ${label} messages...`);
const socket = await connectToSmtp(testServer.hostname, testServer.port);
try {
await waitForGreeting(socket);
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
// Send a few messages of this size
const messageCount = 5;
const timings: number[] = [];
for (let i = 0; i < messageCount; i++) {
const startTime = Date.now();
await sendSmtpCommand(socket, 'MAIL FROM:<size-test@example.com>', '250');
await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
await sendSmtpCommand(socket, 'DATA', '354');
// Create message with padding to reach target size
const padding = 'X'.repeat(Math.max(0, size - 200)); // Account for headers
const emailContent = createMimeMessage({
from: 'size-test@example.com',
to: 'recipient@example.com',
subject: `${label} Performance Test`,
text: padding
});
socket.write(emailContent);
socket.write('\r\n.\r\n');
// Wait for acceptance
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout')), 30000);
const onData = (data: Buffer) => {
if (data.toString().includes('250')) {
clearTimeout(timeout);
socket.removeListener('data', onData);
resolve();
}
};
socket.on('data', onData);
});
const duration = Date.now() - startTime;
timings.push(duration);
// Reset for next message
await sendSmtpCommand(socket, 'RSET', '250');
}
const avgTime = timings.reduce((a, b) => a + b, 0) / timings.length;
const throughputMBps = (size / 1024 / 1024) / (avgTime / 1000);
console.log(` Average time: ${avgTime.toFixed(0)}ms`);
console.log(` Throughput: ${throughputMBps.toFixed(2)} MB/s`);
} finally {
await closeSmtpConnection(socket);
}
}
console.log('\n✅ Large message throughput test completed');
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
console.log('✅ Test server stopped');
});
tap.start();

View File

@ -0,0 +1,388 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-02: Concurrency testing - Multiple simultaneous connections', async (tools) => {
const done = tools.defer();
const concurrentCount = 20;
const connectionResults: Array<{
connectionId: number;
success: boolean;
duration: number;
error?: string;
}> = [];
const createConcurrentConnection = (connectionId: number): Promise<void> => {
return new Promise((resolve) => {
const startTime = Date.now();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 10000
});
let state = 'connecting';
let receivedData = '';
const timeoutHandle = setTimeout(() => {
socket.destroy();
connectionResults.push({
connectionId,
success: false,
duration: Date.now() - startTime,
error: 'Connection timeout'
});
resolve();
}, 10000);
socket.on('connect', () => {
state = 'connected';
});
socket.on('data', (chunk) => {
receivedData += chunk.toString();
const lines = receivedData.split('\r\n');
for (const line of lines) {
if (!line.trim()) continue;
if (state === 'connected' && line.startsWith('220')) {
state = 'ehlo';
socket.write(`EHLO testhost-${connectionId}\r\n`);
} else if (state === 'ehlo' && line.includes('250 ') && !line.includes('250-')) {
// Final 250 response received
state = 'quit';
socket.write('QUIT\r\n');
} else if (state === 'quit' && line.startsWith('221')) {
clearTimeout(timeoutHandle);
socket.end();
connectionResults.push({
connectionId,
success: true,
duration: Date.now() - startTime
});
resolve();
}
}
});
socket.on('error', (error) => {
clearTimeout(timeoutHandle);
connectionResults.push({
connectionId,
success: false,
duration: Date.now() - startTime,
error: error.message
});
resolve();
});
socket.on('close', () => {
clearTimeout(timeoutHandle);
if (!connectionResults.find(r => r.connectionId === connectionId)) {
connectionResults.push({
connectionId,
success: false,
duration: Date.now() - startTime,
error: 'Connection closed unexpectedly'
});
}
resolve();
});
});
};
try {
// Create all concurrent connections
const promises: Promise<void>[] = [];
console.log(`Creating ${concurrentCount} concurrent connections...`);
for (let i = 0; i < concurrentCount; i++) {
promises.push(createConcurrentConnection(i));
// Small stagger to avoid overwhelming the system
if (i % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// Wait for all connections to complete
await Promise.all(promises);
// Analyze results
const successful = connectionResults.filter(r => r.success).length;
const failed = connectionResults.filter(r => !r.success).length;
const successRate = successful / concurrentCount;
const avgDuration = connectionResults
.filter(r => r.success)
.reduce((sum, r) => sum + r.duration, 0) / successful || 0;
console.log(`\nConcurrency Test Results:`);
console.log(`Total connections: ${concurrentCount}`);
console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`);
console.log(`Failed: ${failed}`);
console.log(`Average duration: ${avgDuration.toFixed(0)}ms`);
if (failed > 0) {
const errors = connectionResults
.filter(r => !r.success)
.map(r => r.error)
.filter((v, i, a) => a.indexOf(v) === i); // unique errors
console.log(`Unique errors: ${errors.join(', ')}`);
}
// Success if at least 80% of connections succeed
expect(successRate).toBeGreaterThanOrEqual(0.8);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('PERF-02: Concurrency testing - Concurrent transactions', async (tools) => {
const done = tools.defer();
const transactionCount = 10;
const transactionResults: Array<{
transactionId: number;
success: boolean;
duration: number;
error?: string;
}> = [];
const performConcurrentTransaction = (transactionId: number): Promise<void> => {
return new Promise((resolve) => {
const startTime = Date.now();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 15000
});
let state = 'connecting';
const timeoutHandle = setTimeout(() => {
socket.destroy();
transactionResults.push({
transactionId,
success: false,
duration: Date.now() - startTime,
error: 'Transaction timeout'
});
resolve();
}, 15000);
const processResponse = async () => {
try {
// Read greeting
await new Promise<void>((res) => {
let greeting = '';
const handleGreeting = (chunk: Buffer) => {
greeting += chunk.toString();
if (greeting.includes('220') && greeting.includes('\r\n')) {
socket.removeListener('data', handleGreeting);
res();
}
};
socket.on('data', handleGreeting);
});
// Send EHLO
socket.write(`EHLO testhost-tx-${transactionId}\r\n`);
await new Promise<void>((res) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
// Look for the end of EHLO response (250 without dash)
if (data.includes('250 ')) {
socket.removeListener('data', handleData);
res();
}
};
socket.on('data', handleData);
});
// Complete email transaction
socket.write(`MAIL FROM:<sender${transactionId}@example.com>\r\n`);
await new Promise<void>((res, rej) => {
let mailResponse = '';
const handleMailResponse = (chunk: Buffer) => {
mailResponse += chunk.toString();
if (mailResponse.includes('\r\n')) {
socket.removeListener('data', handleMailResponse);
if (!mailResponse.includes('250')) {
rej(new Error('MAIL FROM failed'));
} else {
res();
}
}
};
socket.on('data', handleMailResponse);
});
socket.write(`RCPT TO:<recipient${transactionId}@example.com>\r\n`);
await new Promise<void>((res, rej) => {
let rcptResponse = '';
const handleRcptResponse = (chunk: Buffer) => {
rcptResponse += chunk.toString();
if (rcptResponse.includes('\r\n')) {
socket.removeListener('data', handleRcptResponse);
if (!rcptResponse.includes('250')) {
rej(new Error('RCPT TO failed'));
} else {
res();
}
}
};
socket.on('data', handleRcptResponse);
});
socket.write('DATA\r\n');
await new Promise<void>((res, rej) => {
let dataResponse = '';
const handleDataResponse = (chunk: Buffer) => {
dataResponse += chunk.toString();
if (dataResponse.includes('\r\n')) {
socket.removeListener('data', handleDataResponse);
if (!dataResponse.includes('354')) {
rej(new Error('DATA command failed'));
} else {
res();
}
}
};
socket.on('data', handleDataResponse);
});
// Send email content
const emailContent = [
`From: sender${transactionId}@example.com`,
`To: recipient${transactionId}@example.com`,
`Subject: Concurrent test ${transactionId}`,
'',
`This is concurrent test message ${transactionId}`,
'.',
''
].join('\r\n');
socket.write(emailContent);
await new Promise<void>((res, rej) => {
let submitResponse = '';
const handleSubmitResponse = (chunk: Buffer) => {
submitResponse += chunk.toString();
if (submitResponse.includes('\r\n') && submitResponse.includes('250')) {
socket.removeListener('data', handleSubmitResponse);
res();
} else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) {
socket.removeListener('data', handleSubmitResponse);
rej(new Error('Message submission failed'));
}
};
socket.on('data', handleSubmitResponse);
});
socket.write('QUIT\r\n');
await new Promise<void>((res) => {
socket.once('data', () => res());
});
clearTimeout(timeoutHandle);
socket.end();
transactionResults.push({
transactionId,
success: true,
duration: Date.now() - startTime
});
resolve();
} catch (error) {
clearTimeout(timeoutHandle);
socket.end();
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.log(`Transaction ${transactionId} failed: ${errorMsg}`);
transactionResults.push({
transactionId,
success: false,
duration: Date.now() - startTime,
error: errorMsg
});
resolve();
}
};
socket.on('connect', () => {
state = 'connected';
processResponse();
});
socket.on('error', (error) => {
clearTimeout(timeoutHandle);
if (!transactionResults.find(r => r.transactionId === transactionId)) {
transactionResults.push({
transactionId,
success: false,
duration: Date.now() - startTime,
error: error.message
});
}
resolve();
});
});
};
try {
// Create concurrent transactions
const promises: Promise<void>[] = [];
console.log(`\nStarting ${transactionCount} concurrent email transactions...`);
for (let i = 0; i < transactionCount; i++) {
promises.push(performConcurrentTransaction(i));
// Small stagger
await new Promise(resolve => setTimeout(resolve, 50));
}
// Wait for all transactions
await Promise.all(promises);
// Analyze results
const successful = transactionResults.filter(r => r.success).length;
const failed = transactionResults.filter(r => !r.success).length;
const successRate = successful / transactionCount;
const avgDuration = transactionResults
.filter(r => r.success)
.reduce((sum, r) => sum + r.duration, 0) / successful || 0;
console.log(`\nConcurrent Transaction Results:`);
console.log(`Total transactions: ${transactionCount}`);
console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`);
console.log(`Failed: ${failed}`);
console.log(`Average duration: ${avgDuration.toFixed(0)}ms`);
// Success if at least 80% of transactions complete
expect(successRate).toBeGreaterThanOrEqual(0.8);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,245 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-03: CPU utilization - Load test', async (tools) => {
const done = tools.defer();
const monitoringDuration = 3000; // 3 seconds (reduced from 5)
const connectionCount = 5; // Reduced from 10
const connections: net.Socket[] = [];
// Add timeout to prevent hanging
const testTimeout = setTimeout(() => {
console.log('CPU test timeout reached, cleaning up...');
for (const socket of connections) {
if (!socket.destroyed) socket.destroy();
}
done.resolve();
}, 30000); // 30 second timeout
try {
// Record initial CPU usage
const initialCpuUsage = process.cpuUsage();
const startTime = Date.now();
// Create multiple connections and send emails
console.log(`Creating ${connectionCount} connections for CPU load test...`);
for (let i = 0; i < connectionCount; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
connections.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', () => {
resolve();
});
socket.once('error', reject);
});
// Process greeting
await new Promise<void>((resolve) => {
let greeting = '';
const handleGreeting = (chunk: Buffer) => {
greeting += chunk.toString();
if (greeting.includes('220') && greeting.includes('\r\n')) {
socket.removeListener('data', handleGreeting);
resolve();
}
};
socket.on('data', handleGreeting);
});
// Send EHLO
socket.write(`EHLO testhost-cpu-${i}\r\n`);
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Keep connection active, don't send full transaction to avoid timeout
}
// Keep connections active during monitoring period
console.log(`Monitoring CPU usage for ${monitoringDuration}ms...`);
// Send periodic NOOP commands to keep connections active
const noopInterval = setInterval(() => {
connections.forEach((socket, idx) => {
if (socket.writable) {
socket.write('NOOP\r\n');
}
});
}, 1000);
await new Promise(resolve => setTimeout(resolve, monitoringDuration));
clearInterval(noopInterval);
// Calculate CPU usage
const finalCpuUsage = process.cpuUsage(initialCpuUsage);
const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000;
const elapsedTime = Date.now() - startTime;
const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100;
console.log(`\nCPU Utilization Results:`);
console.log(`Total CPU time: ${totalCpuTimeMs.toFixed(0)}ms`);
console.log(`Elapsed time: ${elapsedTime}ms`);
console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`);
console.log(`User CPU: ${(finalCpuUsage.user / 1000).toFixed(0)}ms`);
console.log(`System CPU: ${(finalCpuUsage.system / 1000).toFixed(0)}ms`);
// Clean up connections
for (const socket of connections) {
if (socket.writable) {
socket.write('QUIT\r\n');
socket.end();
}
}
// Test passes if CPU usage is reasonable (less than 80%)
expect(cpuUtilizationPercent).toBeLessThan(80);
clearTimeout(testTimeout);
done.resolve();
} catch (error) {
// Clean up on error
connections.forEach(socket => socket.destroy());
clearTimeout(testTimeout);
done.reject(error);
}
});
tap.test('PERF-03: CPU utilization - Stress test', async (tools) => {
const done = tools.defer();
const testDuration = 2000; // 2 seconds (reduced from 3)
let requestCount = 0;
// Add timeout to prevent hanging
const testTimeout = setTimeout(() => {
console.log('Stress test timeout reached, completing...');
done.resolve();
}, 15000); // 15 second timeout
try {
const initialCpuUsage = process.cpuUsage();
const startTime = Date.now();
console.log(`\nRunning CPU stress test for ${testDuration}ms...`);
// Create a single connection for rapid requests
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
let greeting = '';
const handleGreeting = (chunk: Buffer) => {
greeting += chunk.toString();
if (greeting.includes('220') && greeting.includes('\r\n')) {
socket.removeListener('data', handleGreeting);
resolve();
}
};
socket.on('data', handleGreeting);
});
// Send EHLO
socket.write('EHLO stresstest\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Rapid command loop
const endTime = Date.now() + testDuration;
const commands = ['NOOP', 'RSET', 'VRFY test@example.com', 'HELP'];
let commandIndex = 0;
while (Date.now() < endTime) {
const command = commands[commandIndex % commands.length];
socket.write(`${command}\r\n`);
await new Promise<void>((resolve) => {
socket.once('data', () => {
requestCount++;
resolve();
});
});
commandIndex++;
// Small delay to avoid overwhelming
if (requestCount % 20 === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// Calculate final CPU usage
const finalCpuUsage = process.cpuUsage(initialCpuUsage);
const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000;
const elapsedTime = Date.now() - startTime;
const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100;
const requestsPerSecond = (requestCount / elapsedTime) * 1000;
console.log(`\nStress Test Results:`);
console.log(`Requests processed: ${requestCount}`);
console.log(`Requests per second: ${requestsPerSecond.toFixed(1)}`);
console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`);
console.log(`CPU time per request: ${(totalCpuTimeMs / requestCount).toFixed(2)}ms`);
socket.write('QUIT\r\n');
socket.end();
// Test passes if CPU usage per request is reasonable
const cpuPerRequest = totalCpuTimeMs / requestCount;
expect(cpuPerRequest).toBeLessThan(10); // Less than 10ms CPU per request
clearTimeout(testTimeout);
done.resolve();
} catch (error) {
clearTimeout(testTimeout);
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,238 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
// Helper function to wait for SMTP response
const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise<string> => {
return new Promise((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeListener('data', handler);
reject(new Error(`Timeout waiting for ${expectedCode} response`));
}, timeout);
const handler = (data: Buffer) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
// Check if we have a complete response
for (const line of lines) {
if (line.startsWith(expectedCode + ' ')) {
clearTimeout(timer);
socket.removeListener('data', handler);
resolve(buffer);
return;
}
}
};
socket.on('data', handler);
});
};
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-04: Memory usage - Connection memory test', async (tools) => {
const done = tools.defer();
const connectionCount = 10; // Reduced from 20 to make test faster
const connections: net.Socket[] = [];
try {
// Force garbage collection if available
if (global.gc) {
global.gc();
}
// Record initial memory usage
const initialMemory = process.memoryUsage();
console.log(`Initial memory usage: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`);
// Create multiple connections with large email content
console.log(`Creating ${connectionCount} connections with large emails...`);
for (let i = 0; i < connectionCount; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
connections.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write(`EHLO testhost-mem-${i}\r\n`);
await waitForResponse(socket, '250');
// Send email transaction
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
socket.write('DATA\r\n');
await waitForResponse(socket, '354');
// Send large email content
const largeContent = 'This is a large email content for memory testing. '.repeat(100);
const emailContent = [
`From: sender${i}@example.com`,
`To: recipient${i}@example.com`,
`Subject: Memory Usage Test ${i}`,
'',
largeContent,
'.',
''
].join('\r\n');
socket.write(emailContent);
await waitForResponse(socket, '250');
// Pause every 5 connections
if (i > 0 && i % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 100));
const intermediateMemory = process.memoryUsage();
console.log(`Memory after ${i} connections: ${Math.round(intermediateMemory.heapUsed / (1024 * 1024))}MB`);
}
}
// Wait to let memory stabilize
await new Promise(resolve => setTimeout(resolve, 2000));
// Record final memory usage
const finalMemory = process.memoryUsage();
const memoryIncreaseMB = (finalMemory.heapUsed - initialMemory.heapUsed) / (1024 * 1024);
const memoryPerConnectionKB = (memoryIncreaseMB * 1024) / connectionCount;
console.log(`\nMemory Usage Results:`);
console.log(`Initial heap: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Final heap: ${Math.round(finalMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`);
console.log(`Memory per connection: ${memoryPerConnectionKB.toFixed(2)}KB`);
console.log(`RSS increase: ${Math.round((finalMemory.rss - initialMemory.rss) / (1024 * 1024))}MB`);
// Clean up connections
for (const socket of connections) {
if (socket.writable) {
socket.write('QUIT\r\n');
socket.end();
}
}
// Test passes if memory increase is reasonable (less than 30MB for 10 connections)
expect(memoryIncreaseMB).toBeLessThan(30);
done.resolve();
} catch (error) {
// Clean up on error
connections.forEach(socket => socket.destroy());
done.reject(error);
}
});
tap.test('PERF-04: Memory usage - Memory leak detection', async (tools) => {
const done = tools.defer();
const iterations = 3; // Reduced from 5
const connectionsPerIteration = 3; // Reduced from 5
try {
// Force GC if available
if (global.gc) {
global.gc();
}
const initialMemory = process.memoryUsage();
const memorySnapshots: number[] = [];
console.log(`\nRunning memory leak detection (${iterations} iterations)...`);
for (let iteration = 0; iteration < iterations; iteration++) {
const sockets: net.Socket[] = [];
// Create and close connections
for (let i = 0; i < connectionsPerIteration; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Quick transaction
await waitForResponse(socket, '220');
socket.write('EHLO leaktest\r\n');
await waitForResponse(socket, '250');
socket.write('QUIT\r\n');
await waitForResponse(socket, '221');
socket.end();
sockets.push(socket);
}
// Wait for sockets to close
await new Promise(resolve => setTimeout(resolve, 500));
// Force cleanup
sockets.forEach(s => s.destroy());
// Force GC if available
if (global.gc) {
global.gc();
}
// Record memory after each iteration
const currentMemory = process.memoryUsage();
const memoryMB = currentMemory.heapUsed / (1024 * 1024);
memorySnapshots.push(memoryMB);
console.log(`Iteration ${iteration + 1}: ${memoryMB.toFixed(2)}MB`);
await new Promise(resolve => setTimeout(resolve, 500));
}
// Check for memory leak pattern
const firstSnapshot = memorySnapshots[0];
const lastSnapshot = memorySnapshots[memorySnapshots.length - 1];
const memoryGrowth = lastSnapshot - firstSnapshot;
const avgGrowthPerIteration = memoryGrowth / (iterations - 1);
console.log(`\nMemory Leak Detection Results:`);
console.log(`First snapshot: ${firstSnapshot.toFixed(2)}MB`);
console.log(`Last snapshot: ${lastSnapshot.toFixed(2)}MB`);
console.log(`Total growth: ${memoryGrowth.toFixed(2)}MB`);
console.log(`Average growth per iteration: ${avgGrowthPerIteration.toFixed(2)}MB`);
// Test passes if average growth per iteration is less than 2MB
expect(avgGrowthPerIteration).toBeLessThan(2);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,363 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-05: Connection processing time - Connection establishment', async (tools) => {
const done = tools.defer();
const testConnections = 10;
const connectionTimes: number[] = [];
try {
console.log(`Testing connection establishment time for ${testConnections} connections...`);
for (let i = 0; i < testConnections; i++) {
const connectionStart = Date.now();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', () => {
const connectionTime = Date.now() - connectionStart;
connectionTimes.push(connectionTime);
resolve();
});
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Clean close
socket.write('QUIT\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
socket.end();
// Small delay between connections
await new Promise(resolve => setTimeout(resolve, 50));
}
// Calculate statistics
const avgConnectionTime = connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length;
const minConnectionTime = Math.min(...connectionTimes);
const maxConnectionTime = Math.max(...connectionTimes);
console.log(`\nConnection Establishment Results:`);
console.log(`Average: ${avgConnectionTime.toFixed(0)}ms`);
console.log(`Min: ${minConnectionTime}ms`);
console.log(`Max: ${maxConnectionTime}ms`);
console.log(`All times: ${connectionTimes.join(', ')}ms`);
// Test passes if average connection time is less than 1000ms
expect(avgConnectionTime).toBeLessThan(1000);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('PERF-05: Connection processing time - Transaction processing', async (tools) => {
const done = tools.defer();
const testTransactions = 10;
const processingTimes: number[] = [];
const fullTransactionTimes: number[] = [];
// Add a timeout to prevent test from hanging
const testTimeout = setTimeout(() => {
console.log('Test timeout reached, moving on...');
done.resolve();
}, 30000); // 30 second timeout
try {
console.log(`\nTesting transaction processing time for ${testTransactions} transactions...`);
for (let i = 0; i < testTransactions; i++) {
const fullTransactionStart = Date.now();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
const processingStart = Date.now();
// Send EHLO
socket.write(`EHLO testhost-perf-${i}\r\n`);
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
// Look for the end of EHLO response (250 without dash)
if (data.includes('250 ')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Send MAIL FROM
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
await new Promise<void>((resolve, reject) => {
let mailResponse = '';
const handleMailResponse = (chunk: Buffer) => {
mailResponse += chunk.toString();
if (mailResponse.includes('\r\n')) {
socket.removeListener('data', handleMailResponse);
if (mailResponse.includes('250')) {
resolve();
} else {
reject(new Error(`MAIL FROM failed: ${mailResponse}`));
}
}
};
socket.on('data', handleMailResponse);
});
// Send RCPT TO
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await new Promise<void>((resolve, reject) => {
let rcptResponse = '';
const handleRcptResponse = (chunk: Buffer) => {
rcptResponse += chunk.toString();
if (rcptResponse.includes('\r\n')) {
socket.removeListener('data', handleRcptResponse);
if (rcptResponse.includes('250')) {
resolve();
} else {
reject(new Error(`RCPT TO failed: ${rcptResponse}`));
}
}
};
socket.on('data', handleRcptResponse);
});
// Send DATA
socket.write('DATA\r\n');
await new Promise<void>((resolve, reject) => {
let dataResponse = '';
const handleDataResponse = (chunk: Buffer) => {
dataResponse += chunk.toString();
if (dataResponse.includes('\r\n')) {
socket.removeListener('data', handleDataResponse);
if (dataResponse.includes('354')) {
resolve();
} else {
reject(new Error(`DATA failed: ${dataResponse}`));
}
}
};
socket.on('data', handleDataResponse);
});
// Send email content
const emailContent = [
`From: sender${i}@example.com`,
`To: recipient${i}@example.com`,
`Subject: Connection Processing Test ${i}`,
'',
'Connection processing time test.',
'.',
''
].join('\r\n');
socket.write(emailContent);
await new Promise<void>((resolve, reject) => {
let submitResponse = '';
const handleSubmitResponse = (chunk: Buffer) => {
submitResponse += chunk.toString();
if (submitResponse.includes('\r\n') && submitResponse.includes('250')) {
socket.removeListener('data', handleSubmitResponse);
resolve();
} else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) {
socket.removeListener('data', handleSubmitResponse);
reject(new Error(`Message submission failed: ${submitResponse}`));
}
};
socket.on('data', handleSubmitResponse);
});
const processingTime = Date.now() - processingStart;
processingTimes.push(processingTime);
// Send QUIT
socket.write('QUIT\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
socket.end();
const fullTransactionTime = Date.now() - fullTransactionStart;
fullTransactionTimes.push(fullTransactionTime);
// Small delay between transactions
await new Promise(resolve => setTimeout(resolve, 50));
}
// Calculate statistics
const avgProcessingTime = processingTimes.reduce((a, b) => a + b, 0) / processingTimes.length;
const minProcessingTime = Math.min(...processingTimes);
const maxProcessingTime = Math.max(...processingTimes);
const avgFullTime = fullTransactionTimes.reduce((a, b) => a + b, 0) / fullTransactionTimes.length;
console.log(`\nTransaction Processing Results:`);
console.log(`Average processing: ${avgProcessingTime.toFixed(0)}ms`);
console.log(`Min processing: ${minProcessingTime}ms`);
console.log(`Max processing: ${maxProcessingTime}ms`);
console.log(`Average full transaction: ${avgFullTime.toFixed(0)}ms`);
// Test passes if average processing time is less than 2000ms
expect(avgProcessingTime).toBeLessThan(2000);
clearTimeout(testTimeout);
done.resolve();
} catch (error) {
clearTimeout(testTimeout);
done.reject(error);
}
});
tap.test('PERF-05: Connection processing time - Command response times', async (tools) => {
const done = tools.defer();
const commandTimings: { [key: string]: number[] } = {
EHLO: [],
NOOP: []
};
// Add a timeout to prevent test from hanging
const testTimeout = setTimeout(() => {
console.log('Command timing test timeout reached, moving on...');
done.resolve();
}, 20000); // 20 second timeout
try {
console.log(`\nMeasuring individual command response times...`);
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
let greeting = '';
const handleGreeting = (chunk: Buffer) => {
greeting += chunk.toString();
if (greeting.includes('220') && greeting.includes('\r\n')) {
socket.removeListener('data', handleGreeting);
resolve();
}
};
socket.on('data', handleGreeting);
});
// Measure EHLO response times
for (let i = 0; i < 3; i++) {
const start = Date.now();
socket.write('EHLO testhost\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ')) {
socket.removeListener('data', handleData);
commandTimings.EHLO.push(Date.now() - start);
resolve();
}
};
socket.on('data', handleData);
});
}
// Measure NOOP response times
for (let i = 0; i < 3; i++) {
const start = Date.now();
socket.write('NOOP\r\n');
await new Promise<void>((resolve) => {
let noopResponse = '';
const handleNoop = (chunk: Buffer) => {
noopResponse += chunk.toString();
if (noopResponse.includes('\r\n')) {
socket.removeListener('data', handleNoop);
commandTimings.NOOP.push(Date.now() - start);
resolve();
}
};
socket.on('data', handleNoop);
});
}
// Close connection
socket.write('QUIT\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => {
socket.end();
resolve();
});
});
// Calculate and display results
console.log(`\nCommand Response Times (ms):`);
for (const [command, times] of Object.entries(commandTimings)) {
if (times.length > 0) {
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`${command}: avg=${avg.toFixed(0)}, samples=[${times.join(', ')}]`);
// All commands should respond in less than 500ms on average
expect(avg).toBeLessThan(500);
}
}
clearTimeout(testTimeout);
done.resolve();
} catch (error) {
clearTimeout(testTimeout);
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,252 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
// Helper function to wait for SMTP response
const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise<string> => {
return new Promise((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeListener('data', handler);
reject(new Error(`Timeout waiting for ${expectedCode} response`));
}, timeout);
const handler = (data: Buffer) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
// Check if we have a complete response
for (const line of lines) {
if (line.startsWith(expectedCode + ' ')) {
clearTimeout(timer);
socket.removeListener('data', handler);
resolve(buffer);
return;
}
}
};
socket.on('data', handler);
});
};
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-06: Message processing time - Various message sizes', async (tools) => {
const done = tools.defer();
const messageSizes = [1000, 5000, 10000, 25000, 50000]; // bytes
const messageProcessingTimes: number[] = [];
const processingRates: number[] = [];
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO testhost\r\n');
await waitForResponse(socket, '250');
console.log('Testing message processing times for various sizes...\n');
for (let i = 0; i < messageSizes.length; i++) {
const messageSize = messageSizes[i];
const messageContent = 'A'.repeat(messageSize);
const messageStart = Date.now();
// Send MAIL FROM
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
// Send RCPT TO
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
// Send DATA
socket.write('DATA\r\n');
await waitForResponse(socket, '354');
// Send email content
const emailContent = [
`From: sender${i}@example.com`,
`To: recipient${i}@example.com`,
`Subject: Message Processing Test ${i} (${messageSize} bytes)`,
'',
messageContent,
'.',
''
].join('\r\n');
socket.write(emailContent);
await waitForResponse(socket, '250');
const messageProcessingTime = Date.now() - messageStart;
messageProcessingTimes.push(messageProcessingTime);
const processingRateKBps = (messageSize / 1024) / (messageProcessingTime / 1000);
processingRates.push(processingRateKBps);
console.log(`${messageSize} bytes: ${messageProcessingTime}ms (${processingRateKBps.toFixed(1)} KB/s)`);
// Send RSET
socket.write('RSET\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Small delay between tests
await new Promise(resolve => setTimeout(resolve, 100));
}
// Calculate statistics
const avgProcessingTime = messageProcessingTimes.reduce((a, b) => a + b, 0) / messageProcessingTimes.length;
const avgProcessingRate = processingRates.reduce((a, b) => a + b, 0) / processingRates.length;
const minProcessingTime = Math.min(...messageProcessingTimes);
const maxProcessingTime = Math.max(...messageProcessingTimes);
console.log(`\nMessage Processing Results:`);
console.log(`Average processing time: ${avgProcessingTime.toFixed(0)}ms`);
console.log(`Min/Max processing time: ${minProcessingTime}ms / ${maxProcessingTime}ms`);
console.log(`Average processing rate: ${avgProcessingRate.toFixed(1)} KB/s`);
socket.write('QUIT\r\n');
socket.end();
// Test passes if average processing time is less than 3000ms and rate > 10KB/s
expect(avgProcessingTime).toBeLessThan(3000);
expect(avgProcessingRate).toBeGreaterThan(10);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('PERF-06: Message processing time - Large message handling', async (tools) => {
const done = tools.defer();
const largeSizes = [100000, 250000, 500000]; // 100KB, 250KB, 500KB
const results: Array<{ size: number; time: number; rate: number }> = [];
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 60000 // Longer timeout for large messages
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write('EHLO testhost-large\r\n');
await waitForResponse(socket, '250');
console.log('\nTesting large message processing...\n');
for (let i = 0; i < largeSizes.length; i++) {
const messageSize = largeSizes[i];
const messageStart = Date.now();
// Send MAIL FROM
socket.write(`MAIL FROM:<largesender${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
// Send RCPT TO
socket.write(`RCPT TO:<largerecipient${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
// Send DATA
socket.write('DATA\r\n');
await waitForResponse(socket, '354');
// Send large email content in chunks to avoid buffer issues
socket.write(`From: largesender${i}@example.com\r\n`);
socket.write(`To: largerecipient${i}@example.com\r\n`);
socket.write(`Subject: Large Message Test ${i} (${messageSize} bytes)\r\n\r\n`);
// Send content in 10KB chunks
const chunkSize = 10000;
let remaining = messageSize;
while (remaining > 0) {
const currentChunk = Math.min(remaining, chunkSize);
socket.write('B'.repeat(currentChunk));
remaining -= currentChunk;
// Small delay to avoid overwhelming buffers
if (remaining > 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
socket.write('\r\n.\r\n');
const response = await waitForResponse(socket, '250', 30000);
expect(response).toInclude('250');
const messageProcessingTime = Date.now() - messageStart;
const processingRateMBps = (messageSize / (1024 * 1024)) / (messageProcessingTime / 1000);
results.push({
size: messageSize,
time: messageProcessingTime,
rate: processingRateMBps
});
console.log(`${(messageSize/1024).toFixed(0)}KB: ${messageProcessingTime}ms (${processingRateMBps.toFixed(2)} MB/s)`);
// Send RSET
socket.write('RSET\r\n');
await waitForResponse(socket, '250');
// Delay between large tests
await new Promise(resolve => setTimeout(resolve, 500));
}
const avgRate = results.reduce((sum, r) => sum + r.rate, 0) / results.length;
console.log(`\nAverage large message rate: ${avgRate.toFixed(2)} MB/s`);
socket.write('QUIT\r\n');
socket.end();
// Test passes if we can process at least 0.5 MB/s
expect(avgRate).toBeGreaterThan(0.5);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,317 @@
import * as plugins from '@git.zone/tstest/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
const TEST_PORT = 2525;
let testServer;
// Helper function to wait for SMTP response
const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise<string> => {
return new Promise((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeListener('data', handler);
reject(new Error(`Timeout waiting for ${expectedCode} response`));
}, timeout);
const handler = (data: Buffer) => {
buffer += data.toString();
const lines = buffer.split('\r\n');
// Check if we have a complete response
for (const line of lines) {
if (line.startsWith(expectedCode + ' ')) {
clearTimeout(timer);
socket.removeListener('data', handler);
resolve(buffer);
return;
}
}
};
socket.on('data', handler);
});
};
tap.test('prepare server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('PERF-07: Resource cleanup - Connection cleanup efficiency', async (tools) => {
const done = tools.defer();
const testConnections = 20; // Reduced from 50
const connections: net.Socket[] = [];
const cleanupTimes: number[] = [];
try {
// Force GC if available
if (global.gc) {
global.gc();
}
const initialMemory = process.memoryUsage();
console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Creating ${testConnections} connections for resource cleanup test...`);
// Create many connections and process emails
for (let i = 0; i < testConnections; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
connections.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Send EHLO
socket.write(`EHLO testhost-cleanup-${i}\r\n`);
await waitForResponse(socket, '250');
// Complete email transaction
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await waitForResponse(socket, '250');
socket.write('DATA\r\n');
await waitForResponse(socket, '354');
const emailContent = [
`From: sender${i}@example.com`,
`To: recipient${i}@example.com`,
`Subject: Resource Cleanup Test ${i}`,
'',
'Testing resource cleanup.',
'.',
''
].join('\r\n');
socket.write(emailContent);
await waitForResponse(socket, '250');
// Pause every 10 connections
if (i > 0 && i % 10 === 0) {
await new Promise(resolve => setTimeout(resolve, 50));
}
}
const midTestMemory = process.memoryUsage();
console.log(`Memory after creating connections: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`);
// Clean up all connections and measure cleanup time
console.log('\nCleaning up connections...');
for (let i = 0; i < connections.length; i++) {
const socket = connections[i];
const cleanupStart = Date.now();
try {
if (socket.writable) {
socket.write('QUIT\r\n');
try {
await waitForResponse(socket, '221', 1000);
} catch (e) {
// Ignore timeout on QUIT
}
}
socket.end();
await new Promise<void>((resolve) => {
socket.once('close', () => resolve());
setTimeout(() => resolve(), 100); // Fallback timeout
});
cleanupTimes.push(Date.now() - cleanupStart);
} catch (error) {
cleanupTimes.push(Date.now() - cleanupStart);
}
}
// Wait for cleanup to complete
await new Promise(resolve => setTimeout(resolve, 2000));
// Force GC if available
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 1000));
}
const finalMemory = process.memoryUsage();
const memoryIncreaseMB = (finalMemory.heapUsed - initialMemory.heapUsed) / (1024 * 1024);
const avgCleanupTime = cleanupTimes.reduce((a, b) => a + b, 0) / cleanupTimes.length;
const maxCleanupTime = Math.max(...cleanupTimes);
console.log(`\nResource Cleanup Results:`);
console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Mid-test memory: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Final memory: ${Math.round(finalMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`);
console.log(`Average cleanup time: ${avgCleanupTime.toFixed(0)}ms`);
console.log(`Max cleanup time: ${maxCleanupTime}ms`);
// Test passes if memory increase is less than 10MB and cleanup is fast
expect(memoryIncreaseMB).toBeLessThan(10);
expect(avgCleanupTime).toBeLessThan(100);
done.resolve();
} catch (error) {
// Emergency cleanup
connections.forEach(socket => socket.destroy());
done.reject(error);
}
});
tap.test('PERF-07: Resource cleanup - File descriptor management', async (tools) => {
const done = tools.defer();
const rapidConnections = 20;
let successfulCleanups = 0;
try {
console.log(`\nTesting rapid connection open/close cycles...`);
for (let i = 0; i < rapidConnections; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 10000
});
try {
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await waitForResponse(socket, '220');
// Quick EHLO/QUIT
socket.write('EHLO rapidtest\r\n');
await waitForResponse(socket, '250');
socket.write('QUIT\r\n');
await waitForResponse(socket, '221');
socket.end();
await new Promise<void>((resolve) => {
socket.once('close', () => {
successfulCleanups++;
resolve();
});
});
} catch (error) {
socket.destroy();
console.log(`Connection ${i} failed:`, error);
}
// Very short delay
await new Promise(resolve => setTimeout(resolve, 20));
}
console.log(`Successful cleanups: ${successfulCleanups}/${rapidConnections}`);
// Test passes if at least 90% of connections cleaned up successfully
const cleanupRate = successfulCleanups / rapidConnections;
expect(cleanupRate).toBeGreaterThanOrEqual(0.9);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('PERF-07: Resource cleanup - Memory recovery after load', async (tools) => {
const done = tools.defer();
try {
// Force GC if available
if (global.gc) {
global.gc();
}
const baselineMemory = process.memoryUsage();
console.log(`\nBaseline memory: ${Math.round(baselineMemory.heapUsed / (1024 * 1024))}MB`);
// Create load
const loadConnections = 10;
const sockets: net.Socket[] = [];
console.log('Creating load...');
for (let i = 0; i < loadConnections; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
sockets.push(socket);
// Just connect, don't send anything
await waitForResponse(socket, '220');
}
const loadMemory = process.memoryUsage();
console.log(`Memory under load: ${Math.round(loadMemory.heapUsed / (1024 * 1024))}MB`);
// Clean up all at once
console.log('Cleaning up all connections...');
sockets.forEach(socket => {
socket.destroy();
});
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 3000));
// Force GC multiple times
if (global.gc) {
for (let i = 0; i < 3; i++) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 500));
}
}
const recoveredMemory = process.memoryUsage();
const memoryIncrease = loadMemory.heapUsed - baselineMemory.heapUsed;
const memoryRecovered = loadMemory.heapUsed - recoveredMemory.heapUsed;
const recoveryPercent = memoryIncrease > 0 ? (memoryRecovered / memoryIncrease) * 100 : 100;
console.log(`Memory after cleanup: ${Math.round(recoveredMemory.heapUsed / (1024 * 1024))}MB`);
console.log(`Memory recovered: ${Math.round(memoryRecovered / (1024 * 1024))}MB`);
console.log(`Recovery percentage: ${recoveryPercent.toFixed(1)}%`);
// Test passes if memory is stable (no significant increase) or we recover at least 50%
if (memoryIncrease < 1024 * 1024) { // Less than 1MB increase
console.log('Memory usage was stable during test - good resource management!');
expect(true).toEqual(true);
} else {
expect(recoveryPercent).toBeGreaterThan(50);
}
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer(testServer);
});
tap.start();