dcrouter/test/suite/performance/test.memory-usage.ts
2025-05-24 00:23:35 +00:00

266 lines
8.0 KiB
TypeScript

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-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<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-mem-${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);
});
// Send 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();
});
});
// 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<void>((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<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Quick transaction
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
socket.write('EHLO leaktest\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();
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);
});
tap.start();