dcrouter/test/suite/performance/test.resource-cleanup.ts

344 lines
10 KiB
TypeScript
Raw Normal View History

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-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();
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<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write(`EHLO testhost-cleanup-${i}\r\n`);
await new Promise<void>((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:<sender${i}@example.com>\r\n`);
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write('DATA\r\n');
await new Promise<void>((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<void>((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<void>((resolve) => {
const timeout = setTimeout(() => resolve(), 1000);
socket.once('data', () => {
clearTimeout(timeout);
resolve();
});
});
}
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
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Quick EHLO/QUIT
socket.write('EHLO rapidtest\r\n');
await new Promise<void>((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<void>((resolve) => {
socket.once('data', () => resolve());
});
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
await new Promise<void>((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 () => {
2025-05-24 00:23:35 +00:00
await stopTestServer(testServer);
2025-05-23 19:03:44 +00:00
});
tap.start();