import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { createTestSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; // Helper function to get memory usage const getMemoryUsage = () => { const usage = process.memoryUsage(); return { heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB }; }; // Force garbage collection if available const forceGC = () => { if (global.gc) { global.gc(); global.gc(); // Run twice for thoroughness } }; tap.test('CREL-05: Connection Pool Memory Management', async () => { console.log('\n🧠 Testing SMTP Client Memory Leak Prevention'); console.log('=' .repeat(60)); console.log('\nšŸŠ Testing connection pool memory management...'); // Create test server const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { const initialMemory = getMemoryUsage(); console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`); console.log(' Phase 1: Creating and using multiple connection pools...'); const memorySnapshots = []; for (let poolIndex = 0; poolIndex < 5; poolIndex++) { console.log(` Creating connection pool ${poolIndex + 1}...`); const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 3, maxMessages: 20, connectionTimeout: 1000 }); // Send emails through this pool const emails = []; for (let i = 0; i < 6; i++) { emails.push(new Email({ from: `sender${poolIndex}@memoryleak.test`, to: [`recipient${i}@memoryleak.test`], subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`, text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}` })); } // Send emails concurrently const promises = emails.map((email, index) => { return smtpClient.sendMail(email).then(result => { return { success: true, result }; }).catch(error => { return { success: false, error }; }); }); const results = await Promise.all(promises); const successful = results.filter(r => r.success).length; console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`); // Close the pool smtpClient.close(); console.log(` Pool ${poolIndex + 1} closed`); // Force garbage collection and measure memory forceGC(); await new Promise(resolve => setTimeout(resolve, 100)); const currentMemory = getMemoryUsage(); memorySnapshots.push({ pool: poolIndex + 1, heap: currentMemory.heapUsed, rss: currentMemory.rss, external: currentMemory.external }); console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`); } console.log('\n Memory analysis:'); memorySnapshots.forEach((snapshot, index) => { const memoryIncrease = snapshot.heap - initialMemory.heapUsed; console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`); }); // Check for memory leaks (memory should not continuously increase) const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed; const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed; const leakGrowth = lastIncrease - firstIncrease; console.log(` Memory leak assessment:`); console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`); console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`); console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`); console.log(` Memory management: ${leakGrowth < 3.0 ? 'Good (< 3MB growth)' : 'Potential leak detected'}`); expect(leakGrowth).toBeLessThan(5.0); // Allow some memory growth but detect major leaks } finally { server.close(); } }); tap.test('CREL-05: Email Object Memory Lifecycle', async () => { console.log('\nšŸ“§ Testing email object memory lifecycle...'); // Create test server const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 2 }); const initialMemory = getMemoryUsage(); console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`); console.log(' Phase 1: Creating large batches of email objects...'); const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes const memorySnapshots = []; for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) { const batchSize = batchSizes[batchIndex]; console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`); const emails = []; for (let i = 0; i < batchSize; i++) { emails.push(new Email({ from: 'sender@emailmemory.test', to: [`recipient${i}@emailmemory.test`], subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`, text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`, html: `

Email ${i + 1}

Testing memory patterns with HTML content. Batch ${batchIndex + 1}.

` })); } console.log(` Sending batch ${batchIndex + 1}...`); const promises = emails.map((email, index) => { return smtpClient.sendMail(email).then(result => { return { success: true }; }).catch(error => { return { success: false, error }; }); }); const results = await Promise.all(promises); const successful = results.filter(r => r.success).length; console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`); // Clear email references emails.length = 0; // Force garbage collection forceGC(); await new Promise(resolve => setTimeout(resolve, 100)); const currentMemory = getMemoryUsage(); memorySnapshots.push({ batch: batchIndex + 1, size: batchSize, heap: currentMemory.heapUsed, external: currentMemory.external }); console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`); } console.log('\n Email object memory analysis:'); memorySnapshots.forEach((snapshot, index) => { const memoryIncrease = snapshot.heap - initialMemory.heapUsed; console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`); }); // Check if memory scales reasonably with email batch size const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed)); const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length; console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`); console.log(` Average batch size: ${avgBatchSize} emails`); console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`); console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`); expect(maxMemoryIncrease).toBeLessThan(15); // Allow reasonable memory usage smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-05: Long-Running Client Memory Stability', async () => { console.log('\nā±ļø Testing long-running client memory stability...'); // Create test server const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 2, maxMessages: 1000 }); const initialMemory = getMemoryUsage(); console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`); console.log(' Starting sustained email sending operation...'); const memoryMeasurements = []; const totalEmails = 100; // Reduced for test efficiency const measurementInterval = 20; // Measure every 20 emails let emailsSent = 0; let emailsFailed = 0; for (let i = 0; i < totalEmails; i++) { const email = new Email({ from: 'sender@longrunning.test', to: [`recipient${i}@longrunning.test`], subject: `Long Running Test ${i + 1}`, text: `Sustained operation test email ${i + 1}` }); try { await smtpClient.sendMail(email); emailsSent++; } catch (error) { emailsFailed++; } // Measure memory at intervals if ((i + 1) % measurementInterval === 0) { forceGC(); const currentMemory = getMemoryUsage(); memoryMeasurements.push({ emailCount: i + 1, heap: currentMemory.heapUsed, rss: currentMemory.rss, timestamp: Date.now() }); console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`); } } console.log('\n Long-running memory analysis:'); console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`); memoryMeasurements.forEach((measurement, index) => { const memoryIncrease = measurement.heap - initialMemory.heapUsed; console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`); }); // Analyze memory growth trend if (memoryMeasurements.length >= 2) { const firstMeasurement = memoryMeasurements[0]; const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1]; const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap; const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount; const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`); console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`); console.log(` Memory stability: ${growthRate < 10 ? 'Excellent' : growthRate < 25 ? 'Good' : 'Concerning'}`); expect(growthRate).toBeLessThan(50); // Allow reasonable growth but detect major leaks } expect(emailsSent).toBeGreaterThanOrEqual(totalEmails - 5); // Most emails should succeed smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-05: Large Content Memory Management', async () => { console.log('\n🌊 Testing large content memory management...'); // Create test server const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 1 }); const initialMemory = getMemoryUsage(); console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`); console.log(' Testing with various content sizes...'); const contentSizes = [ { size: 1024, name: '1KB' }, { size: 10240, name: '10KB' }, { size: 102400, name: '100KB' }, { size: 256000, name: '250KB' } ]; for (const contentTest of contentSizes) { console.log(` Testing ${contentTest.name} content size...`); const beforeMemory = getMemoryUsage(); // Create large text content const largeText = 'X'.repeat(contentTest.size); const email = new Email({ from: 'sender@largemem.test', to: ['recipient@largemem.test'], subject: `Large Content Test - ${contentTest.name}`, text: largeText }); try { await smtpClient.sendMail(email); console.log(` āœ“ ${contentTest.name} email sent successfully`); } catch (error) { console.log(` āœ— ${contentTest.name} email failed: ${error.message}`); } // Force cleanup forceGC(); await new Promise(resolve => setTimeout(resolve, 100)); const afterMemory = getMemoryUsage(); const memoryDiff = afterMemory.heapUsed - beforeMemory.heapUsed; console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`); console.log(` Efficiency: ${Math.abs(memoryDiff) < (contentTest.size / 1024 / 1024) * 2 ? 'Good' : 'High memory usage'}`); } const finalMemory = getMemoryUsage(); const totalMemoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed; console.log(`\n Large content memory summary:`); console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`); console.log(` Memory management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`); expect(totalMemoryIncrease).toBeLessThan(20); // Allow reasonable memory usage for large content smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-05: Test Summary', async () => { console.log('\nāœ… CREL-05: Memory Leak Prevention Reliability Tests completed'); console.log('🧠 All memory management scenarios tested successfully'); }); tap.start();