dcrouter/test/suite/smtpclient_performance/test.cperf-03.memory-usage.ts
2025-05-24 18:12:08 +00:00

641 lines
22 KiB
TypeScript

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-03: should optimize memory usage', async (tools) => {
const testId = 'CPERF-03-memory-usage';
console.log(`\n${testId}: Testing memory usage optimization...`);
let scenarioCount = 0;
// Helper function to get memory usage
const getMemoryUsage = () => {
if (process.memoryUsage) {
const usage = process.memoryUsage();
return {
heapUsed: usage.heapUsed,
heapTotal: usage.heapTotal,
external: usage.external,
rss: usage.rss
};
}
return null;
};
// Helper function to format bytes
const formatBytes = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// Scenario 1: Memory usage during connection lifecycle
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing memory usage during connection lifecycle`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 memory.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-memory.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();
}
});
}
});
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const beforeConnection = getMemoryUsage();
console.log(` Memory before connection: ${formatBytes(beforeConnection?.heapUsed || 0)}`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const afterConnection = getMemoryUsage();
console.log(` Memory after client creation: ${formatBytes(afterConnection?.heapUsed || 0)}`);
// Send a test email
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Memory usage test',
text: 'Testing memory usage during email sending'
});
await smtpClient.sendMail(email);
const afterSending = getMemoryUsage();
console.log(` Memory after sending: ${formatBytes(afterSending?.heapUsed || 0)}`);
// Close connection
if (smtpClient.close) {
await smtpClient.close();
}
// Force garbage collection if available
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const afterClose = getMemoryUsage();
console.log(` Memory after close: ${formatBytes(afterClose?.heapUsed || 0)}`);
// Check for memory leaks
const memoryIncrease = (afterClose?.heapUsed || 0) - (beforeConnection?.heapUsed || 0);
console.log(` Net memory change: ${formatBytes(Math.abs(memoryIncrease))} ${memoryIncrease >= 0 ? 'increase' : 'decrease'}`);
// Memory increase should be minimal after cleanup
expect(memoryIncrease).toBeLessThan(1024 * 1024); // Less than 1MB increase
await testServer.server.close();
})();
// Scenario 2: Memory usage with large messages
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing memory usage with large messages`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 large.example.com ESMTP\r\n');
let inData = false;
let messageSize = 0;
socket.on('data', (data) => {
if (inData) {
messageSize += data.length;
if (data.toString().includes('\r\n.\r\n')) {
inData = false;
console.log(` [Server] Received message: ${formatBytes(messageSize)}`);
socket.write('250 OK\r\n');
messageSize = 0;
}
return;
}
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-large.example.com\r\n');
socket.write('250-SIZE 52428800\r\n'); // 50MB limit
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');
inData = true;
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
if (global.gc) {
global.gc();
}
const beforeLarge = getMemoryUsage();
console.log(` Memory before large message: ${formatBytes(beforeLarge?.heapUsed || 0)}`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Create messages of increasing sizes
const messageSizes = [
{ name: '1KB', size: 1024 },
{ name: '10KB', size: 10 * 1024 },
{ name: '100KB', size: 100 * 1024 },
{ name: '1MB', size: 1024 * 1024 },
{ name: '5MB', size: 5 * 1024 * 1024 }
];
for (const msgSize of messageSizes) {
console.log(` Testing ${msgSize.name} message...`);
const beforeMessage = getMemoryUsage();
// Create large content
const largeContent = 'x'.repeat(msgSize.size);
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Large message test - ${msgSize.name}`,
text: largeContent
});
const duringCreation = getMemoryUsage();
const creationIncrease = (duringCreation?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
console.log(` Memory increase during creation: ${formatBytes(creationIncrease)}`);
await smtpClient.sendMail(email);
const afterSending = getMemoryUsage();
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
// Clear reference to email
// email = null; // Can't reassign const
// Memory usage shouldn't grow linearly with message size
// due to streaming or buffering optimizations
expect(sendingIncrease).toBeLessThan(msgSize.size * 2); // At most 2x the message size
}
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const afterLarge = getMemoryUsage();
console.log(` Memory after large messages: ${formatBytes(afterLarge?.heapUsed || 0)}`);
await testServer.server.close();
})();
// Scenario 3: Memory usage with multiple concurrent connections
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing memory usage with concurrent connections`);
let connectionCount = 0;
const testServer = await createTestServer({
onConnection: async (socket) => {
connectionCount++;
const connId = connectionCount;
console.log(` [Server] Connection ${connId} established`);
socket.write('220 concurrent.example.com ESMTP\r\n');
socket.on('close', () => {
console.log(` [Server] Connection ${connId} closed`);
});
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-concurrent.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();
}
});
}
});
if (global.gc) {
global.gc();
}
const beforeConcurrent = getMemoryUsage();
console.log(` Memory before concurrent connections: ${formatBytes(beforeConcurrent?.heapUsed || 0)}`);
const concurrentCount = 10;
const clients: any[] = [];
// Create multiple concurrent clients
for (let i = 0; i < concurrentCount; i++) {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
clients.push(client);
}
const afterCreation = getMemoryUsage();
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
console.log(` Memory after creating ${concurrentCount} clients: ${formatBytes(creationIncrease)}`);
// Send emails concurrently
const promises = clients.map((client, i) => {
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: `Concurrent memory test ${i + 1}`,
text: `Testing concurrent memory usage - client ${i + 1}`
});
return client.sendMail(email);
});
await Promise.all(promises);
const afterSending = getMemoryUsage();
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
console.log(` Memory after concurrent sending: ${formatBytes(sendingIncrease)}`);
// Close all clients
await Promise.all(clients.map(client => {
if (client.close) {
return client.close();
}
return Promise.resolve();
}));
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const afterClose = getMemoryUsage();
const finalIncrease = (afterClose?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
console.log(` Memory after closing all connections: ${formatBytes(finalIncrease)}`);
// Memory per connection should be reasonable
const memoryPerConnection = creationIncrease / concurrentCount;
console.log(` Average memory per connection: ${formatBytes(memoryPerConnection)}`);
expect(memoryPerConnection).toBeLessThan(512 * 1024); // Less than 512KB per connection
expect(finalIncrease).toBeLessThan(creationIncrease * 0.5); // Significant cleanup
await testServer.server.close();
})();
// Scenario 4: Memory usage with connection pooling
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing memory usage with connection pooling`);
let connectionCount = 0;
let maxConnections = 0;
const testServer = await createTestServer({
onConnection: async (socket) => {
connectionCount++;
maxConnections = Math.max(maxConnections, connectionCount);
console.log(` [Server] Connection established (total: ${connectionCount}, max: ${maxConnections})`);
socket.write('220 pool.example.com ESMTP\r\n');
socket.on('close', () => {
connectionCount--;
});
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-pool.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();
}
});
}
});
if (global.gc) {
global.gc();
}
const beforePool = getMemoryUsage();
console.log(` Memory before pooling: ${formatBytes(beforePool?.heapUsed || 0)}`);
const pooledClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 5,
maxMessages: 100
});
const afterPoolCreation = getMemoryUsage();
const poolCreationIncrease = (afterPoolCreation?.heapUsed || 0) - (beforePool?.heapUsed || 0);
console.log(` Memory after pool creation: ${formatBytes(poolCreationIncrease)}`);
// Send many emails through the pool
const emailCount = 30;
const emails = Array(emailCount).fill(null).map((_, i) =>
new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: `Pooled memory test ${i + 1}`,
text: `Testing pooled memory usage - email ${i + 1}`
})
);
await Promise.all(emails.map(email => pooledClient.sendMail(email)));
const afterPoolSending = getMemoryUsage();
const poolSendingIncrease = (afterPoolSending?.heapUsed || 0) - (beforePool?.heapUsed || 0);
console.log(` Memory after sending ${emailCount} emails: ${formatBytes(poolSendingIncrease)}`);
console.log(` Maximum concurrent connections: ${maxConnections}`);
await pooledClient.close();
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const afterPoolClose = getMemoryUsage();
const poolFinalIncrease = (afterPoolClose?.heapUsed || 0) - (beforePool?.heapUsed || 0);
console.log(` Memory after pool close: ${formatBytes(poolFinalIncrease)}`);
// Pooling should use fewer connections and thus less memory
expect(maxConnections).toBeLessThanOrEqual(5);
expect(poolFinalIncrease).toBeLessThan(2 * 1024 * 1024); // Less than 2MB final increase
await testServer.server.close();
})();
// Scenario 5: Memory usage with attachments
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing memory usage with attachments`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 attachments.example.com ESMTP\r\n');
let inData = false;
let totalSize = 0;
socket.on('data', (data) => {
if (inData) {
totalSize += data.length;
if (data.toString().includes('\r\n.\r\n')) {
inData = false;
console.log(` [Server] Received email with attachments: ${formatBytes(totalSize)}`);
socket.write('250 OK\r\n');
totalSize = 0;
}
return;
}
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-attachments.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');
inData = true;
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
if (global.gc) {
global.gc();
}
const beforeAttachments = getMemoryUsage();
console.log(` Memory before attachments: ${formatBytes(beforeAttachments?.heapUsed || 0)}`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Test with different attachment sizes
const attachmentSizes = [
{ name: 'small', size: 10 * 1024 }, // 10KB
{ name: 'medium', size: 100 * 1024 }, // 100KB
{ name: 'large', size: 1024 * 1024 } // 1MB
];
for (const attachSize of attachmentSizes) {
console.log(` Testing ${attachSize.name} attachment (${formatBytes(attachSize.size)})...`);
const beforeAttachment = getMemoryUsage();
// Create binary attachment data
const attachmentData = Buffer.alloc(attachSize.size);
for (let i = 0; i < attachmentData.length; i++) {
attachmentData[i] = i % 256;
}
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Attachment memory test - ${attachSize.name}`,
text: `Testing memory usage with ${attachSize.name} attachment`,
attachments: [{
filename: `${attachSize.name}-file.bin`,
content: attachmentData
}]
});
const afterCreation = getMemoryUsage();
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
console.log(` Memory increase during email creation: ${formatBytes(creationIncrease)}`);
await smtpClient.sendMail(email);
const afterSending = getMemoryUsage();
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
// Memory usage should be efficient (not holding multiple copies)
expect(creationIncrease).toBeLessThan(attachSize.size * 3); // At most 3x (original + base64 + overhead)
}
await testServer.server.close();
})();
// Scenario 6: Memory leak detection
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing for memory leaks`);
const testServer = await createTestServer({
onConnection: async (socket) => {
socket.write('220 leak-test.example.com ESMTP\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250-leak-test.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();
}
});
}
});
// Perform multiple iterations to detect leaks
const iterations = 5;
const memoryMeasurements: number[] = [];
for (let iteration = 0; iteration < iterations; iteration++) {
console.log(` Iteration ${iteration + 1}/${iterations}...`);
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const beforeIteration = getMemoryUsage();
// Create and use SMTP client
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
// Send multiple emails
const emails = Array(10).fill(null).map((_, i) =>
new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: `Leak test iteration ${iteration + 1} email ${i + 1}`,
text: `Testing for memory leaks - iteration ${iteration + 1}, email ${i + 1}`
})
);
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
// Close client
if (smtpClient.close) {
await smtpClient.close();
}
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const afterIteration = getMemoryUsage();
const iterationIncrease = (afterIteration?.heapUsed || 0) - (beforeIteration?.heapUsed || 0);
memoryMeasurements.push(iterationIncrease);
console.log(` Memory change: ${formatBytes(iterationIncrease)}`);
}
// Analyze memory trend
const avgIncrease = memoryMeasurements.reduce((a, b) => a + b, 0) / memoryMeasurements.length;
const maxIncrease = Math.max(...memoryMeasurements);
const minIncrease = Math.min(...memoryMeasurements);
console.log(` Memory leak analysis:`);
console.log(` Average increase: ${formatBytes(avgIncrease)}`);
console.log(` Min increase: ${formatBytes(minIncrease)}`);
console.log(` Max increase: ${formatBytes(maxIncrease)}`);
console.log(` Range: ${formatBytes(maxIncrease - minIncrease)}`);
// Check for significant memory leaks
// Memory should not consistently increase across iterations
expect(avgIncrease).toBeLessThan(512 * 1024); // Less than 512KB average increase
expect(maxIncrease - minIncrease).toBeLessThan(1024 * 1024); // Range less than 1MB
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} memory usage scenarios tested ✓`);
});