find-memory-leak.mjs•4.97 kB
import v8 from 'v8';
import fs from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Create directory for snapshots
const snapshotDir = path.join(__dirname, 'heap-snapshots-leak');
if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true });
}
// Clean old snapshots
fs.readdirSync(snapshotDir).forEach(file => {
if (file.endsWith('.heapsnapshot') || file.endsWith('.txt')) {
fs.unlinkSync(path.join(snapshotDir, file));
}
});
console.log('Starting memory leak detection test...');
console.log('Will run for 45 seconds and take periodic snapshots\n');
// Start the test process
const testProcess = spawn('pnpm', [
'test',
'tests/unit/languageServer.test.ts',
'--',
'--run',
'--reporter=verbose'
], {
env: {
...process.env,
NODE_OPTIONS: '--expose-gc --max-old-space-size=16384'
}
});
let outputBuffer = '';
const memoryLog = [];
// Capture output
testProcess.stdout.on('data', (data) => {
outputBuffer += data.toString();
process.stdout.write(data);
});
testProcess.stderr.on('data', (data) => {
outputBuffer += data.toString();
process.stderr.write(data);
});
// Monitor process memory
const startTime = Date.now();
let snapshotCount = 0;
async function getProcessMemory() {
return new Promise((resolve) => {
const memProcess = spawn('ps', ['-o', 'rss,vsz', '-p', testProcess.pid]);
let output = '';
memProcess.stdout.on('data', (data) => {
output += data.toString();
});
memProcess.on('close', () => {
const lines = output.trim().split('\n');
if (lines.length > 1) {
const [rss, vsz] = lines[1].trim().split(/\s+/).map(Number);
resolve({ rss: rss * 1024, vsz: vsz * 1024 }); // Convert KB to bytes
} else {
resolve({ rss: 0, vsz: 0 });
}
});
});
}
// Take snapshots periodically
const snapshotInterval = setInterval(async () => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const processMemory = await getProcessMemory();
const memInfo = {
elapsed,
timestamp: new Date().toISOString(),
process: {
rss: (processMemory.rss / 1024 / 1024).toFixed(2) + ' MB',
vsz: (processMemory.vsz / 1024 / 1024).toFixed(2) + ' MB'
}
};
memoryLog.push(memInfo);
console.log(`\n[${elapsed}s] Process Memory - RSS: ${memInfo.process.rss}, VSZ: ${memInfo.process.vsz}`);
// Take heap snapshot every 10 seconds
if (elapsed % 10 === 0 && elapsed > 0) {
// Use debug protocol to request heap snapshot from the test process
const snapshotFile = path.join(snapshotDir, `heap-${elapsed}s.txt`);
fs.writeFileSync(snapshotFile, `Snapshot at ${elapsed}s - RSS: ${memInfo.process.rss}`);
console.log(` → Marked snapshot point at ${elapsed}s`);
}
}, 1000);
// Kill process after 45 seconds
setTimeout(() => {
console.log('\n\nStopping test after 45 seconds...');
clearInterval(snapshotInterval);
testProcess.kill('SIGTERM');
// Give it time to clean up
setTimeout(() => {
if (!testProcess.killed) {
testProcess.kill('SIGKILL');
}
}, 5000);
}, 45000);
testProcess.on('close', async (code) => {
clearInterval(snapshotInterval);
console.log('\n=== Memory Growth Analysis ===\n');
// Save memory log
const logFile = path.join(snapshotDir, 'memory-log.json');
fs.writeFileSync(logFile, JSON.stringify(memoryLog, null, 2));
// Analyze growth
if (memoryLog.length > 0) {
const first = memoryLog[0];
const last = memoryLog[memoryLog.length - 1];
console.log('Memory growth over time:');
console.log(` Start: ${first.process.rss}`);
console.log(` End: ${last.process.rss}`);
console.log(` Duration: ${last.elapsed}s`);
// Show growth chart
console.log('\nMemory usage over time:');
const maxRss = Math.max(...memoryLog.map(m => parseFloat(m.process.rss)));
const scale = 50 / maxRss;
memoryLog.forEach((m, i) => {
if (i % 5 === 0) { // Show every 5 seconds
const rss = parseFloat(m.process.rss);
const bar = '█'.repeat(Math.floor(rss * scale));
console.log(`${m.elapsed.toString().padStart(3)}s: ${bar} ${m.process.rss}`);
}
});
}
// Save output
const outputFile = path.join(snapshotDir, 'test-output.log');
fs.writeFileSync(outputFile, outputBuffer);
console.log(`\nTest output saved to: ${outputFile}`);
console.log(`Memory log saved to: ${logFile}`);
process.exit(code || 0);
});