import si from 'systeminformation'; import { Mutex } from "async-mutex"; import os from 'os'; import fs from 'fs'; import { logger } from '../lib/logger'; const IS_KUBERNETES = process.env.IS_KUBERNETES === "true"; const MAX_CPU = process.env.MAX_CPU ? parseFloat(process.env.MAX_CPU) : 0.8; const MAX_RAM = process.env.MAX_RAM ? parseFloat(process.env.MAX_RAM) : 0.8; const CACHE_DURATION = process.env.SYS_INFO_MAX_CACHE_DURATION ? parseFloat(process.env.SYS_INFO_MAX_CACHE_DURATION) : 150; class SystemMonitor { private static instance: SystemMonitor; private static instanceMutex = new Mutex(); private cpuUsageCache: number | null = null; private memoryUsageCache: number | null = null; private lastCpuCheck: number = 0; private lastMemoryCheck: number = 0; // Variables for CPU usage calculation private previousCpuUsage: number = 0; private previousTime: number = Date.now(); private constructor() {} public static async getInstance(): Promise { if (SystemMonitor.instance) { return SystemMonitor.instance; } await this.instanceMutex.runExclusive(async () => { if (!SystemMonitor.instance) { SystemMonitor.instance = new SystemMonitor(); } }); return SystemMonitor.instance; } public async checkMemoryUsage() { if (IS_KUBERNETES) { return this._checkMemoryUsageKubernetes(); } return this._checkMemoryUsage(); } private readMemoryCurrent(): number { const data = fs.readFileSync('/sys/fs/cgroup/memory.current', 'utf8'); return parseInt(data.trim(), 10); } private readMemoryMax(): number { const data = fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim(); if (data === 'max') { return Infinity; } return parseInt(data, 10); } private async _checkMemoryUsageKubernetes() { try { const currentMemoryUsage = this.readMemoryCurrent(); const memoryLimit = this.readMemoryMax(); let memoryUsagePercentage: number; if (memoryLimit === Infinity) { // No memory limit set; use total system memory const totalMemory = os.totalmem(); memoryUsagePercentage = currentMemoryUsage / totalMemory; } else { memoryUsagePercentage = currentMemoryUsage / memoryLimit; } // console.log("Memory usage:", memoryUsagePercentage); return memoryUsagePercentage; } catch (error) { logger.error(`Error calculating memory usage: ${error}`); return 0; // Fallback to 0% usage } } private async _checkMemoryUsage() { const now = Date.now(); if (this.memoryUsageCache !== null && (now - this.lastMemoryCheck) < CACHE_DURATION) { return this.memoryUsageCache; } const memoryData = await si.mem(); const totalMemory = memoryData.total; const availableMemory = memoryData.available; const usedMemory = totalMemory - availableMemory; const usedMemoryPercentage = (usedMemory / totalMemory); this.memoryUsageCache = usedMemoryPercentage; this.lastMemoryCheck = now; return usedMemoryPercentage; } public async checkCpuUsage() { if (IS_KUBERNETES) { return this._checkCpuUsageKubernetes(); } return this._checkCpuUsage(); } private readCpuUsage(): number { const data = fs.readFileSync('/sys/fs/cgroup/cpu.stat', 'utf8'); const match = data.match(/^usage_usec (\d+)$/m); if (match) { return parseInt(match[1], 10); } throw new Error('Could not read usage_usec from cpu.stat'); } private getNumberOfCPUs(): number { let cpus: number[] = []; try { const cpusetPath = '/sys/fs/cgroup/cpuset.cpus.effective'; const data = fs.readFileSync(cpusetPath, 'utf8').trim(); if (!data) { throw new Error(`${cpusetPath} is empty.`); } cpus = this.parseCpuList(data); if (cpus.length === 0) { throw new Error('No CPUs found in cpuset.cpus.effective'); } } catch (error) { logger.warn(`Unable to read cpuset.cpus.effective, defaulting to OS CPUs: ${error}`); cpus = os.cpus().map((cpu, index) => index); } return cpus.length; } private parseCpuList(cpuList: string): number[] { const ranges = cpuList.split(','); const cpus: number[] = []; ranges.forEach((range) => { const [startStr, endStr] = range.split('-'); const start = parseInt(startStr, 10); const end = endStr !== undefined ? parseInt(endStr, 10) : start; for (let i = start; i <= end; i++) { cpus.push(i); } }); return cpus; } private async _checkCpuUsageKubernetes() { try { const usage = this.readCpuUsage(); // In microseconds (µs) const now = Date.now(); // Check if it's the first run if (this.previousCpuUsage === 0) { // Initialize previous values this.previousCpuUsage = usage; this.previousTime = now; // Return 0% CPU usage on first run return 0; } const deltaUsage = usage - this.previousCpuUsage; // In µs const deltaTime = (now - this.previousTime) * 1000; // Convert ms to µs const numCPUs = this.getNumberOfCPUs(); // Get the number of CPUs // Calculate the CPU usage percentage and normalize by the number of CPUs const cpuUsagePercentage = (deltaUsage / deltaTime) / numCPUs; // Update previous values this.previousCpuUsage = usage; this.previousTime = now; // console.log("CPU usage:", cpuUsagePercentage); return cpuUsagePercentage; } catch (error) { logger.error(`Error calculating CPU usage: ${error}`); return 0; // Fallback to 0% usage } } private async _checkCpuUsage() { const now = Date.now(); if (this.cpuUsageCache !== null && (now - this.lastCpuCheck) < CACHE_DURATION) { return this.cpuUsageCache; } const cpuData = await si.currentLoad(); const cpuLoad = cpuData.currentLoad / 100; this.cpuUsageCache = cpuLoad; this.lastCpuCheck = now; return cpuLoad; } public async acceptConnection() { const cpuUsage = await this.checkCpuUsage(); const memoryUsage = await this.checkMemoryUsage(); return cpuUsage < MAX_CPU && memoryUsage < MAX_RAM; } public clearCache() { this.cpuUsageCache = null; this.memoryUsageCache = null; this.lastCpuCheck = 0; this.lastMemoryCheck = 0; } } export default SystemMonitor.getInstance();