2025-05-23 19:09:30 +00:00
|
|
|
import * as plugins from '@git.zone/tstest/tapbundle';
|
|
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
2025-05-23 19:03:44 +00:00
|
|
|
import * as net from 'net';
|
2025-05-23 21:20:39 +00:00
|
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
const TEST_PORT = 2525;
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
let testServer;
|
|
|
|
|
2025-05-24 11:34:05 +00:00
|
|
|
// Helper function to wait for SMTP response
|
|
|
|
const waitForResponse = (socket: net.Socket, expectedCode: string, timeout = 5000): Promise<string> => {
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2025-05-23 19:03:44 +00:00
|
|
|
tap.test('prepare server', async () => {
|
2025-05-24 00:23:35 +00:00
|
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
2025-05-23 19:03:44 +00:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('PERF-07: Resource cleanup - Connection cleanup efficiency', async (tools) => {
|
|
|
|
const done = tools.defer();
|
2025-05-24 11:34:05 +00:00
|
|
|
const testConnections = 20; // Reduced from 50
|
2025-05-23 19:03:44 +00:00
|
|
|
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<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', resolve);
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Read greeting
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '220');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write(`EHLO testhost-cleanup-${i}\r\n`);
|
|
|
|
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '250');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Complete email transaction
|
|
|
|
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '250');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '250');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.write('DATA\r\n');
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '354');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
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);
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '250');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// 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');
|
2025-05-24 11:34:05 +00:00
|
|
|
try {
|
|
|
|
await waitForResponse(socket, '221', 1000);
|
|
|
|
} catch (e) {
|
|
|
|
// Ignore timeout on QUIT
|
|
|
|
}
|
2025-05-23 19:03:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
socket.end();
|
|
|
|
await new Promise<void>((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<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', resolve);
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Read greeting
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '220');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Quick EHLO/QUIT
|
|
|
|
socket.write('EHLO rapidtest\r\n');
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '250');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.write('QUIT\r\n');
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '221');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
await new Promise<void>((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<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', resolve);
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
sockets.push(socket);
|
|
|
|
|
|
|
|
// Just connect, don't send anything
|
2025-05-24 11:34:05 +00:00
|
|
|
await waitForResponse(socket, '220');
|
2025-05-23 19:03:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2025-05-24 11:34:05 +00:00
|
|
|
const memoryIncrease = loadMemory.heapUsed - baselineMemory.heapUsed;
|
2025-05-23 19:03:44 +00:00
|
|
|
const memoryRecovered = loadMemory.heapUsed - recoveredMemory.heapUsed;
|
2025-05-24 11:34:05 +00:00
|
|
|
const recoveryPercent = memoryIncrease > 0 ? (memoryRecovered / memoryIncrease) * 100 : 100;
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
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)}%`);
|
|
|
|
|
2025-05-24 11:34:05 +00:00
|
|
|
// Test passes if memory is stable (no significant increase) or we recover at least 50%
|
|
|
|
if (memoryIncrease < 1024 * 1024) { // Less than 1MB increase
|
|
|
|
console.log('Memory usage was stable during test - good resource management!');
|
|
|
|
expect(true).toEqual(true);
|
|
|
|
} else {
|
|
|
|
expect(recoveryPercent).toBeGreaterThan(50);
|
|
|
|
}
|
2025-05-23 19:03:44 +00:00
|
|
|
done.resolve();
|
|
|
|
} catch (error) {
|
|
|
|
done.reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('cleanup server', async () => {
|
2025-05-24 00:23:35 +00:00
|
|
|
await stopTestServer(testServer);
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
2025-05-25 19:05:43 +00:00
|
|
|
export default tap.start();
|