update
This commit is contained in:
@ -1,641 +1,332 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from './plugins.js';
|
||||
import { createTestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
tap.test('CPERF-03: should optimize memory usage', async (tools) => {
|
||||
const testId = 'CPERF-03-memory-usage';
|
||||
console.log(`\n${testId}: Testing memory usage optimization...`);
|
||||
let testServer: ITestServer;
|
||||
|
||||
let scenarioCount = 0;
|
||||
// Helper function to get memory usage
|
||||
const getMemoryUsage = () => {
|
||||
if (process.memoryUsage) {
|
||||
const usage = process.memoryUsage();
|
||||
return {
|
||||
heapUsed: usage.heapUsed,
|
||||
heapTotal: usage.heapTotal,
|
||||
external: usage.external,
|
||||
rss: usage.rss
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Helper function to get memory usage
|
||||
const getMemoryUsage = () => {
|
||||
if (process.memoryUsage) {
|
||||
const usage = process.memoryUsage();
|
||||
return {
|
||||
heapUsed: usage.heapUsed,
|
||||
heapTotal: usage.heapTotal,
|
||||
external: usage.external,
|
||||
rss: usage.rss
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// Helper function to format bytes
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
// Helper function to format bytes
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
};
|
||||
tap.test('setup - start SMTP server for memory tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 0,
|
||||
enableStarttls: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Scenario 1: Memory usage during connection lifecycle
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage during connection lifecycle`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 memory.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-memory.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeConnection = getMemoryUsage();
|
||||
console.log(` Memory before connection: ${formatBytes(beforeConnection?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const afterConnection = getMemoryUsage();
|
||||
console.log(` Memory after client creation: ${formatBytes(afterConnection?.heapUsed || 0)}`);
|
||||
|
||||
// Send a test email
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Memory usage test',
|
||||
text: 'Testing memory usage during email sending'
|
||||
});
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
console.log(` Memory after sending: ${formatBytes(afterSending?.heapUsed || 0)}`);
|
||||
|
||||
// Close connection
|
||||
if (smtpClient.close) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterClose = getMemoryUsage();
|
||||
console.log(` Memory after close: ${formatBytes(afterClose?.heapUsed || 0)}`);
|
||||
|
||||
// Check for memory leaks
|
||||
const memoryIncrease = (afterClose?.heapUsed || 0) - (beforeConnection?.heapUsed || 0);
|
||||
console.log(` Net memory change: ${formatBytes(Math.abs(memoryIncrease))} ${memoryIncrease >= 0 ? 'increase' : 'decrease'}`);
|
||||
|
||||
// Memory increase should be minimal after cleanup
|
||||
expect(memoryIncrease).toBeLessThan(1024 * 1024); // Less than 1MB increase
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 2: Memory usage with large messages
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with large messages`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 large.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let messageSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
messageSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received message: ${formatBytes(messageSize)}`);
|
||||
socket.write('250 OK\r\n');
|
||||
messageSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-large.example.com\r\n');
|
||||
socket.write('250-SIZE 52428800\r\n'); // 50MB limit
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeLarge = getMemoryUsage();
|
||||
console.log(` Memory before large message: ${formatBytes(beforeLarge?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Create messages of increasing sizes
|
||||
const messageSizes = [
|
||||
{ name: '1KB', size: 1024 },
|
||||
{ name: '10KB', size: 10 * 1024 },
|
||||
{ name: '100KB', size: 100 * 1024 },
|
||||
{ name: '1MB', size: 1024 * 1024 },
|
||||
{ name: '5MB', size: 5 * 1024 * 1024 }
|
||||
];
|
||||
|
||||
for (const msgSize of messageSizes) {
|
||||
console.log(` Testing ${msgSize.name} message...`);
|
||||
|
||||
const beforeMessage = getMemoryUsage();
|
||||
|
||||
// Create large content
|
||||
const largeContent = 'x'.repeat(msgSize.size);
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Large message test - ${msgSize.name}`,
|
||||
text: largeContent
|
||||
});
|
||||
|
||||
const duringCreation = getMemoryUsage();
|
||||
const creationIncrease = (duringCreation?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
|
||||
console.log(` Memory increase during creation: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeMessage?.heapUsed || 0);
|
||||
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Clear reference to email
|
||||
// email = null; // Can't reassign const
|
||||
|
||||
// Memory usage shouldn't grow linearly with message size
|
||||
// due to streaming or buffering optimizations
|
||||
expect(sendingIncrease).toBeLessThan(msgSize.size * 2); // At most 2x the message size
|
||||
}
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterLarge = getMemoryUsage();
|
||||
console.log(` Memory after large messages: ${formatBytes(afterLarge?.heapUsed || 0)}`);
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 3: Memory usage with multiple concurrent connections
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with concurrent connections`);
|
||||
|
||||
let connectionCount = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
const connId = connectionCount;
|
||||
console.log(` [Server] Connection ${connId} established`);
|
||||
socket.write('220 concurrent.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log(` [Server] Connection ${connId} closed`);
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-concurrent.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeConcurrent = getMemoryUsage();
|
||||
console.log(` Memory before concurrent connections: ${formatBytes(beforeConcurrent?.heapUsed || 0)}`);
|
||||
|
||||
const concurrentCount = 10;
|
||||
const clients: any[] = [];
|
||||
|
||||
// Create multiple concurrent clients
|
||||
for (let i = 0; i < concurrentCount; i++) {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
const afterCreation = getMemoryUsage();
|
||||
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after creating ${concurrentCount} clients: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
// Send emails concurrently
|
||||
const promises = clients.map((client, i) => {
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Concurrent memory test ${i + 1}`,
|
||||
text: `Testing concurrent memory usage - client ${i + 1}`
|
||||
});
|
||||
return client.sendMail(email);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after concurrent sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Close all clients
|
||||
await Promise.all(clients.map(client => {
|
||||
if (client.close) {
|
||||
return client.close();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterClose = getMemoryUsage();
|
||||
const finalIncrease = (afterClose?.heapUsed || 0) - (beforeConcurrent?.heapUsed || 0);
|
||||
console.log(` Memory after closing all connections: ${formatBytes(finalIncrease)}`);
|
||||
|
||||
// Memory per connection should be reasonable
|
||||
const memoryPerConnection = creationIncrease / concurrentCount;
|
||||
console.log(` Average memory per connection: ${formatBytes(memoryPerConnection)}`);
|
||||
|
||||
expect(memoryPerConnection).toBeLessThan(512 * 1024); // Less than 512KB per connection
|
||||
expect(finalIncrease).toBeLessThan(creationIncrease * 0.5); // Significant cleanup
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 4: Memory usage with connection pooling
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with connection pooling`);
|
||||
|
||||
let connectionCount = 0;
|
||||
let maxConnections = 0;
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
connectionCount++;
|
||||
maxConnections = Math.max(maxConnections, connectionCount);
|
||||
console.log(` [Server] Connection established (total: ${connectionCount}, max: ${maxConnections})`);
|
||||
socket.write('220 pool.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('close', () => {
|
||||
connectionCount--;
|
||||
});
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-pool.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforePool = getMemoryUsage();
|
||||
console.log(` Memory before pooling: ${formatBytes(beforePool?.heapUsed || 0)}`);
|
||||
|
||||
const pooledClient = createSmtpClient({
|
||||
tap.test('CPERF-03: Memory usage during connection lifecycle', async (tools) => {
|
||||
tools.timeout(30000);
|
||||
|
||||
const memoryBefore = getMemoryUsage();
|
||||
console.log('Initial memory usage:', {
|
||||
heapUsed: formatBytes(memoryBefore.heapUsed),
|
||||
heapTotal: formatBytes(memoryBefore.heapTotal),
|
||||
rss: formatBytes(memoryBefore.rss)
|
||||
});
|
||||
|
||||
// Create and close multiple connections
|
||||
const connectionCount = 10;
|
||||
|
||||
for (let i = 0; i < connectionCount; i++) {
|
||||
const client = await createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
debug: false
|
||||
});
|
||||
|
||||
const afterPoolCreation = getMemoryUsage();
|
||||
const poolCreationIncrease = (afterPoolCreation?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after pool creation: ${formatBytes(poolCreationIncrease)}`);
|
||||
|
||||
// Send many emails through the pool
|
||||
const emailCount = 30;
|
||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Pooled memory test ${i + 1}`,
|
||||
text: `Testing pooled memory usage - email ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emails.map(email => pooledClient.sendMail(email)));
|
||||
|
||||
const afterPoolSending = getMemoryUsage();
|
||||
const poolSendingIncrease = (afterPoolSending?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after sending ${emailCount} emails: ${formatBytes(poolSendingIncrease)}`);
|
||||
console.log(` Maximum concurrent connections: ${maxConnections}`);
|
||||
|
||||
await pooledClient.close();
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterPoolClose = getMemoryUsage();
|
||||
const poolFinalIncrease = (afterPoolClose?.heapUsed || 0) - (beforePool?.heapUsed || 0);
|
||||
console.log(` Memory after pool close: ${formatBytes(poolFinalIncrease)}`);
|
||||
|
||||
// Pooling should use fewer connections and thus less memory
|
||||
expect(maxConnections).toBeLessThanOrEqual(5);
|
||||
expect(poolFinalIncrease).toBeLessThan(2 * 1024 * 1024); // Less than 2MB final increase
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 5: Memory usage with attachments
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing memory usage with attachments`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
console.log(' [Server] Client connected');
|
||||
socket.write('220 attachments.example.com ESMTP\r\n');
|
||||
|
||||
let inData = false;
|
||||
let totalSize = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
if (inData) {
|
||||
totalSize += data.length;
|
||||
if (data.toString().includes('\r\n.\r\n')) {
|
||||
inData = false;
|
||||
console.log(` [Server] Received email with attachments: ${formatBytes(totalSize)}`);
|
||||
socket.write('250 OK\r\n');
|
||||
totalSize = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-attachments.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Send a test email
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Memory test ${i + 1}`,
|
||||
text: 'Testing memory usage'
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
const beforeAttachments = getMemoryUsage();
|
||||
console.log(` Memory before attachments: ${formatBytes(beforeAttachments?.heapUsed || 0)}`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
});
|
||||
|
||||
// Test with different attachment sizes
|
||||
const attachmentSizes = [
|
||||
{ name: 'small', size: 10 * 1024 }, // 10KB
|
||||
{ name: 'medium', size: 100 * 1024 }, // 100KB
|
||||
{ name: 'large', size: 1024 * 1024 } // 1MB
|
||||
];
|
||||
|
||||
for (const attachSize of attachmentSizes) {
|
||||
console.log(` Testing ${attachSize.name} attachment (${formatBytes(attachSize.size)})...`);
|
||||
|
||||
const beforeAttachment = getMemoryUsage();
|
||||
|
||||
// Create binary attachment data
|
||||
const attachmentData = Buffer.alloc(attachSize.size);
|
||||
for (let i = 0; i < attachmentData.length; i++) {
|
||||
attachmentData[i] = i % 256;
|
||||
}
|
||||
|
||||
const email = new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Attachment memory test - ${attachSize.name}`,
|
||||
text: `Testing memory usage with ${attachSize.name} attachment`,
|
||||
attachments: [{
|
||||
filename: `${attachSize.name}-file.bin`,
|
||||
content: attachmentData
|
||||
}]
|
||||
});
|
||||
|
||||
const afterCreation = getMemoryUsage();
|
||||
const creationIncrease = (afterCreation?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
|
||||
console.log(` Memory increase during email creation: ${formatBytes(creationIncrease)}`);
|
||||
|
||||
await smtpClient.sendMail(email);
|
||||
|
||||
const afterSending = getMemoryUsage();
|
||||
const sendingIncrease = (afterSending?.heapUsed || 0) - (beforeAttachment?.heapUsed || 0);
|
||||
console.log(` Memory increase after sending: ${formatBytes(sendingIncrease)}`);
|
||||
|
||||
// Memory usage should be efficient (not holding multiple copies)
|
||||
expect(creationIncrease).toBeLessThan(attachSize.size * 3); // At most 3x (original + base64 + overhead)
|
||||
}
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
|
||||
// Scenario 6: Memory leak detection
|
||||
await (async () => {
|
||||
scenarioCount++;
|
||||
console.log(`\nScenario ${scenarioCount}: Testing for memory leaks`);
|
||||
|
||||
const testServer = await createTestServer({
|
||||
onConnection: async (socket) => {
|
||||
socket.write('220 leak-test.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
socket.write('250-leak-test.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
} else if (command === '.') {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (command === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
await client.sendMail(email);
|
||||
await client.close();
|
||||
|
||||
// Small delay between connections
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const memoryAfter = getMemoryUsage();
|
||||
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed;
|
||||
|
||||
console.log(`Memory after ${connectionCount} connections:`, {
|
||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
||||
heapTotal: formatBytes(memoryAfter.heapTotal),
|
||||
rss: formatBytes(memoryAfter.rss)
|
||||
});
|
||||
console.log(`Memory increase: ${formatBytes(memoryIncrease)}`);
|
||||
console.log(`Average per connection: ${formatBytes(memoryIncrease / connectionCount)}`);
|
||||
|
||||
// Memory increase should be reasonable
|
||||
expect(memoryIncrease / connectionCount).toBeLessThan(1024 * 1024); // Less than 1MB per connection
|
||||
});
|
||||
|
||||
tap.test('CPERF-03: Memory usage with large messages', async (tools) => {
|
||||
tools.timeout(30000);
|
||||
|
||||
const client = await createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
debug: false
|
||||
});
|
||||
|
||||
const memoryBefore = getMemoryUsage();
|
||||
console.log('Memory before large messages:', {
|
||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
||||
});
|
||||
|
||||
// Send messages of increasing size
|
||||
const sizes = [1024, 10240, 102400]; // 1KB, 10KB, 100KB
|
||||
|
||||
for (const size of sizes) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Large message test (${formatBytes(size)})`,
|
||||
text: 'x'.repeat(size)
|
||||
});
|
||||
|
||||
await client.sendMail(email);
|
||||
|
||||
const memoryAfter = getMemoryUsage();
|
||||
console.log(`Memory after ${formatBytes(size)} message:`, {
|
||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
||||
increase: formatBytes(memoryAfter.heapUsed - memoryBefore.heapUsed)
|
||||
});
|
||||
|
||||
// Small delay
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
await client.close();
|
||||
|
||||
const memoryFinal = getMemoryUsage();
|
||||
const totalIncrease = memoryFinal.heapUsed - memoryBefore.heapUsed;
|
||||
|
||||
console.log(`Total memory increase: ${formatBytes(totalIncrease)}`);
|
||||
|
||||
// Memory should not grow excessively
|
||||
expect(totalIncrease).toBeLessThan(10 * 1024 * 1024); // Less than 10MB total
|
||||
});
|
||||
|
||||
// Perform multiple iterations to detect leaks
|
||||
const iterations = 5;
|
||||
const memoryMeasurements: number[] = [];
|
||||
tap.test('CPERF-03: Memory usage with connection pooling', async (tools) => {
|
||||
tools.timeout(30000);
|
||||
|
||||
const memoryBefore = getMemoryUsage();
|
||||
console.log('Memory before pooling test:', {
|
||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
||||
});
|
||||
|
||||
const pooledClient = await createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 3,
|
||||
debug: false
|
||||
});
|
||||
|
||||
// Send multiple emails through the pool
|
||||
const emailCount = 15;
|
||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
||||
new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Pooled memory test ${i + 1}`,
|
||||
text: 'Testing memory with connection pooling'
|
||||
})
|
||||
);
|
||||
|
||||
// Send in batches
|
||||
for (let i = 0; i < emails.length; i += 3) {
|
||||
const batch = emails.slice(i, i + 3);
|
||||
await Promise.all(batch.map(email =>
|
||||
pooledClient.sendMail(email).catch(err => console.log('Send error:', err.message))
|
||||
));
|
||||
|
||||
// Check memory after each batch
|
||||
const memoryNow = getMemoryUsage();
|
||||
console.log(`Memory after batch ${Math.floor(i/3) + 1}:`, {
|
||||
heapUsed: formatBytes(memoryNow.heapUsed),
|
||||
increase: formatBytes(memoryNow.heapUsed - memoryBefore.heapUsed)
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
await pooledClient.close();
|
||||
|
||||
const memoryFinal = getMemoryUsage();
|
||||
const totalIncrease = memoryFinal.heapUsed - memoryBefore.heapUsed;
|
||||
|
||||
console.log(`Total memory increase with pooling: ${formatBytes(totalIncrease)}`);
|
||||
console.log(`Average per email: ${formatBytes(totalIncrease / emailCount)}`);
|
||||
|
||||
// Pooling should be memory efficient
|
||||
expect(totalIncrease / emailCount).toBeLessThan(500 * 1024); // Less than 500KB per email
|
||||
});
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
console.log(` Iteration ${iteration + 1}/${iterations}...`);
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const beforeIteration = getMemoryUsage();
|
||||
|
||||
// Create and use SMTP client
|
||||
const smtpClient = createSmtpClient({
|
||||
tap.test('CPERF-03: Memory cleanup after errors', async (tools) => {
|
||||
tools.timeout(30000);
|
||||
|
||||
const memoryBefore = getMemoryUsage();
|
||||
console.log('Memory before error test:', {
|
||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
||||
});
|
||||
|
||||
// Try to send emails that might fail
|
||||
const errorCount = 5;
|
||||
|
||||
for (let i = 0; i < errorCount; i++) {
|
||||
try {
|
||||
const client = await createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false
|
||||
secure: false,
|
||||
connectionTimeout: 1000, // Short timeout
|
||||
debug: false
|
||||
});
|
||||
|
||||
// Send multiple emails
|
||||
const emails = Array(10).fill(null).map((_, i) =>
|
||||
new plugins.smartmail.Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i + 1}@example.com`],
|
||||
subject: `Leak test iteration ${iteration + 1} email ${i + 1}`,
|
||||
text: `Testing for memory leaks - iteration ${iteration + 1}, email ${i + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
|
||||
|
||||
// Close client
|
||||
if (smtpClient.close) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const afterIteration = getMemoryUsage();
|
||||
const iterationIncrease = (afterIteration?.heapUsed || 0) - (beforeIteration?.heapUsed || 0);
|
||||
memoryMeasurements.push(iterationIncrease);
|
||||
|
||||
console.log(` Memory change: ${formatBytes(iterationIncrease)}`);
|
||||
// Create a large email that might cause issues
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Error test ${i + 1}`,
|
||||
text: 'x'.repeat(100000), // 100KB
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: Buffer.alloc(50000).toString('base64'), // 50KB attachment
|
||||
encoding: 'base64'
|
||||
}]
|
||||
});
|
||||
|
||||
await client.sendMail(email);
|
||||
await client.close();
|
||||
} catch (error) {
|
||||
console.log(`Error ${i + 1} handled: ${error.message}`);
|
||||
}
|
||||
|
||||
// Analyze memory trend
|
||||
const avgIncrease = memoryMeasurements.reduce((a, b) => a + b, 0) / memoryMeasurements.length;
|
||||
const maxIncrease = Math.max(...memoryMeasurements);
|
||||
const minIncrease = Math.min(...memoryMeasurements);
|
||||
|
||||
console.log(` Memory leak analysis:`);
|
||||
console.log(` Average increase: ${formatBytes(avgIncrease)}`);
|
||||
console.log(` Min increase: ${formatBytes(minIncrease)}`);
|
||||
console.log(` Max increase: ${formatBytes(maxIncrease)}`);
|
||||
console.log(` Range: ${formatBytes(maxIncrease - minIncrease)}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const memoryAfter = getMemoryUsage();
|
||||
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed;
|
||||
|
||||
console.log(`Memory after ${errorCount} error scenarios:`, {
|
||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
||||
increase: formatBytes(memoryIncrease)
|
||||
});
|
||||
|
||||
// Memory should be properly cleaned up after errors
|
||||
expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024); // Less than 5MB increase
|
||||
});
|
||||
|
||||
// Check for significant memory leaks
|
||||
// Memory should not consistently increase across iterations
|
||||
expect(avgIncrease).toBeLessThan(512 * 1024); // Less than 512KB average increase
|
||||
expect(maxIncrease - minIncrease).toBeLessThan(1024 * 1024); // Range less than 1MB
|
||||
tap.test('CPERF-03: Long-running memory stability', async (tools) => {
|
||||
tools.timeout(60000);
|
||||
|
||||
const client = await createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
debug: false
|
||||
});
|
||||
|
||||
const memorySnapshots = [];
|
||||
const duration = 10000; // 10 seconds
|
||||
const interval = 2000; // Check every 2 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log('Testing memory stability over time...');
|
||||
|
||||
let emailsSent = 0;
|
||||
|
||||
while (Date.now() - startTime < duration) {
|
||||
// Send an email
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Stability test ${++emailsSent}`,
|
||||
text: `Testing memory stability at ${new Date().toISOString()}`
|
||||
});
|
||||
|
||||
try {
|
||||
await client.sendMail(email);
|
||||
} catch (error) {
|
||||
console.log('Send error:', error.message);
|
||||
}
|
||||
|
||||
// Take memory snapshot
|
||||
const memory = getMemoryUsage();
|
||||
const elapsed = Date.now() - startTime;
|
||||
memorySnapshots.push({
|
||||
time: elapsed,
|
||||
heapUsed: memory.heapUsed
|
||||
});
|
||||
|
||||
console.log(`[${elapsed}ms] Heap: ${formatBytes(memory.heapUsed)}, Emails sent: ${emailsSent}`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
await client.close();
|
||||
|
||||
// Analyze memory growth
|
||||
const firstSnapshot = memorySnapshots[0];
|
||||
const lastSnapshot = memorySnapshots[memorySnapshots.length - 1];
|
||||
const memoryGrowth = lastSnapshot.heapUsed - firstSnapshot.heapUsed;
|
||||
const growthRate = memoryGrowth / (lastSnapshot.time / 1000); // bytes per second
|
||||
|
||||
console.log(`\nMemory stability results:`);
|
||||
console.log(` Duration: ${lastSnapshot.time}ms`);
|
||||
console.log(` Emails sent: ${emailsSent}`);
|
||||
console.log(` Memory growth: ${formatBytes(memoryGrowth)}`);
|
||||
console.log(` Growth rate: ${formatBytes(growthRate)}/second`);
|
||||
|
||||
// Memory growth should be minimal over time
|
||||
expect(growthRate).toBeLessThan(150 * 1024); // Less than 150KB/second growth
|
||||
});
|
||||
|
||||
await testServer.server.close();
|
||||
})();
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
console.log(`\n${testId}: All ${scenarioCount} memory usage scenarios tested ✓`);
|
||||
});
|
||||
export default tap.start();
|
Reference in New Issue
Block a user