UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

526 lines 20.5 kB
/** * Enhanced performance metrics collection for logging * Provides comprehensive monitoring and analysis capabilities */ import { randomBytes } from 'node:crypto'; import { loadavg } from 'node:os'; import { generateCorrelationId } from './index.js'; import { getSafeCpuUsage, getSafeMemory } from './safe-process.js'; // Create correlation context function function createCorrelationContext() { const id = generateCorrelationId(); return { id, getId: () => id, }; } import { correlationStorage } from './correlation.js'; // Private CPU usage functions (used internally) function getCpuUsage() { return getSafeCpuUsage(); } function calculateCpuUsage(startUsage, endUsage, timeDelta) { const userDelta = endUsage.user - startUsage.user; const systemDelta = endUsage.system - startUsage.system; const totalDelta = userDelta + systemDelta; // Convert to percentage (microseconds to seconds) return (totalDelta / (timeDelta * 1000)) * 100; } // Lazy logger initialization to avoid circular dependency let _logger = null; function getLogger() { if (!_logger) { // Use dynamic import to avoid circular dependency const loggingModule = import('./index.js'); // For now, create a simple fallback logger _logger = { fatal: () => { }, error: () => { }, warn: () => { }, info: () => { }, http: () => { }, debug: () => { }, trace: () => { }, child: () => _logger, isLevelEnabled: () => false, flush: async () => { }, flushSync: () => { }, end: async () => { }, }; void loggingModule.then(module => { _logger = module.getLogger(); }); } return _logger; } // Create a logger proxy that maintains the same API const logger = new Proxy({}, { get(_target, prop) { const loggerInstance = getLogger(); if (loggerInstance && typeof loggerInstance === 'object' && prop in loggerInstance) { // biome-ignore lint/suspicious/noExplicitAny: Dynamic property access const value = loggerInstance[prop]; return typeof value === 'function' ? value.bind(loggerInstance) : value; } // Return fallback methods that match Logger interface const fallbacks = { fatal: (_msg, ..._args) => { }, error: (_msg, ..._args) => { }, warn: (_msg, ..._args) => { }, info: (_msg, ..._args) => { }, http: (_msg, ..._args) => { }, debug: (_msg, ..._args) => { }, trace: (_msg, ..._args) => { }, child: () => logger, isLevelEnabled: () => false, flush: async () => { }, end: async () => { }, }; return fallbacks[prop] || (() => { }); }, }); /** * Start a performance measurement */ export function startMetrics() { return { startTime: performance.now(), memoryUsage: getSafeMemory(), }; } /** * End a performance measurement and calculate duration */ export function endMetrics(metrics) { const endTime = performance.now(); const duration = endTime - metrics.startTime; return { ...metrics, duration, memoryUsage: getSafeMemory(), }; } /** * Calculate memory usage delta */ export function calculateMemoryDelta(initial, final) { return { heapUsedDelta: final.heapUsed - initial.heapUsed, heapTotalDelta: final.heapTotal - initial.heapTotal, externalDelta: final.external - initial.external, rssDelta: final.rss - initial.rss, }; } /** * Format memory usage for logging */ export function formatMemoryUsage(memory) { return { heapUsed: formatBytes(memory.heapUsed), heapTotal: formatBytes(memory.heapTotal), external: formatBytes(memory.external), rss: formatBytes(memory.rss), }; } /** * Format bytes to human readable string */ export function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } /** * Performance tracking decorator with structured logging integration */ export function trackPerformance(fn, name, options) { const { logLevel = 'debug', trackMemory = true, trackCpu = true, trackArgs = false, thresholds = {}, } = options || {}; return (async (...args) => { const metrics = startMetrics(); const cpuStart = getSafeCpuUsage(); // Create new correlation context for this performance tracking const context = createCorrelationContext(); // Use AsyncLocalStorage.run() directly for proper async context handling return correlationStorage.run(context, async () => { try { const result = await fn(...args); const end = endMetrics(metrics); const cpuEnd = getSafeCpuUsage(); const memoryDelta = calculateMemoryDelta(metrics.memoryUsage || getSafeMemory(), end.memoryUsage || getSafeMemory()); // Calculate CPU usage percentage const userDelta = cpuEnd.user - cpuStart.user; const systemDelta = cpuEnd.system - cpuStart.system; const totalDelta = userDelta + systemDelta; const cpuPercent = (totalDelta / (end.duration * 1000)) * 100; // Prepare performance data const perfData = { functionName: name, duration: `${end.duration.toFixed(2)}ms`, durationMs: end.duration, correlationId: context.id, source: 'performance-tracker', }; if (trackMemory) { perfData.memoryDelta = memoryDelta; perfData.memoryDeltaFormatted = { heapUsed: formatBytes(memoryDelta.heapUsedDelta), heapTotal: formatBytes(memoryDelta.heapTotalDelta), external: formatBytes(memoryDelta.externalDelta), rss: formatBytes(memoryDelta.rssDelta), }; perfData.currentMemory = formatMemoryUsage(end.memoryUsage || getSafeMemory()); } if (trackCpu) { perfData.cpuPercent = `${cpuPercent.toFixed(2)}%`; perfData.cpuUsageRaw = cpuPercent; } if (trackArgs) { perfData.argCount = args.length; perfData.argTypes = args.map(arg => typeof arg); } // Check thresholds and set appropriate log level let finalLogLevel = logLevel; const warnings = []; if (thresholds.duration && end.duration > thresholds.duration) { warnings.push(`Duration threshold exceeded: ${end.duration.toFixed(2)}ms > ${thresholds.duration}ms`); finalLogLevel = 'warn'; } if (thresholds.memory && memoryDelta.heapUsedDelta > thresholds.memory * 1024 * 1024) { warnings.push(`Memory threshold exceeded: ${formatBytes(memoryDelta.heapUsedDelta)} > ${formatBytes(thresholds.memory * 1024 * 1024)}`); finalLogLevel = 'warn'; } if (thresholds.cpu && cpuPercent > thresholds.cpu) { warnings.push(`CPU threshold exceeded: ${cpuPercent.toFixed(2)}% > ${thresholds.cpu}%`); finalLogLevel = 'warn'; } if (warnings.length > 0) { perfData.thresholdWarnings = warnings; } // Log performance metrics with structured logging logger[finalLogLevel](`Performance: ${name}`, perfData); return result; } catch (error) { const end = endMetrics(metrics); const memoryDelta = calculateMemoryDelta(metrics.memoryUsage || getSafeMemory(), end.memoryUsage || getSafeMemory()); const cpuEnd = getCpuUsage(); const cpuPercent = calculateCpuUsage(cpuStart, cpuEnd, end.duration); // Log error performance metrics logger.error(`Performance Error: ${name}`, { functionName: name, duration: `${end.duration.toFixed(2)}ms`, durationMs: end.duration, error: error instanceof Error ? error.message : error, errorType: error instanceof Error ? error.constructor.name : typeof error, memoryDelta: trackMemory ? memoryDelta : undefined, cpuPercent: trackCpu ? `${cpuPercent.toFixed(2)}%` : undefined, argCount: trackArgs ? args.length : undefined, correlationId: context.id, source: 'performance-tracker-error', }); throw error; } }); }); } /** * Enhanced function execution time measurement with structured logging */ export async function measureTime(fn, label, options) { const { logPerformance = true, trackMemory = true, trackCpu = false, thresholds = {}, } = options || {}; const start = performance.now(); const memoryStart = getSafeMemory(); const cpuStart = trackCpu ? getSafeCpuUsage() : undefined; // Create new correlation context for this measurement const context = createCorrelationContext(); // Use AsyncLocalStorage.run() directly for proper async context handling return correlationStorage.run(context, async () => { try { const result = await fn(); const duration = performance.now() - start; const memoryEnd = getSafeMemory(); const cpuEnd = trackCpu ? getSafeCpuUsage() : undefined; let memoryDelta; let cpuUsage; if (trackMemory) { memoryDelta = calculateMemoryDelta(memoryStart, memoryEnd); } if (trackCpu && cpuStart && cpuEnd) { // Calculate CPU usage percentage const userDelta = cpuEnd.user - cpuStart.user; const systemDelta = cpuEnd.system - cpuStart.system; const totalDelta = userDelta + systemDelta; cpuUsage = (totalDelta / (duration * 1000)) * 100; } if (logPerformance) { const perfData = { label: label || 'Anonymous function', duration: `${duration.toFixed(2)}ms`, durationMs: duration, correlationId: context.id, source: 'measure-time', }; if (memoryDelta) { perfData.memoryDelta = memoryDelta; perfData.memoryDeltaFormatted = { heapUsed: formatBytes(memoryDelta.heapUsedDelta), heapTotal: formatBytes(memoryDelta.heapTotalDelta), external: formatBytes(memoryDelta.externalDelta), rss: formatBytes(memoryDelta.rssDelta), }; } if (cpuUsage !== undefined) { perfData.cpuUsage = `${cpuUsage.toFixed(2)}%`; perfData.cpuUsageRaw = cpuUsage; } // Check thresholds and adjust log level let logLevel = 'debug'; const warnings = []; if (thresholds.duration && duration > thresholds.duration) { warnings.push(`Duration threshold exceeded: ${duration.toFixed(2)}ms > ${thresholds.duration}ms`); logLevel = 'warn'; } if (thresholds.memory && memoryDelta && memoryDelta.heapUsedDelta > thresholds.memory * 1024 * 1024) { warnings.push(`Memory threshold exceeded: ${formatBytes(memoryDelta.heapUsedDelta)} > ${formatBytes(thresholds.memory * 1024 * 1024)}`); logLevel = 'warn'; } if (warnings.length > 0) { perfData.thresholdWarnings = warnings; } logger[logLevel](`Measured function execution`, perfData); } return { result, duration, memoryDelta, cpuUsage }; } catch (error) { const duration = performance.now() - start; logger.error(`Function execution measurement failed`, { label: label || 'Anonymous function', duration: `${duration.toFixed(2)}ms`, durationMs: duration, error: error instanceof Error ? error.message : error, errorType: error instanceof Error ? error.constructor.name : typeof error, correlationId: context.id, source: 'measure-time-error', }); throw error; } }); } /** * Enhanced memory threshold checking with structured logging */ export function checkMemoryThresholds(memory, options) { const { heapUsagePercentThreshold = 0.8, heapUsageAbsoluteThreshold = 512, logLevel = 'warn', correlationId = generateCorrelationId(), } = options || {}; const heapUsedMB = memory.heapUsed / 1024 / 1024; const heapTotalMB = memory.heapTotal / 1024 / 1024; const externalMB = memory.external / 1024 / 1024; const rssMB = memory.rss / 1024 / 1024; const heapUsagePercent = heapUsedMB / heapTotalMB; const warnings = []; let isHealthy = true; // Check heap usage percentage if (heapUsagePercent > heapUsagePercentThreshold) { warnings.push(`High heap usage percentage: ${(heapUsagePercent * 100).toFixed(1)}% > ${(heapUsagePercentThreshold * 100).toFixed(1)}%`); isHealthy = false; } // Check absolute heap usage if (heapUsedMB > heapUsageAbsoluteThreshold) { warnings.push(`High absolute heap usage: ${heapUsedMB.toFixed(2)}MB > ${heapUsageAbsoluteThreshold}MB`); isHealthy = false; } // Check external memory usage if (externalMB > 256) { warnings.push(`High external memory usage: ${externalMB.toFixed(2)}MB`); isHealthy = false; } // Check RSS (Resident Set Size) if (rssMB > 1024) { warnings.push(`High RSS usage: ${rssMB.toFixed(2)}MB`); isHealthy = false; } const metrics = { heapUsedMB, heapTotalMB, heapUsagePercent, externalMB, rssMB, }; // Log with structured logging if (!isHealthy) { logger[logLevel]('Memory threshold warnings detected', { metrics, warnings, thresholds: { heapUsagePercent: `${(heapUsagePercentThreshold * 100).toFixed(1)}%`, heapUsageAbsolute: `${heapUsageAbsoluteThreshold}MB`, external: '256MB', rss: '1024MB', }, correlationId, source: 'memory-threshold-check', }); } else { logger.debug('Memory usage is healthy', { metrics, correlationId, source: 'memory-threshold-check', }); } return { isHealthy, warnings, metrics }; } /** * Take a comprehensive system performance snapshot */ export function takePerformanceSnapshot(options) { const correlationId = options?.correlationId || generateCorrelationId(); const includeCpu = options?.includeCpu !== false; // Capture memory once to avoid double call and ensure consistency const memory = getSafeMemory(); const uptime = process.uptime(); const snapshot = { timestamp: new Date().toISOString(), memory, memoryFormatted: formatMemoryUsage(memory), cpu: includeCpu ? getSafeCpuUsage() : { user: 0, system: 0 }, uptime, uptimeFormatted: formatUptime(uptime), loadAverage: loadavg(), platform: process.platform, arch: process.arch, nodeVersion: process.version, pid: process.pid, correlationId, }; logger.debug('Performance snapshot taken', { timestamp: snapshot.timestamp, memoryFormatted: snapshot.memoryFormatted, uptimeFormatted: snapshot.uptimeFormatted, loadAverage: snapshot.loadAverage, correlationId, source: 'performance-snapshot', }); return snapshot; } /** * Format uptime to human readable string */ function formatUptime(seconds) { const days = Math.floor(seconds / (24 * 3600)); const hours = Math.floor((seconds % (24 * 3600)) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); const parts = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (secs > 0 || parts.length === 0) parts.push(`${secs}s`); return parts.join(' '); } /** * Performance monitor class for ongoing tracking (used internally) */ class PerformanceMonitor { measurements = new Map(); maxMeasurements; constructor(maxMeasurements = 100) { this.maxMeasurements = maxMeasurements; } /** * Start measuring an operation */ start(operation) { const id = `${operation}_${Date.now()}_${randomBytes(8).toString('hex')}`; const metrics = startMetrics(); if (!this.measurements.has(operation)) { this.measurements.set(operation, []); } const operationMeasurements = this.measurements.get(operation); if (!operationMeasurements) { this.measurements.set(operation, []); return this.start(operation); // Retry with empty array } operationMeasurements.push({ ...metrics, id }); // Keep only the last N measurements if (operationMeasurements.length > this.maxMeasurements) { operationMeasurements.shift(); } return id; } /** * End measuring an operation */ end(operation, id) { const operationMeasurements = this.measurements.get(operation); if (!operationMeasurements) return null; const index = operationMeasurements.findIndex(m => 'id' in m && m.id === id); if (index === -1) return null; const metrics = operationMeasurements[index]; const end = endMetrics(metrics); // Update the measurement operationMeasurements[index] = end; return end; } /** * Get statistics for an operation */ getStats(operation) { const operationMeasurements = this.measurements.get(operation); if (!operationMeasurements || operationMeasurements.length === 0) { return null; } const durations = operationMeasurements .filter((m) => 'duration' in m) .map(m => m.duration); if (durations.length === 0) return null; return { count: durations.length, avgDuration: durations.reduce((a, b) => a + b, 0) / durations.length, minDuration: Math.min(...durations), maxDuration: Math.max(...durations), totalDuration: durations.reduce((a, b) => a + b, 0), }; } /** * Get all operation statistics */ getAllStats() { const stats = {}; for (const operation of this.measurements.keys()) { stats[operation] = this.getStats(operation); } return stats; } /** * Clear all measurements */ clear() { this.measurements.clear(); } /** * Clear measurements for a specific operation */ clearOperation(operation) { this.measurements.delete(operation); } } /** * Global performance monitor instance */ export const globalPerformanceMonitor = new PerformanceMonitor(); //# sourceMappingURL=performance.js.map