UNPKG

@digitalnodecom/node-red-contrib-analyzer

Version:

A Node-RED global service that monitors function nodes for debugging artifacts and performance issues. Features real-time quality metrics, Vue.js dashboard, and comprehensive code analysis.

325 lines (283 loc) 11.5 kB
const os = require('os'); const PerformanceDatabase = require('../database/performance-db'); class PerformanceMonitor { constructor() { this.isMonitoring = false; this.config = { cpuThreshold: 75, // CPU usage percentage memoryThreshold: 80, // Memory usage percentage eventLoopThreshold: 20, // Event loop lag in milliseconds sustainedAlertDuration: 5 * 60 * 1000, // 5 minutes in milliseconds alertCooldown: 30 * 60 * 1000, // 30 minutes cooldown between alerts dbRetentionDays: 7, // Keep data for 7 days pruneInterval: 24 * 60 * 60 * 1000 // Prune every 24 hours }; this.lastCpuUsage = process.cpuUsage(); this.lastCheck = Date.now(); this.lastAlertTimes = { cpu: 0, memory: 0, eventLoop: 0 }; this.lastPruneTime = 0; this.pruningScheduled = false; this.db = new PerformanceDatabase(); this.db.setRetentionDays(this.config.dbRetentionDays); } // Start monitoring performance metrics start() { this.isMonitoring = true; this.collectMetrics(); // Schedule pruning only once at startup if (!this.pruningScheduled) { this.schedulePeriodicPruning(); this.pruningScheduled = true; } } // Stop monitoring stop() { this.isMonitoring = false; if (this.db) { this.db.close(); } } // Schedule periodic pruning of old data schedulePeriodicPruning() { const now = Date.now(); if (now - this.lastPruneTime > this.config.pruneInterval) { this.pruneOldData(); this.lastPruneTime = now; } // Schedule next pruning check in 1 hour (skip in test environment) if (process.env.NODE_ENV !== 'test') { setTimeout(() => { if (this.isMonitoring && this.pruningScheduled) { this.schedulePeriodicPruning(); } }, 60 * 60 * 1000); // 1 hour } } // Prune old data using the database class async pruneOldData() { try { const result = await this.db.pruneOldData(this.config.dbRetentionDays); // eslint-disable-next-line no-console console.log('Performance monitoring:', result.message); } catch (error) { // eslint-disable-next-line no-console console.error('Error pruning old performance data:', error); } } // Collect current performance metrics collectMetrics() { const now = Date.now(); const currentCpuUsage = process.cpuUsage(); const memoryUsage = process.memoryUsage(); // Calculate CPU usage percentage const cpuPercent = this.calculateCpuPercent(currentCpuUsage); // Calculate memory usage percentage const memoryPercent = this.calculateMemoryPercent(memoryUsage); // Measure event loop lag const eventLoopStart = process.hrtime(); setImmediate(() => { if (!this.isMonitoring) return; // Exit if monitoring stopped const eventLoopLag = process.hrtime(eventLoopStart); const lagMs = eventLoopLag[0] * 1000 + eventLoopLag[1] / 1000000; // Store all metrics in database this.storeMetricsInDb(cpuPercent, memoryPercent, memoryUsage.rss, lagMs); }); // Update last values this.lastCpuUsage = currentCpuUsage; this.lastCheck = now; return { cpu: cpuPercent, memory: memoryPercent, memoryDetails: memoryUsage, timestamp: now }; } // Store metrics in database async storeMetricsInDb(cpuUsage, memoryUsage, memoryRss, eventLoopLag) { try { await this.db.storeMetrics(cpuUsage, memoryUsage, memoryRss, eventLoopLag); } catch (error) { console.error('Error storing performance metrics:', error); } } // Calculate CPU usage percentage calculateCpuPercent(currentCpuUsage) { const timeDiff = Date.now() - this.lastCheck; if (timeDiff === 0) return 0; const userDiff = currentCpuUsage.user - this.lastCpuUsage.user; const systemDiff = currentCpuUsage.system - this.lastCpuUsage.system; const totalDiff = userDiff + systemDiff; // Convert microseconds to percentage const cpuPercent = (totalDiff / (timeDiff * 1000)) * 100; return Math.min(Math.max(cpuPercent, 0), 100); } // Calculate memory usage percentage calculateMemoryPercent(memoryUsage) { // Use RSS (Resident Set Size) as the primary memory metric const totalMemory = os.totalmem(); return (memoryUsage.rss / totalMemory) * 100; } // Get current performance summary with time-based alerting async getPerformanceSummary() { const current = this.collectMetrics(); const averages = await this.calculateAverages(); const alerts = await this.checkSustainedThresholds(current); return { current, averages, alerts, uptime: this.getUptime(), processInfo: this.getProcessInfo() }; } // Calculate averages from database for recent metrics async calculateAverages(windowMinutes = 10) { try { return await this.db.getAverages(windowMinutes); } catch (error) { console.error('Error calculating averages:', error); return { cpu: 0, memory: 0, eventLoop: 0 }; } } // Check if metrics have been above thresholds for sustained periods async checkSustainedThresholds(current) { const alerts = []; const now = Date.now(); // Get average values for the sustained period duration const durationMinutes = this.config.sustainedAlertDuration / 60000; const averages = await this.calculateAverages(durationMinutes); // Check CPU sustained threshold if (averages.cpu > this.config.cpuThreshold) { const sustainedResult = await this.checkSustainedMetric('cpu_usage', this.config.cpuThreshold, this.config.sustainedAlertDuration); if (sustainedResult.sustained && (now - this.lastAlertTimes.cpu) > this.config.alertCooldown) { alerts.push({ type: 'cpu', message: `High CPU usage sustained for ${durationMinutes.toFixed(1)} minutes: ${averages.cpu.toFixed(1)}%`, severity: 'warning', threshold: this.config.cpuThreshold, current: averages.cpu, sustainedDuration: this.config.sustainedAlertDuration }); this.lastAlertTimes.cpu = now; await this.recordAlert('cpu', this.config.cpuThreshold, averages.cpu, durationMinutes); } } // Check Memory sustained threshold if (averages.memory > this.config.memoryThreshold) { const sustainedResult = await this.checkSustainedMetric('memory_usage', this.config.memoryThreshold, this.config.sustainedAlertDuration); if (sustainedResult.sustained && (now - this.lastAlertTimes.memory) > this.config.alertCooldown) { alerts.push({ type: 'memory', message: `High memory usage sustained for ${durationMinutes.toFixed(1)} minutes: ${averages.memory.toFixed(1)}%`, severity: 'warning', threshold: this.config.memoryThreshold, current: averages.memory, sustainedDuration: this.config.sustainedAlertDuration }); this.lastAlertTimes.memory = now; await this.recordAlert('memory', this.config.memoryThreshold, averages.memory, durationMinutes); } } return alerts; } // Check if a specific metric has been above threshold for sustained duration async checkSustainedMetric(metricColumn, threshold, duration) { try { return await this.db.checkSustainedMetric(metricColumn, threshold, duration); } catch (error) { console.error('Error checking sustained metric:', error); return { sustained: false, ratio: 0, totalReadings: 0, exceedingReadings: 0 }; } } // Record alert in database async recordAlert(metricType, threshold, currentValue, durationMinutes) { try { await this.db.recordAlert(metricType, threshold, currentValue, durationMinutes); } catch (error) { console.error('Error recording alert:', error); } } // Get process uptime getUptime() { const uptime = process.uptime(); const hours = Math.floor(uptime / 3600); const minutes = Math.floor((uptime % 3600) / 60); const seconds = Math.floor(uptime % 60); return { seconds: uptime, formatted: `${hours}h ${minutes}m ${seconds}s` }; } // Get process information getProcessInfo() { return { pid: process.pid, nodeVersion: process.version, platform: process.platform, arch: process.arch, title: process.title }; } // Update configuration updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } // Get recent metrics from database async getRecentMetrics(metricType, count = 10) { try { return await this.db.getRecentMetrics(metricType, count); } catch (error) { console.error('Error getting recent metrics:', error); return []; } } // Clear all metrics history async clearHistory() { try { await this.db.clearAll(); } catch (error) { console.error('Error clearing metrics history:', error); } } // Check if performance monitoring is healthy async isHealthy() { const summary = await this.getPerformanceSummary(); return summary.alerts.length === 0; } // Get performance trend from database async getTrend(metricType, windowMinutes = 60) { try { return await this.db.getTrend(metricType, windowMinutes); } catch (error) { console.error('Error calculating trend:', error); return 'insufficient_data'; } } // Get alert history from database async getAlertHistory(limitCount = 50) { try { return await this.db.getAlertHistory(limitCount); } catch (error) { console.error('Error getting alert history:', error); return []; } } // Get database statistics async getDatabaseStats() { try { return await this.db.getStats(); } catch (error) { console.error('Error getting database stats:', error); return null; } } // Get database path getDatabasePath() { return this.db ? this.db.dbPath : null; } } module.exports = PerformanceMonitor;