UNPKG

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
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() }; } }