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;
|
|
|
|
|
|
|
|
tap.test('prepare server', async () => {
|
|
|
|
await startTestServer();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('PERF-03: CPU utilization - Load test', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
const monitoringDuration = 5000; // 5 seconds
|
|
|
|
const connectionCount = 10;
|
|
|
|
const connections: net.Socket[] = [];
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Record initial CPU usage
|
|
|
|
const initialCpuUsage = process.cpuUsage();
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
// Create multiple connections and send emails
|
|
|
|
console.log(`Creating ${connectionCount} connections for CPU load test...`);
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Process greeting
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
socket.once('data', () => resolve());
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write(`EHLO testhost-cpu-${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 email content
|
|
|
|
const emailContent = [
|
|
|
|
`From: sender${i}@example.com`,
|
|
|
|
`To: recipient${i}@example.com`,
|
|
|
|
`Subject: CPU Utilization Test ${i}`,
|
|
|
|
'',
|
|
|
|
`This email tests CPU utilization during concurrent operations.`,
|
|
|
|
`Connection ${i} of ${connectionCount}`,
|
|
|
|
'.',
|
|
|
|
''
|
|
|
|
].join('\r\n');
|
|
|
|
|
|
|
|
socket.write(emailContent);
|
|
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => {
|
|
|
|
const response = chunk.toString();
|
|
|
|
expect(response).toInclude('250');
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep connections active during monitoring period
|
|
|
|
console.log(`Monitoring CPU usage for ${monitoringDuration}ms...`);
|
|
|
|
|
|
|
|
// Send periodic NOOP commands to keep connections active
|
|
|
|
const noopInterval = setInterval(() => {
|
|
|
|
connections.forEach((socket, idx) => {
|
|
|
|
if (socket.writable) {
|
|
|
|
socket.write('NOOP\r\n');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, monitoringDuration));
|
|
|
|
clearInterval(noopInterval);
|
|
|
|
|
|
|
|
// Calculate CPU usage
|
|
|
|
const finalCpuUsage = process.cpuUsage(initialCpuUsage);
|
|
|
|
const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000;
|
|
|
|
const elapsedTime = Date.now() - startTime;
|
|
|
|
const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100;
|
|
|
|
|
|
|
|
console.log(`\nCPU Utilization Results:`);
|
|
|
|
console.log(`Total CPU time: ${totalCpuTimeMs.toFixed(0)}ms`);
|
|
|
|
console.log(`Elapsed time: ${elapsedTime}ms`);
|
|
|
|
console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`);
|
|
|
|
console.log(`User CPU: ${(finalCpuUsage.user / 1000).toFixed(0)}ms`);
|
|
|
|
console.log(`System CPU: ${(finalCpuUsage.system / 1000).toFixed(0)}ms`);
|
|
|
|
|
|
|
|
// Clean up connections
|
|
|
|
for (const socket of connections) {
|
|
|
|
if (socket.writable) {
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test passes if CPU usage is reasonable (less than 80%)
|
|
|
|
expect(cpuUtilizationPercent).toBeLessThan(80);
|
|
|
|
done.resolve();
|
|
|
|
} catch (error) {
|
|
|
|
// Clean up on error
|
|
|
|
connections.forEach(socket => socket.destroy());
|
|
|
|
done.reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('PERF-03: CPU utilization - Stress test', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
const testDuration = 3000; // 3 seconds
|
|
|
|
let requestCount = 0;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const initialCpuUsage = process.cpuUsage();
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
console.log(`\nRunning CPU stress test for ${testDuration}ms...`);
|
|
|
|
|
|
|
|
// Create a single connection for rapid requests
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Read greeting
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
socket.once('data', () => resolve());
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO stresstest\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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Rapid command loop
|
|
|
|
const endTime = Date.now() + testDuration;
|
|
|
|
const commands = ['NOOP', 'RSET', 'VRFY test@example.com', 'HELP'];
|
|
|
|
let commandIndex = 0;
|
|
|
|
|
|
|
|
while (Date.now() < endTime) {
|
|
|
|
const command = commands[commandIndex % commands.length];
|
|
|
|
socket.write(`${command}\r\n`);
|
|
|
|
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
socket.once('data', () => {
|
|
|
|
requestCount++;
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
commandIndex++;
|
|
|
|
|
|
|
|
// Small delay to avoid overwhelming
|
|
|
|
if (requestCount % 20 === 0) {
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate final CPU usage
|
|
|
|
const finalCpuUsage = process.cpuUsage(initialCpuUsage);
|
|
|
|
const totalCpuTimeMs = (finalCpuUsage.user + finalCpuUsage.system) / 1000;
|
|
|
|
const elapsedTime = Date.now() - startTime;
|
|
|
|
const cpuUtilizationPercent = (totalCpuTimeMs / elapsedTime) * 100;
|
|
|
|
const requestsPerSecond = (requestCount / elapsedTime) * 1000;
|
|
|
|
|
|
|
|
console.log(`\nStress Test Results:`);
|
|
|
|
console.log(`Requests processed: ${requestCount}`);
|
|
|
|
console.log(`Requests per second: ${requestsPerSecond.toFixed(1)}`);
|
|
|
|
console.log(`CPU utilization: ${cpuUtilizationPercent.toFixed(1)}%`);
|
|
|
|
console.log(`CPU time per request: ${(totalCpuTimeMs / requestCount).toFixed(2)}ms`);
|
|
|
|
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
// Test passes if CPU usage per request is reasonable
|
|
|
|
const cpuPerRequest = totalCpuTimeMs / requestCount;
|
|
|
|
expect(cpuPerRequest).toBeLessThan(10); // Less than 10ms CPU per request
|
|
|
|
done.resolve();
|
|
|
|
} catch (error) {
|
|
|
|
done.reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('cleanup server', async () => {
|
|
|
|
await stopTestServer();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.start();
|