vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
492 lines (491 loc) • 20.1 kB
JavaScript
import os from 'os';
import v8 from 'v8';
import logger from '../../../logger.js';
import { RecursionGuard } from '../../../utils/recursion-guard.js';
import { MemoryCache } from './memoryCache.js';
export class MemoryManager {
options;
caches = new Map();
grammarManager = null;
monitorTimer = null;
gcTimer = null;
maxMemoryBytes;
static DEFAULT_OPTIONS = {
maxMemoryPercentage: 0.4,
monitorInterval: 30000,
autoManage: true,
pruneThreshold: 0.7,
prunePercentage: 0.3
};
constructor(options = {}) {
this.options = {
...MemoryManager.DEFAULT_OPTIONS,
...options
};
const totalMemory = os.totalmem();
this.maxMemoryBytes = totalMemory * this.options.maxMemoryPercentage;
logger.info(`MemoryManager created with max memory: ${this.formatBytes(this.maxMemoryBytes)} (${this.options.maxMemoryPercentage * 100}% of system memory)`);
if (this.options.autoManage) {
this.startMonitoring();
}
}
registerCache(cache) {
const stats = cache.getStats();
this.caches.set(stats.name, cache);
logger.debug(`Registered cache "${stats.name}" with MemoryManager`);
}
unregisterCache(name) {
this.caches.delete(name);
logger.debug(`Unregistered cache "${name}" from MemoryManager`);
}
registerGrammarManager(manager) {
this.grammarManager = manager;
logger.debug('Registered GrammarManager with MemoryManager');
}
startMonitoring() {
if (this.monitorTimer) {
return;
}
this.monitorTimer = setInterval(async () => {
await this.checkMemoryUsage();
}, this.options.monitorInterval);
setImmediate(() => {
logger.debug(`Started memory monitoring with interval: ${this.options.monitorInterval}ms`);
});
}
stopMonitoring() {
if (this.monitorTimer) {
clearInterval(this.monitorTimer);
this.monitorTimer = null;
logger.debug('Stopped memory monitoring');
}
}
startPeriodicGC(interval = 5 * 60 * 1000) {
if (this.gcTimer) {
clearInterval(this.gcTimer);
}
this.gcTimer = setInterval(() => {
const stats = this.getMemoryStats();
if (stats.formatted.memoryStatus !== 'normal') {
logger.info(`Memory status is ${stats.formatted.memoryStatus}, running garbage collection`);
this.runGarbageCollection();
}
else {
logger.debug('Memory status is normal, skipping garbage collection');
}
}, interval);
this.gcTimer.unref();
logger.info(`Started periodic garbage collection with interval: ${interval}ms`);
}
stopPeriodicGC() {
if (this.gcTimer) {
clearInterval(this.gcTimer);
this.gcTimer = null;
logger.info('Stopped periodic garbage collection');
}
}
async checkMemoryUsage() {
const result = await RecursionGuard.executeWithRecursionGuard('MemoryManager.checkMemoryUsage', () => {
const stats = this.getMemoryStats();
const heapUsed = stats.raw.heapStats.used_heap_size;
const heapLimit = stats.raw.heapStats.heap_size_limit;
const heapPercentage = heapUsed / heapLimit;
logger.debug(`Memory usage: ${this.formatBytes(heapUsed)} / ${this.formatBytes(heapLimit)} (${(heapPercentage * 100).toFixed(2)}%)`);
if (heapPercentage > this.options.pruneThreshold) {
logger.info(`Memory usage exceeds threshold (${(this.options.pruneThreshold * 100).toFixed(2)}%), pruning caches...`);
this.pruneCaches();
}
}, {
maxDepth: 3,
enableLogging: false,
executionTimeout: 5000
}, `instance_${this.constructor.name}_${Date.now()}`);
if (!result.success && result.recursionDetected) {
logger.warn('Memory usage check skipped due to recursion detection');
}
else if (!result.success && result.error) {
logger.error({ err: result.error }, 'Memory usage check failed');
}
}
pruneCaches() {
const allStats = Array.from(this.caches.values()).map(cache => cache.getStats());
allStats.sort((a, b) => b.totalSize - a.totalSize);
for (const stats of allStats) {
const cache = this.caches.get(stats.name);
if (cache) {
const entriesToRemove = Math.ceil(stats.size * this.options.prunePercentage);
if (entriesToRemove > 0) {
logger.debug(`Pruning ${entriesToRemove} entries from cache "${stats.name}"`);
if (entriesToRemove >= stats.size) {
cache.clear();
}
else {
for (let i = 0; i < entriesToRemove; i++) {
cache.set('__dummy__' + i, null);
cache.delete('__dummy__' + i);
}
}
}
}
}
}
getMemoryStats() {
if (RecursionGuard.isMethodExecuting('MemoryManager.getMemoryStats')) {
logger.debug('Memory stats request skipped due to recursion detection');
return this.createFallbackMemoryStats();
}
const totalMemory = os.totalmem();
const freeMemory = os.freemem();
const memoryUsage = (totalMemory - freeMemory) / totalMemory;
const memoryUsagePercentage = memoryUsage * 100;
const processMemory = process.memoryUsage();
const heapStats = v8.getHeapStatistics();
const heapSpaceStats = v8.getHeapSpaceStatistics();
const highMemoryThreshold = 0.8;
const criticalMemoryThreshold = 0.9;
let memoryStatus = 'normal';
if (memoryUsage > criticalMemoryThreshold) {
memoryStatus = 'critical';
}
else if (memoryUsage > highMemoryThreshold) {
memoryStatus = 'high';
}
const cacheStats = Array.from(this.caches.values()).map(cache => cache.getStats());
const totalCacheSize = cacheStats.reduce((total, cache) => total + (cache.totalSize || 0), 0);
const grammarStats = this.grammarManager ? this.grammarManager.getStats() : {};
const formattedStats = {
totalSystemMemory: this.formatBytes(totalMemory),
freeSystemMemory: this.formatBytes(freeMemory),
usedSystemMemory: this.formatBytes(totalMemory - freeMemory),
memoryUsagePercentage: memoryUsagePercentage.toFixed(2) + '%',
memoryStatus,
process: {
rss: this.formatBytes(processMemory.rss),
heapTotal: this.formatBytes(processMemory.heapTotal),
heapUsed: this.formatBytes(processMemory.heapUsed),
external: this.formatBytes(processMemory.external),
arrayBuffers: this.formatBytes(processMemory.arrayBuffers || 0),
},
v8: {
heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
totalHeapSize: this.formatBytes(heapStats.total_heap_size),
usedHeapSize: this.formatBytes(heapStats.used_heap_size),
heapSizeExecutable: this.formatBytes(heapStats.total_heap_size_executable),
mallocedMemory: this.formatBytes(heapStats.malloced_memory),
peakMallocedMemory: this.formatBytes(heapStats.peak_malloced_memory),
},
cache: {
totalSize: this.formatBytes(totalCacheSize),
cacheCount: cacheStats.length,
},
thresholds: {
highMemoryThreshold: (highMemoryThreshold * 100) + '%',
criticalMemoryThreshold: (criticalMemoryThreshold * 100) + '%',
}
};
return {
formatted: formattedStats,
raw: {
totalSystemMemory: totalMemory,
freeSystemMemory: freeMemory,
memoryUsagePercentage: memoryUsage,
processMemory,
heapStats,
heapSpaceStats,
},
cacheStats,
grammarStats,
timestamp: Date.now(),
};
}
formatBytes(bytes, decimals = 2) {
if (bytes === 0)
return '0 Bytes';
if (!bytes || isNaN(bytes))
return 'Unknown';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
if (i < 0 || i >= sizes.length)
return `${bytes} Bytes`;
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
createASTCache() {
const cache = new MemoryCache({
name: 'ast-cache',
maxEntries: 500,
maxAge: 15 * 60 * 1000,
sizeCalculator: (tree) => {
let nodeCount = 0;
let currentNode = tree.rootNode;
const nodesToVisit = [currentNode];
while (nodesToVisit.length > 0) {
currentNode = nodesToVisit.pop() || null;
if (!currentNode)
continue;
nodeCount++;
for (let i = 0; i < currentNode.childCount; i++) {
const child = currentNode.child(i);
if (child) {
nodesToVisit.push(child);
}
}
}
return nodeCount * 200;
},
maxSize: 50 * 1024 * 1024,
dispose: (_key, _tree) => {
}
});
this.registerCache(cache);
return cache;
}
createSourceCodeCache() {
const cache = new MemoryCache({
name: 'source-code-cache',
maxEntries: 500,
maxAge: 15 * 60 * 1000,
sizeCalculator: (sourceCode) => {
return sourceCode.length;
},
maxSize: 30 * 1024 * 1024,
dispose: (_key, _sourceCode) => {
}
});
this.registerCache(cache);
return cache;
}
runGarbageCollection() {
logger.info('Running manual garbage collection...');
const beforeStats = this.getMemoryStats();
for (const cache of this.caches.values()) {
cache.clear();
}
logger.info('All caches cleared successfully.');
if (this.grammarManager) {
this.grammarManager.unloadUnusedGrammars();
logger.info('Unused grammars unloaded successfully.');
}
if (typeof global !== 'undefined' && global.gc) {
try {
logger.debug('Calling global.gc() to suggest garbage collection');
global.gc();
}
catch (error) {
logger.warn('Failed to suggest garbage collection', { error });
}
}
else {
logger.debug('global.gc not available. Run Node.js with --expose-gc to enable manual GC suggestions');
}
const afterStats = this.getMemoryStats();
const memoryFreed = beforeStats.raw.processMemory.heapUsed - afterStats.raw.processMemory.heapUsed;
logger.info(`Memory usage after cleanup: ${afterStats.formatted.process.heapUsed} / ${afterStats.formatted.v8.heapSizeLimit}`);
if (memoryFreed > 0) {
logger.info(`Memory freed: ${this.formatBytes(memoryFreed)}`);
}
else {
logger.warn(`Memory usage increased by: ${this.formatBytes(Math.abs(memoryFreed))}`);
}
logger.info(`Memory status: ${afterStats.formatted.memoryStatus}`);
if (afterStats.formatted.memoryStatus !== 'normal') {
logger.warn(`Memory usage is still ${afterStats.formatted.memoryStatus} after cleanup. Consider restarting the process.`);
}
}
detectMemoryPressure() {
const stats = this.getMemoryStats();
const heapUsed = stats.raw.heapStats.used_heap_size;
const heapLimit = stats.raw.heapStats.heap_size_limit;
const heapPercentage = heapUsed / heapLimit;
const systemUsed = stats.raw.totalSystemMemory - stats.raw.freeSystemMemory;
const systemPercentage = systemUsed / stats.raw.totalSystemMemory;
let level = 'normal';
const recommendations = [];
if (heapPercentage > 0.95 || systemPercentage > 0.95) {
level = 'critical';
recommendations.push('Immediate emergency cleanup required');
recommendations.push('Consider restarting the process');
recommendations.push('Reduce cache sizes aggressively');
}
else if (heapPercentage > 0.85 || systemPercentage > 0.85) {
level = 'high';
recommendations.push('Aggressive cache pruning recommended');
recommendations.push('Reduce concurrent operations');
recommendations.push('Monitor memory usage closely');
}
else if (heapPercentage > 0.7 || systemPercentage > 0.7) {
level = 'moderate';
recommendations.push('Consider cache pruning');
recommendations.push('Monitor memory trends');
}
else {
recommendations.push('Memory usage is within normal limits');
}
return {
level,
heapUsagePercentage: heapPercentage * 100,
systemMemoryPercentage: systemPercentage * 100,
recommendations
};
}
async emergencyCleanup() {
const beforeStats = this.getMemoryStats();
const actions = [];
try {
logger.warn('Emergency memory cleanup initiated', {
heapUsed: this.formatBytes(beforeStats.raw.heapStats.used_heap_size),
heapLimit: this.formatBytes(beforeStats.raw.heapStats.heap_size_limit)
});
for (const [name, cache] of this.caches.entries()) {
const beforeSize = cache.getSize();
cache.clear();
actions.push(`Cleared cache '${name}' (${beforeSize} items)`);
}
if (this.grammarManager) {
try {
await this.grammarManager.unloadUnusedGrammars();
actions.push('Cleared grammar manager caches');
}
catch (error) {
logger.warn({ err: error }, 'Failed to clear grammar manager caches');
}
}
if (global.gc) {
global.gc();
actions.push('Forced garbage collection');
}
else {
actions.push('Garbage collection not available (run with --expose-gc)');
}
const requireCache = require.cache;
let clearedModules = 0;
for (const key in requireCache) {
if (key.includes('node_modules') &&
!key.includes('logger') &&
!key.includes('core')) {
delete requireCache[key];
clearedModules++;
}
}
if (clearedModules > 0) {
actions.push(`Cleared ${clearedModules} modules from require cache`);
}
await new Promise(resolve => setTimeout(resolve, 100));
const afterStats = this.getMemoryStats();
const freedMemory = beforeStats.raw.heapStats.used_heap_size - afterStats.raw.heapStats.used_heap_size;
logger.info('Emergency cleanup completed', {
freedMemory: this.formatBytes(freedMemory),
actions: actions.length,
newHeapUsage: this.formatBytes(afterStats.raw.heapStats.used_heap_size)
});
return {
success: true,
freedMemory,
actions
};
}
catch (error) {
logger.error({ err: error }, 'Emergency cleanup failed');
return {
success: false,
freedMemory: 0,
actions,
error: error instanceof Error ? error.message : String(error)
};
}
}
async checkAndExecuteEmergencyCleanup() {
const pressure = this.detectMemoryPressure();
if (pressure.level === 'critical') {
logger.warn('Critical memory pressure detected, executing emergency cleanup', {
heapUsage: pressure.heapUsagePercentage,
systemUsage: pressure.systemMemoryPercentage
});
const result = await this.emergencyCleanup();
if (result.success) {
logger.info('Emergency cleanup successful', {
freedMemory: this.formatBytes(result.freedMemory),
actions: result.actions
});
return true;
}
else {
logger.error('Emergency cleanup failed', {
error: result.error,
actions: result.actions
});
return false;
}
}
return false;
}
createFallbackMemoryStats() {
const totalMemory = os.totalmem();
const freeMemory = os.freemem();
return {
raw: {
totalSystemMemory: totalMemory,
freeSystemMemory: freeMemory,
memoryUsagePercentage: (totalMemory - freeMemory) / totalMemory,
processMemory: {
rss: 0,
heapTotal: 0,
heapUsed: 0,
external: 0,
arrayBuffers: 0
},
heapStats: {
total_heap_size: 0,
total_heap_size_executable: 0,
total_physical_size: 0,
total_available_size: 0,
used_heap_size: 0,
heap_size_limit: 0,
malloced_memory: 0,
peak_malloced_memory: 0,
does_zap_garbage: 0,
number_of_native_contexts: 0,
number_of_detached_contexts: 0,
total_global_handles_size: 0,
used_global_handles_size: 0,
external_memory: 0
},
heapSpaceStats: []
},
formatted: {
totalSystemMemory: this.formatBytes(totalMemory),
freeSystemMemory: this.formatBytes(freeMemory),
usedSystemMemory: this.formatBytes(totalMemory - freeMemory),
memoryUsagePercentage: '0.00%',
memoryStatus: 'normal',
process: {
rss: '0 B',
heapTotal: '0 B',
heapUsed: '0 B',
external: '0 B',
arrayBuffers: '0 B'
},
v8: {
heapSizeLimit: '0 B',
totalHeapSize: '0 B',
usedHeapSize: '0 B',
heapSizeExecutable: '0 B',
mallocedMemory: '0 B',
peakMallocedMemory: '0 B'
},
cache: {
totalSize: '0 B',
cacheCount: 0
},
thresholds: {
highMemoryThreshold: '80%',
criticalMemoryThreshold: '90%'
}
},
cacheStats: [],
grammarStats: {},
timestamp: Date.now()
};
}
}