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; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('PERF-07: Resource cleanup - Connection cleanup efficiency', async (tools) => { const done = tools.defer(); const testConnections = 50; const connections: net.Socket[] = []; const cleanupTimes: number[] = []; try { // Force GC if available if (global.gc) { global.gc(); } const initialMemory = process.memoryUsage(); console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); console.log(`Creating ${testConnections} connections for resource cleanup test...`); // Create many connections and process emails for (let i = 0; i < testConnections; 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-cleanup-${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); }); // Complete 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(); }); }); const emailContent = [ `From: sender${i}@example.com`, `To: recipient${i}@example.com`, `Subject: Resource Cleanup Test ${i}`, '', 'Testing resource cleanup.', '.', '' ].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 10 connections if (i > 0 && i % 10 === 0) { await new Promise(resolve => setTimeout(resolve, 50)); } } const midTestMemory = process.memoryUsage(); console.log(`Memory after creating connections: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`); // Clean up all connections and measure cleanup time console.log('\nCleaning up connections...'); for (let i = 0; i < connections.length; i++) { const socket = connections[i]; const cleanupStart = Date.now(); try { if (socket.writable) { socket.write('QUIT\r\n'); await new Promise((resolve) => { const timeout = setTimeout(() => resolve(), 1000); socket.once('data', () => { clearTimeout(timeout); resolve(); }); }); } socket.end(); await new Promise((resolve) => { socket.once('close', () => resolve()); setTimeout(() => resolve(), 100); // Fallback timeout }); cleanupTimes.push(Date.now() - cleanupStart); } catch (error) { cleanupTimes.push(Date.now() - cleanupStart); } } // Wait for cleanup to complete await new Promise(resolve => setTimeout(resolve, 2000)); // Force GC if available if (global.gc) { global.gc(); await new Promise(resolve => setTimeout(resolve, 1000)); } const finalMemory = process.memoryUsage(); const memoryIncreaseMB = (finalMemory.heapUsed - initialMemory.heapUsed) / (1024 * 1024); const avgCleanupTime = cleanupTimes.reduce((a, b) => a + b, 0) / cleanupTimes.length; const maxCleanupTime = Math.max(...cleanupTimes); console.log(`\nResource Cleanup Results:`); console.log(`Initial memory: ${Math.round(initialMemory.heapUsed / (1024 * 1024))}MB`); console.log(`Mid-test memory: ${Math.round(midTestMemory.heapUsed / (1024 * 1024))}MB`); console.log(`Final memory: ${Math.round(finalMemory.heapUsed / (1024 * 1024))}MB`); console.log(`Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`); console.log(`Average cleanup time: ${avgCleanupTime.toFixed(0)}ms`); console.log(`Max cleanup time: ${maxCleanupTime}ms`); // Test passes if memory increase is less than 10MB and cleanup is fast expect(memoryIncreaseMB).toBeLessThan(10); expect(avgCleanupTime).toBeLessThan(100); done.resolve(); } catch (error) { // Emergency cleanup connections.forEach(socket => socket.destroy()); done.reject(error); } }); tap.test('PERF-07: Resource cleanup - File descriptor management', async (tools) => { const done = tools.defer(); const rapidConnections = 20; let successfulCleanups = 0; try { console.log(`\nTesting rapid connection open/close cycles...`); for (let i = 0; i < rapidConnections; i++) { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 10000 }); try { await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Quick EHLO/QUIT socket.write('EHLO rapidtest\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(); await new Promise((resolve) => { socket.once('close', () => { successfulCleanups++; resolve(); }); }); } catch (error) { socket.destroy(); console.log(`Connection ${i} failed:`, error); } // Very short delay await new Promise(resolve => setTimeout(resolve, 20)); } console.log(`Successful cleanups: ${successfulCleanups}/${rapidConnections}`); // Test passes if at least 90% of connections cleaned up successfully const cleanupRate = successfulCleanups / rapidConnections; expect(cleanupRate).toBeGreaterThanOrEqual(0.9); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('PERF-07: Resource cleanup - Memory recovery after load', async (tools) => { const done = tools.defer(); try { // Force GC if available if (global.gc) { global.gc(); } const baselineMemory = process.memoryUsage(); console.log(`\nBaseline memory: ${Math.round(baselineMemory.heapUsed / (1024 * 1024))}MB`); // Create load const loadConnections = 10; const sockets: net.Socket[] = []; console.log('Creating load...'); for (let i = 0; i < loadConnections; 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); }); sockets.push(socket); // Just connect, don't send anything await new Promise((resolve) => { socket.once('data', () => resolve()); }); } const loadMemory = process.memoryUsage(); console.log(`Memory under load: ${Math.round(loadMemory.heapUsed / (1024 * 1024))}MB`); // Clean up all at once console.log('Cleaning up all connections...'); sockets.forEach(socket => { socket.destroy(); }); // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 3000)); // Force GC multiple times if (global.gc) { for (let i = 0; i < 3; i++) { global.gc(); await new Promise(resolve => setTimeout(resolve, 500)); } } const recoveredMemory = process.memoryUsage(); const memoryRecovered = loadMemory.heapUsed - recoveredMemory.heapUsed; const recoveryPercent = (memoryRecovered / (loadMemory.heapUsed - baselineMemory.heapUsed)) * 100; console.log(`Memory after cleanup: ${Math.round(recoveredMemory.heapUsed / (1024 * 1024))}MB`); console.log(`Memory recovered: ${Math.round(memoryRecovered / (1024 * 1024))}MB`); console.log(`Recovery percentage: ${recoveryPercent.toFixed(1)}%`); // Test passes if we recover at least 50% of the memory used during load expect(recoveryPercent).toBeGreaterThan(50); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(testServer); }); tap.start();