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 => { 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((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:\r\n`); await waitForResponse(socket, '250'); socket.write(`RCPT TO:\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((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); }); export default tap.start();