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 '../server.loader.js'; const TEST_PORT = 2525; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('PERF-04: Memory usage - Connection memory test', async (tools) => { const done = tools.defer(); const connectionCount = 20; 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write(`EHLO testhost-mem-${i}\r\n`); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Send email transaction socket.write(`MAIL FROM:\r\n`); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write(`RCPT TO:\r\n`); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('DATA\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('354'); resolve(); }); }); // 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 new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); // 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 50MB for 20 connections) expect(memoryIncreaseMB).toBeLessThan(50); 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 = 5; const connectionsPerIteration = 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 new Promise((resolve) => { socket.once('data', () => resolve()); }); socket.write('EHLO leaktest\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); socket.write('QUIT\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); 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(); }); tap.start();