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 ✓`); });