vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
232 lines (231 loc) • 8.84 kB
JavaScript
import v8 from 'v8';
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import os from 'os';
import logger from '../../../logger.js';
import { getMemoryStats } from '../parser.js';
export class MemoryLeakDetector {
options;
memorySamples = [];
snapshots = [];
snapshotTimer = null;
checkTimer = null;
isInitialized = false;
static DEFAULT_OPTIONS = {
snapshotDir: path.join(os.tmpdir(), 'code-map-generator-heap-snapshots'),
snapshotInterval: 5 * 60 * 1000,
maxSnapshots: 5,
leakThreshold: 0.2,
autoDetect: true,
checkInterval: 60 * 1000,
maxSamples: 10
};
constructor(options = {}) {
this.options = {
...MemoryLeakDetector.DEFAULT_OPTIONS,
...options
};
}
async init() {
if (this.isInitialized) {
return;
}
await fs.mkdir(this.options.snapshotDir, { recursive: true });
if (this.options.autoDetect) {
this.startAutomaticDetection();
}
this.isInitialized = true;
logger.info(`MemoryLeakDetector initialized with snapshot directory: ${this.options.snapshotDir}`);
}
startAutomaticDetection() {
this.startMemorySampling();
this.startSnapshotSchedule();
logger.info('Automatic memory leak detection started');
}
stopAutomaticDetection() {
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}
if (this.snapshotTimer) {
clearInterval(this.snapshotTimer);
this.snapshotTimer = null;
}
logger.info('Automatic memory leak detection stopped');
}
startMemorySampling() {
if (this.checkTimer) {
clearInterval(this.checkTimer);
}
this.takeMemorySample();
this.checkTimer = setInterval(() => {
this.takeMemorySample();
this.analyzeMemoryTrend();
}, this.options.checkInterval);
logger.debug(`Memory sampling started with interval: ${this.options.checkInterval}ms`);
}
takeMemorySample() {
const memoryUsage = process.memoryUsage();
const sample = {
timestamp: Date.now(),
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
rss: memoryUsage.rss,
external: memoryUsage.external,
arrayBuffers: memoryUsage.arrayBuffers || 0
};
this.memorySamples.push(sample);
if (this.memorySamples.length > this.options.maxSamples) {
this.memorySamples.shift();
}
logger.debug(`Memory sample taken: ${JSON.stringify(sample)}`);
}
startSnapshotSchedule() {
if (this.snapshotTimer) {
clearInterval(this.snapshotTimer);
}
this.snapshotTimer = setInterval(() => {
this.takeHeapSnapshot();
}, this.options.snapshotInterval);
logger.debug(`Heap snapshot schedule started with interval: ${this.options.snapshotInterval}ms`);
}
async takeHeapSnapshot() {
const timestamp = Date.now();
const snapshotPath = path.join(this.options.snapshotDir, `heap-snapshot-${timestamp}.heapsnapshot`);
try {
const snapshot = v8.getHeapSnapshot();
const writeStream = fsSync.createWriteStream(snapshotPath);
snapshot.pipe(writeStream);
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
const memoryUsage = process.memoryUsage();
const metadata = {
timestamp,
path: snapshotPath,
memoryUsage: {
heapUsed: memoryUsage.heapUsed,
heapTotal: memoryUsage.heapTotal,
rss: memoryUsage.rss,
external: memoryUsage.external,
arrayBuffers: memoryUsage.arrayBuffers || 0
}
};
this.snapshots.push(metadata);
if (this.snapshots.length > this.options.maxSnapshots) {
const oldestSnapshot = this.snapshots.shift();
if (oldestSnapshot) {
try {
await fs.unlink(oldestSnapshot.path);
logger.debug(`Deleted old heap snapshot: ${oldestSnapshot.path}`);
}
catch (error) {
logger.warn(`Failed to delete old heap snapshot: ${oldestSnapshot.path}`, { error });
}
}
}
logger.info(`Heap snapshot taken: ${snapshotPath}`);
return snapshotPath;
}
catch (error) {
logger.error(`Failed to take heap snapshot: ${error}`);
throw error;
}
}
analyzeMemoryTrend() {
if (this.memorySamples.length < 2) {
const stats = getMemoryStats();
return {
leakDetected: false,
trend: 'stable',
samples: [...this.memorySamples],
latestStats: stats,
timestamp: Date.now()
};
}
const oldestSample = this.memorySamples[0];
const newestSample = this.memorySamples[this.memorySamples.length - 1];
const heapUsedChange = (newestSample.heapUsed - oldestSample.heapUsed) / oldestSample.heapUsed;
const externalChange = (newestSample.external - oldestSample.external) / oldestSample.external;
const arrayBuffersChange = oldestSample.arrayBuffers > 0
? (newestSample.arrayBuffers - oldestSample.arrayBuffers) / oldestSample.arrayBuffers
: 0;
let trend = 'stable';
if (heapUsedChange > 0.05) {
trend = 'increasing';
}
else if (heapUsedChange < -0.05) {
trend = 'decreasing';
}
let leakDetected = false;
let leakType;
let increasePercentage;
if (heapUsedChange > this.options.leakThreshold) {
leakDetected = true;
leakType = 'heap';
increasePercentage = heapUsedChange * 100;
logger.warn(`Potential heap memory leak detected: ${increasePercentage.toFixed(2)}% increase over ${this.memorySamples.length} samples`);
}
else if (externalChange > this.options.leakThreshold) {
leakDetected = true;
leakType = 'external';
increasePercentage = externalChange * 100;
logger.warn(`Potential external memory leak detected: ${increasePercentage.toFixed(2)}% increase over ${this.memorySamples.length} samples`);
}
else if (arrayBuffersChange > this.options.leakThreshold) {
leakDetected = true;
leakType = 'arrayBuffers';
increasePercentage = arrayBuffersChange * 100;
logger.warn(`Potential array buffers memory leak detected: ${increasePercentage.toFixed(2)}% increase over ${this.memorySamples.length} samples`);
}
const stats = getMemoryStats();
return {
leakDetected,
leakType,
increasePercentage,
trend,
samples: [...this.memorySamples],
latestStats: stats,
timestamp: Date.now()
};
}
async compareHeapSnapshots(snapshot1Path, snapshot2Path) {
logger.info(`Comparing heap snapshots: ${snapshot1Path} and ${snapshot2Path}`);
const stat1 = await fs.stat(snapshot1Path);
const stat2 = await fs.stat(snapshot2Path);
const sizeDiff = stat2.size - stat1.size;
const percentChange = (sizeDiff / stat1.size) * 100;
return {
snapshot1Path,
snapshot2Path,
snapshot1Size: stat1.size,
snapshot2Size: stat2.size,
sizeDiff,
percentChange,
memoryDifference: sizeDiff,
objectCountDifference: 0,
leakSuspects: [],
analysis: {
hasLeak: sizeDiff > 0,
confidence: Math.min(Math.abs(percentChange) / 10, 1),
recommendations: sizeDiff > 0 ? ['Monitor memory usage', 'Check for memory leaks'] : ['Memory usage is stable']
}
};
}
getLatestSnapshot() {
if (this.snapshots.length === 0) {
return undefined;
}
return this.snapshots[this.snapshots.length - 1];
}
getAllSnapshots() {
return [...this.snapshots];
}
cleanup() {
this.stopAutomaticDetection();
this.memorySamples = [];
logger.info('Memory leak detector cleaned up');
}
}