/**
* Global memory management for resource-intensive operations
*
* Provides memory reservation and tracking to prevent OOM (Out of Memory) errors.
* Enforces global 100MB limit with per-operation 10MB limits.
*
* @module memoryManager
*/
import { RESOURCE_LIMITS } from '../constants.js';
/**
* Current memory usage statistics
*/
export interface MemoryUsage {
/** Currently used memory in bytes */
used: number;
/** Total memory limit in bytes */
limit: number;
/** Available memory in bytes */
available: number;
/** Percentage of memory used (0-100) */
percentage: number;
}
/**
* Memory reservation record
*/
export interface MemoryReservation {
/** Unique reservation ID */
id: string;
/** Reserved memory size in bytes */
size: number;
/** Timestamp when reservation was created */
timestamp: number;
/** Tool name that made the reservation */
toolName: string;
}
/**
* Memory manager singleton class
* Tracks and enforces memory limits across all operations
*/
class MemoryManager {
private reservations: Map<string, MemoryReservation> = new Map();
private readonly maxMemoryBytes: number;
constructor(maxMemoryBytes = RESOURCE_LIMITS.GLOBAL_MEMORY_LIMIT_BYTES) {
this.maxMemoryBytes = maxMemoryBytes;
}
/**
* Reserve memory for an operation
*
* @param size - Amount of memory to reserve in bytes
* @param toolName - Name of the tool making the reservation
* @returns Reservation ID if successful, null if insufficient memory
*
* @example
* ```typescript
* const id = memoryManager.reserve(10 * 1024 * 1024, 'local_ripgrep');
* if (!id) {
* throw new Error('Insufficient memory');
* }
* try {
* // Perform operation
* } finally {
* memoryManager.release(id);
* }
* ```
*/
reserve(size: number, toolName: string): string | null {
const currentUsage = this.getCurrentUsage();
if (currentUsage.used + size > this.maxMemoryBytes) {
return null; // Insufficient memory
}
const id = `${toolName}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
this.reservations.set(id, {
id,
size,
timestamp: Date.now(),
toolName
});
return id;
}
/**
* Release a memory reservation
*
* @param id - Reservation ID to release
* @returns true if reservation was found and released, false otherwise
*/
release(id: string): boolean {
return this.reservations.delete(id);
}
/**
* Get current memory usage statistics
*
* @returns Current memory usage information
*/
getCurrentUsage(): MemoryUsage {
const used = Array.from(this.reservations.values())
.reduce((sum, res) => sum + res.size, 0);
return {
used,
limit: this.maxMemoryBytes,
available: this.maxMemoryBytes - used,
percentage: (used / this.maxMemoryBytes) * 100
};
}
/**
* Get all reservations for a specific tool
*
* @param toolName - Tool name to filter by
* @returns Array of reservations for the tool
*/
getReservationsByTool(toolName: string): MemoryReservation[] {
return Array.from(this.reservations.values())
.filter(res => res.toolName === toolName);
}
/**
* Clean up stale reservations (older than 5 minutes)
* Automatically called periodically to prevent memory leaks
*
* @returns Number of reservations cleaned up
*/
cleanup(): number {
const now = Date.now();
const staleThreshold = RESOURCE_LIMITS.MEMORY_RESERVATION_TIMEOUT_MS;
let cleaned = 0;
for (const [id, reservation] of this.reservations) {
if (now - reservation.timestamp > staleThreshold) {
this.reservations.delete(id);
cleaned++;
}
}
return cleaned;
}
/**
* Get detailed memory statistics
*
* @returns Detailed statistics about memory usage by tool
*/
getDetailedStats(): Record<string, { count: number; totalBytes: number }> {
const stats: Record<string, { count: number; totalBytes: number }> = {};
for (const reservation of this.reservations.values()) {
if (!stats[reservation.toolName]) {
stats[reservation.toolName] = { count: 0, totalBytes: 0 };
}
stats[reservation.toolName].count++;
stats[reservation.toolName].totalBytes += reservation.size;
}
return stats;
}
/**
* Format memory usage as human-readable string
*
* @param bytes - Number of bytes
* @returns Formatted string (e.g., "10.5 MB")
*/
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
/**
* Get formatted memory usage report
*
* @returns Human-readable memory usage report
*/
getUsageReport(): string {
const usage = this.getCurrentUsage();
const stats = this.getDetailedStats();
const lines = [
'Memory Usage Report',
'='.repeat(50),
`Total: ${this.formatBytes(usage.used)} / ${this.formatBytes(usage.limit)} (${usage.percentage.toFixed(1)}%)`,
`Available: ${this.formatBytes(usage.available)}`,
`Reservations: ${this.reservations.size}`,
''
];
if (Object.keys(stats).length > 0) {
lines.push('By Tool:');
for (const [toolName, data] of Object.entries(stats)) {
lines.push(` ${toolName}: ${data.count} reservations, ${this.formatBytes(data.totalBytes)}`);
}
} else {
lines.push('No active reservations');
}
return lines.join('\n');
}
}
/**
* Global memory manager instance
* Singleton to ensure consistent memory tracking across the application
*/
export const memoryManager = new MemoryManager();
/**
* Auto-cleanup stale reservations every minute
* Prevents memory leaks from operations that don't properly release
*/
setInterval(() => {
const cleaned = memoryManager.cleanup();
if (cleaned > 0) {
console.log(`[MemoryManager] Cleaned ${cleaned} stale reservations`);
}
}, 60 * 1000);
/**
* Helper function to execute an operation with memory reservation
*
* @param size - Memory to reserve in bytes
* @param toolName - Name of the tool
* @param operation - Async function to execute
* @returns Result of the operation
* @throws Error if insufficient memory or operation fails
*
* @example
* ```typescript
* const result = await withMemoryReservation(
* 10 * 1024 * 1024,
* 'local_ripgrep',
* async () => {
* return await performExpensiveOperation();
* }
* );
* ```
*/
export async function withMemoryReservation<T>(
size: number,
toolName: string,
operation: () => Promise<T>
): Promise<T> {
const reservationId = memoryManager.reserve(size, toolName);
if (!reservationId) {
const usage = memoryManager.getCurrentUsage();
throw new Error(
`Insufficient memory: ${Math.round(usage.used / 1024 / 1024)}MB / ${Math.round(usage.limit / 1024 / 1024)}MB used (${Math.round(usage.percentage)}%)`
);
}
try {
return await operation();
} finally {
memoryManager.release(reservationId);
}
}