pragma Singleton import Qt.labs.folderlistmodel import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services.UI Singleton { id: root // Component registration - only poll when something needs system stat data function registerComponent(componentId) { root._registered[componentId] = true; root._registered = Object.assign({}, root._registered); Logger.d("SystemStat", "Component registered:", componentId, "- total:", root._registeredCount); } function unregisterComponent(componentId) { delete root._registered[componentId]; root._registered = Object.assign({}, root._registered); Logger.d("SystemStat", "Component unregistered:", componentId, "- total:", root._registeredCount); } property var _registered: ({}) readonly property int _registeredCount: Object.keys(_registered).length readonly property bool _lockScreenActive: PanelService.lockScreen?.active ?? false readonly property bool shouldRun: _registeredCount > 0 && !_lockScreenActive // Polling intervals (hardcoded to sensible values per stat type) readonly property int cpuUsageIntervalMs: 1000 readonly property int cpuFreqIntervalMs: 3000 readonly property int memIntervalMs: 5000 readonly property int networkIntervalMs: 3000 readonly property int loadAvgIntervalMs: 10000 readonly property int diskIntervalMs: 30000 readonly property int gpuIntervalMs: 5000 // Public values property real cpuUsage: 0 property list coresUsage: [] property real cpuTemp: 0 property string cpuFreq: "0.0GHz" property real cpuFreqRatio: 0 property real cpuGlobalMaxFreq: 3.5 property real gpuTemp: 0 property bool gpuAvailable: false property string gpuType: "" // "amd", "intel", "nvidia" property real memGb: 0 property real memPercent: 0 property real memTotalGb: 0 property real swapGb: 0 property real swapPercent: 0 property real swapTotalGb: 0 property var diskPercents: ({}) property var diskAvailPercents: ({}) // available disk space in percent property var diskUsedGb: ({}) // Used space in GB per mount point property var diskAvailableGb: ({}) // available space in GB per mount point property var diskSizeGb: ({}) // Total size in GB per mount point property real rxSpeed: 0 property real txSpeed: 0 property real zfsArcSizeKb: 0 // ZFS ARC cache size in KB property real zfsArcCminKb: 0 // ZFS ARC minimum (non-reclaimable) size in KB property real loadAvg1: 0 property real loadAvg5: 0 property real loadAvg15: 0 property int nproc: 0 // Number of cpu cores // History arrays (2 minutes of data, length computed from polling interval) // Pre-filled with zeros so the graph scrolls smoothly from the start readonly property int historyDurationMs: (1 * 60 * 1000) // 1 minute // Computed history lengths based on polling intervals readonly property int cpuHistoryLength: Math.ceil(historyDurationMs / cpuUsageIntervalMs) readonly property int gpuHistoryLength: Math.ceil(historyDurationMs / gpuIntervalMs) readonly property int memHistoryLength: Math.ceil(historyDurationMs / memIntervalMs) readonly property int diskHistoryLength: Math.max(10, Math.ceil(historyDurationMs / diskIntervalMs)) readonly property int networkHistoryLength: Math.ceil(historyDurationMs / networkIntervalMs) property var cpuHistory: new Array(cpuHistoryLength).fill(0) property var cpuTempHistory: new Array(cpuHistoryLength).fill(40) // Reasonable default temp property var gpuTempHistory: new Array(gpuHistoryLength).fill(40) // Reasonable default temp property var memHistory: new Array(memHistoryLength).fill(0) property var diskHistories: ({}) // Keyed by mount path, initialized on first update property var rxSpeedHistory: new Array(networkHistoryLength).fill(0) property var txSpeedHistory: new Array(networkHistoryLength).fill(0) // Historical min/max tracking (since shell started) for consistent graph scaling // Temperature defaults create a valid 30-80°C range that expands as real data comes in property real cpuTempHistoryMin: 30 property real cpuTempHistoryMax: 80 property real gpuTempHistoryMin: 30 property real gpuTempHistoryMax: 80 // Network uses autoscaling from current history window // Disk is always 0-100% // History management - called from update functions, not change handlers // (change handlers don't fire when value stays the same) function pushCpuHistory() { let h = cpuHistory.slice(); h.push(cpuUsage); if (h.length > cpuHistoryLength) h.shift(); cpuHistory = h; } function pushCpuTempHistory() { if (cpuTemp > 0) { if (cpuTemp < cpuTempHistoryMin) cpuTempHistoryMin = cpuTemp; if (cpuTemp > cpuTempHistoryMax) cpuTempHistoryMax = cpuTemp; } let h = cpuTempHistory.slice(); h.push(cpuTemp); if (h.length > cpuHistoryLength) h.shift(); cpuTempHistory = h; } function pushGpuHistory() { if (gpuTemp > 0) { if (gpuTemp < gpuTempHistoryMin) gpuTempHistoryMin = gpuTemp; if (gpuTemp > gpuTempHistoryMax) gpuTempHistoryMax = gpuTemp; } let h = gpuTempHistory.slice(); h.push(gpuTemp); if (h.length > gpuHistoryLength) h.shift(); gpuTempHistory = h; } function pushMemHistory() { let h = memHistory.slice(); h.push(memPercent); if (h.length > memHistoryLength) h.shift(); memHistory = h; } function pushDiskHistory() { let newHistories = {}; for (let path in diskPercents) { // Pre-fill with zeros if this is a new path let h = diskHistories[path] ? diskHistories[path].slice() : new Array(diskHistoryLength).fill(0); h.push(diskPercents[path]); if (h.length > diskHistoryLength) h.shift(); newHistories[path] = h; } diskHistories = newHistories; } function pushNetworkHistory() { let rxH = rxSpeedHistory.slice(); rxH.push(rxSpeed); if (rxH.length > networkHistoryLength) rxH.shift(); rxSpeedHistory = rxH; let txH = txSpeedHistory.slice(); txH.push(txSpeed); if (txH.length > networkHistoryLength) txH.shift(); txSpeedHistory = txH; } // Network max speed tracking (autoscales from current history window) // Minimum floor of 1 MB/s so graph doesn't fluctuate at low speeds readonly property real rxMaxSpeed: { const max = Math.max(...rxSpeedHistory); return Math.max(max, 1000000); // 1 MB/s floor } readonly property real txMaxSpeed: { const max = Math.max(...txSpeedHistory); return Math.max(max, 512000); // 512 KB/s floor } // Ready-to-use ratios based on current maximums (0..1 range) readonly property real rxRatio: rxMaxSpeed > 0 ? Math.min(1, rxSpeed / rxMaxSpeed) : 0 readonly property real txRatio: txMaxSpeed > 0 ? Math.min(1, txSpeed / txMaxSpeed) : 0 // Color resolution (respects useCustomColors setting) readonly property color warningColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor || Color.mTertiary) : Color.mTertiary readonly property color criticalColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError // Threshold values from settings readonly property int cpuWarningThreshold: Settings.data.systemMonitor.cpuWarningThreshold readonly property int cpuCriticalThreshold: Settings.data.systemMonitor.cpuCriticalThreshold readonly property int tempWarningThreshold: Settings.data.systemMonitor.tempWarningThreshold readonly property int tempCriticalThreshold: Settings.data.systemMonitor.tempCriticalThreshold readonly property int gpuWarningThreshold: Settings.data.systemMonitor.gpuWarningThreshold readonly property int gpuCriticalThreshold: Settings.data.systemMonitor.gpuCriticalThreshold readonly property int memWarningThreshold: Settings.data.systemMonitor.memWarningThreshold readonly property int memCriticalThreshold: Settings.data.systemMonitor.memCriticalThreshold readonly property int swapWarningThreshold: Settings.data.systemMonitor.swapWarningThreshold readonly property int swapCriticalThreshold: Settings.data.systemMonitor.swapCriticalThreshold readonly property int diskWarningThreshold: Settings.data.systemMonitor.diskWarningThreshold readonly property int diskCriticalThreshold: Settings.data.systemMonitor.diskCriticalThreshold readonly property int diskAvailWarningThreshold: Settings.data.systemMonitor.diskAvailWarningThreshold readonly property int diskAvailCriticalThreshold: Settings.data.systemMonitor.diskAvailCriticalThreshold // Computed warning/critical states (uses >= inclusive comparison) readonly property bool cpuWarning: cpuUsage >= cpuWarningThreshold readonly property bool cpuCritical: cpuUsage >= cpuCriticalThreshold readonly property bool tempWarning: cpuTemp >= tempWarningThreshold readonly property bool tempCritical: cpuTemp >= tempCriticalThreshold readonly property bool gpuWarning: gpuAvailable && gpuTemp >= gpuWarningThreshold readonly property bool gpuCritical: gpuAvailable && gpuTemp >= gpuCriticalThreshold readonly property bool memWarning: memPercent >= memWarningThreshold readonly property bool memCritical: memPercent >= memCriticalThreshold readonly property bool swapWarning: swapPercent >= swapWarningThreshold readonly property bool swapCritical: swapPercent >= swapCriticalThreshold // Helper functions for disk (disk path is dynamic) function isDiskWarning(diskPath, available = false) { return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailWarningThreshold : (diskPercents[diskPath] || 0) >= diskWarningThreshold; } function isDiskCritical(diskPath, available = false) { return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailCriticalThreshold : (diskPercents[diskPath] || 0) >= diskCriticalThreshold; } // Ready-to-use stat colors (for gauges, panels, icons) readonly property color cpuColor: cpuCritical ? criticalColor : (cpuWarning ? warningColor : Color.mPrimary) readonly property color tempColor: tempCritical ? criticalColor : (tempWarning ? warningColor : Color.mPrimary) readonly property color gpuColor: gpuCritical ? criticalColor : (gpuWarning ? warningColor : Color.mPrimary) readonly property color memColor: memCritical ? criticalColor : (memWarning ? warningColor : Color.mPrimary) readonly property color swapColor: swapCritical ? criticalColor : (swapWarning ? warningColor : Color.mPrimary) function getCoreUsageColor(usage) { if (usage >= cpuCriticalThreshold) return criticalColor; if (usage >= cpuWarningThreshold) return warningColor; return Color.mPrimary; } function getDiskColor(diskPath, available = false) { return isDiskCritical(diskPath, available) ? criticalColor : (isDiskWarning(diskPath, available) ? warningColor : Color.mPrimary); } // Internal state for CPU calculation property var prevCpuStats: null property var prevCpuCoresStats: null // Internal state for network speed calculation // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered // since the computer started, so their value will easily overlfow a 32bit int. property real prevRxBytes: 0 property real prevTxBytes: 0 property real prevTime: 0 // Cpu temperature is the most complex readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] property string cpuTempSensorName: "" property string cpuTempHwmonPath: "" // For Intel coretemp averaging of all cores/sensors property var intelTempValues: [] property int intelTempFilesChecked: 0 property int intelTempMaxFiles: 20 // Will test up to temp20_input // Thermal zone fallback (for ARM SoCs with SCMI sensors, etc.) property var cpuThermalZonePaths: [] // All matching CPU zones for averaging property string gpuThermalZonePath: "" // GPU temperature detection // On dual-GPU systems, we prioritize discrete GPUs over integrated GPUs // Priority: NVIDIA (opt-in) > AMD dGPU > Intel Arc > AMD iGPU // Note: NVIDIA requires opt-in because nvidia-smi wakes the dGPU on laptops, draining battery readonly property var supportedTempGpuSensorNames: ["amdgpu", "xe"] property string gpuTempHwmonPath: "" property var foundGpuSensors: [] // [{hwmonPath, type, hasDedicatedVram}] property int gpuVramCheckIndex: 0 // -------------------------------------------- Component.onCompleted: { Logger.i("SystemStat", "Service started (polling deferred until a consumer registers)."); // Kickoff the cpu name detection for temperature (one-time probes, not polling) cpuTempNameReader.checkNext(); // Kickoff the gpu sensor detection for temperature (one-time probes, not polling) gpuTempNameReader.checkNext(); // Get nproc on startup (one-time) nprocProcess.running = true; } onShouldRunChanged: { if (shouldRun) { // Reset differential state so first readings after resume are clean root.prevCpuStats = null; root.prevCpuCoresStats = null; root.prevTime = 0; // Trigger initial reads zfsArcStatsFile.reload(); loadAvgFile.reload(); // Start persistent disk shell if (!dfShell.running) { dfShell.running = true; } } else { // Stop persistent disk shell if (dfShell.running) { dfShell.running = false; } } } // Re-run GPU detection when dGPU opt-in setting changes Connections { target: Settings.data.systemMonitor function onEnableDgpuMonitoringChanged() { Logger.i("SystemStat", "dGPU monitoring opt-in setting changed, re-detecting GPUs"); restartGpuDetection(); } } // Reset differential state after suspend so the first reading is treated as fresh Connections { target: Time function onResumed() { Logger.i("SystemStat", "System resumed - resetting differential state"); root.prevCpuStats = null; root.prevCpuCoresStats = null; root.prevTime = 0; } } function restartGpuDetection() { // Reset GPU state root.gpuAvailable = false; root.gpuType = ""; root.gpuTempHwmonPath = ""; root.gpuTemp = 0; root.foundGpuSensors = []; root.gpuVramCheckIndex = 0; // Restart GPU detection gpuTempNameReader.currentIndex = 0; gpuTempNameReader.checkNext(); } // -------------------------------------------- // Timer for CPU usage and temperature Timer { id: cpuTimer interval: root.cpuUsageIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: { cpuStatFile.reload(); updateCpuTemperature(); } } // Timer for CPU frequency (slower — /proc/cpuinfo is large and freq changes infrequently) Timer { id: cpuFreqTimer interval: root.cpuFreqIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: cpuInfoFile.reload() } // Timer for load average Timer { id: loadAvgTimer interval: root.loadAvgIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: loadAvgFile.reload() } // Timer for memory stats Timer { id: memoryTimer interval: root.memIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: { memInfoFile.reload(); zfsArcStatsFile.reload(); } } // Timer for disk usage Timer { id: diskTimer interval: root.diskIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: { if (dfShell.running) { dfShell.write("df --output=target,pcent,used,size,avail --block-size=1 -x efivarfs 2>/dev/null; echo '@@DF_END@@'\n"); } } } // Timer for network speeds Timer { id: networkTimer interval: root.networkIntervalMs repeat: true running: root.shouldRun triggeredOnStart: true onTriggered: netDevFile.reload() } // Timer for GPU temperature Timer { id: gpuTempTimer interval: root.gpuIntervalMs repeat: true running: root.shouldRun && root.gpuAvailable triggeredOnStart: true onTriggered: updateGpuTemperature() } // -------------------------------------------- // FileView components for reading system files FileView { id: memInfoFile path: "/proc/meminfo" onLoaded: parseMemoryInfo(text()) } FileView { id: cpuStatFile path: "/proc/stat" onLoaded: calculateCpuUsage(text()) } FileView { id: netDevFile path: "/proc/net/dev" onLoaded: calculateNetworkSpeed(text()) } FileView { id: loadAvgFile path: "/proc/loadavg" onLoaded: parseLoadAverage(text()) } // ZFS ARC stats file (only exists on ZFS systems) FileView { id: zfsArcStatsFile path: "/proc/spl/kstat/zfs/arcstats" printErrors: false onLoaded: parseZfsArcStats(text()) onLoadFailed: { // File doesn't exist (non-ZFS system), set ARC values to 0 root.zfsArcSizeKb = 0; root.zfsArcCminKb = 0; } } // -------------------------------------------- // Persistent shell for disk usage queries (avoids fork+exec of large Quickshell process every poll) // Uses 'df' aka 'disk free' // "-x efivarfs" skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering // --block-size=1 gives us bytes for precise GB calculation // Timer writes commands to stdin; SplitParser reads output delimited by @@DF_END@@ Process { id: dfShell command: ["sh"] stdinEnabled: true running: false onRunningChanged: { if (!running && root.shouldRun) { // Restart if it died unexpectedly while we still need it Logger.w("SystemStat", "Disk shell exited unexpectedly, restarting"); Qt.callLater(() => { dfShell.running = true; }); } } stdout: SplitParser { splitMarker: "@@DF_END@@" onRead: data => { const lines = data.trim().split('\n'); const newPercents = {}; const newAvailPercents = {}; const newUsedGb = {}; const newSizeGb = {}; const newAvailableGb = {}; const bytesPerGb = 1000 * 1000 * 1000; // Start from line 1 (skip header) for (var i = 1; i < lines.length; i++) { const parts = lines[i].trim().split(/\s+/); if (parts.length >= 5) { const target = parts[0]; const percent = parseInt(parts[1].replace(/[^0-9]/g, '')) || 0; const usedBytes = parseFloat(parts[2]) || 0; const sizeBytes = parseFloat(parts[3]) || 0; const availBytes = parseFloat(parts[4]) || 0; const availPercent = sizeBytes > 0 ? (availBytes / sizeBytes) * 100 : 0; newPercents[target] = percent; newAvailPercents[target] = Math.round(availPercent); newUsedGb[target] = usedBytes / bytesPerGb; newSizeGb[target] = sizeBytes / bytesPerGb; newAvailableGb[target] = availBytes / bytesPerGb; } } root.diskPercents = newPercents; root.diskAvailPercents = newAvailPercents; root.diskUsedGb = newUsedGb; root.diskSizeGb = newSizeGb; root.diskAvailableGb = newAvailableGb; root.pushDiskHistory(); } } } // Process to get number of processors Process { id: nprocProcess command: ["nproc"] running: false stdout: StdioCollector { onStreamFinished: { root.nproc = parseInt(text.trim()); } } } // FileView to get avg cpu frequency (replaces subprocess spawn of `cat /proc/cpuinfo`) FileView { id: cpuInfoFile path: "/proc/cpuinfo" onLoaded: { let txt = text(); let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g); if (matches && matches.length > 0) { let totalFreq = 0.0; for (let i = 0; i < matches.length; i++) { totalFreq += parseFloat(matches[i].split(":")[1]); } let avgFreq = (totalFreq / matches.length) / 1000.0; root.cpuFreq = avgFreq.toFixed(1) + "GHz"; cpuMaxFreqFile.reload(); if (avgFreq > root.cpuGlobalMaxFreq) root.cpuGlobalMaxFreq = avgFreq; if (root.cpuGlobalMaxFreq > 0) { root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq); } } } } // FileView to get maximum CPU frequency limit (replaces subprocess spawn) // Reads cpu0's scaling_max_freq as representative value FileView { id: cpuMaxFreqFile path: "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq" printErrors: false onLoaded: { let maxKHz = parseInt(text().trim()); if (!isNaN(maxKHz) && maxKHz > 0) { let newMaxFreq = maxKHz / 1000000.0; if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) { root.cpuGlobalMaxFreq = newMaxFreq; } } } } // -------------------------------------------- // -------------------------------------------- // CPU Temperature // It's more complex. // ---- // #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower" FileView { id: cpuTempNameReader property int currentIndex: 0 printErrors: false function checkNext() { if (currentIndex >= 16) { // No hwmon sensor found, try thermal_zone fallback (ARM SoCs, SCMI, etc.) thermalZoneScanner.startScan(); return; } //Logger.i("SystemStat", "---- Probing: hwmon", currentIndex) cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`; cpuTempNameReader.reload(); } onLoaded: { const name = text().trim(); if (root.supportedTempCpuSensorNames.includes(name)) { root.cpuTempSensorName = name; root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`; Logger.i("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`); } else { currentIndex++; Qt.callLater(() => { // Qt.callLater is mandatory checkNext(); }); } } onLoadFailed: function (error) { currentIndex++; Qt.callLater(() => { // Qt.callLater is mandatory checkNext(); }); } } // ---- // #2 - Read sensor value FileView { id: cpuTempReader printErrors: false onLoaded: { const data = text().trim(); if (root.cpuTempSensorName === "coretemp") { // For Intel, collect all temperature values const temp = parseInt(data) / 1000.0; //console.log(temp, cpuTempReader.path) root.intelTempValues.push(temp); Qt.callLater(() => { // Qt.callLater is mandatory checkNextIntelTemp(); }); } else { // For AMD sensors (k10temp and zenpower), directly set the temperature root.cpuTemp = Math.round(parseInt(data) / 1000.0); root.pushCpuTempHistory(); } } onLoadFailed: function (error) { Qt.callLater(() => { // Qt.callLater is mandatory checkNextIntelTemp(); }); } } // -------------------------------------------- // Thermal zone fallback for CPU and GPU temperature // Used on ARM SoCs (e.g., SCMI sensors) where hwmon doesn't expose // coretemp/k10temp/zenpower. Scans /sys/class/thermal/thermal_zoneN/type // for CPU and GPU zone names, then reads temp from all matching zones. // // CPU: reads all cpu-*-thermal zones and reports the hottest core. // GPU: prefers gpu-avg-thermal (firmware average), falls back to max of gpu[0-9]-thermal. FileView { id: thermalZoneScanner property int currentIndex: 0 property var cpuZones: [] property var gpuZones: [] property string gpuAvgZonePath: "" printErrors: false function startScan() { currentIndex = 0; cpuZones = []; gpuZones = []; gpuAvgZonePath = ""; checkNext(); } function checkNext() { if (currentIndex >= 20) { finishScan(); return; } thermalZoneScanner.path = `/sys/class/thermal/thermal_zone${currentIndex}/type`; thermalZoneScanner.reload(); } onLoaded: { const name = text().trim(); const zonePath = `/sys/class/thermal/thermal_zone${currentIndex}`; if (name.startsWith("cpu") && name.endsWith("thermal")) { cpuZones.push({ "type": name, "path": zonePath + "/temp" }); } else if (name === "gpu-avg-thermal") { gpuAvgZonePath = zonePath + "/temp"; } else if (/^gpu[0-9]+-?thermal$/.test(name)) { gpuZones.push({ "type": name, "path": zonePath + "/temp" }); } currentIndex++; Qt.callLater(() => { checkNext(); }); } onLoadFailed: function (error) { currentIndex++; Qt.callLater(() => { checkNext(); }); } function finishScan() { // CPU thermal zones if (cpuZones.length > 0) { root.cpuTempSensorName = "thermal_zone"; root.cpuThermalZonePaths = cpuZones.map(z => z.path); const types = cpuZones.map(z => z.type).join(", "); Logger.i("SystemStat", `Found ${cpuZones.length} CPU thermal zone(s): ${types}`); } else if (root.cpuTempHwmonPath === "") { Logger.w("SystemStat", "No supported temperature sensor found"); } // GPU thermal zones if (gpuAvgZonePath !== "") { root.gpuThermalZonePath = gpuAvgZonePath; root.gpuAvailable = true; root.gpuType = "thermal_zone"; Logger.i("SystemStat", `Found GPU thermal zone: gpu-avg-thermal`); } else if (gpuZones.length > 0) { root.gpuThermalZonePaths = gpuZones.map(z => z.path); root.gpuThermalZonePath = gpuZones[0].path; // fallback single path root.gpuAvailable = true; root.gpuType = "thermal_zone"; const types = gpuZones.map(z => z.type).join(", "); Logger.i("SystemStat", `Found ${gpuZones.length} GPU thermal zone(s): ${types} (using max)`); } } } // Thermal zone reader for CPU: reads all zones, reports max (hottest core) FileView { id: cpuThermalZoneReader property int currentZoneIndex: 0 property var collectedTemps: [] printErrors: false onLoaded: { const temp = parseInt(text().trim()) / 1000.0; if (!isNaN(temp) && temp > 0) collectedTemps.push(temp); currentZoneIndex++; Qt.callLater(() => { readNextCpuThermalZone(); }); } onLoadFailed: function (error) { currentZoneIndex++; Qt.callLater(() => { readNextCpuThermalZone(); }); } } function readNextCpuThermalZone() { if (cpuThermalZoneReader.currentZoneIndex >= root.cpuThermalZonePaths.length) { if (cpuThermalZoneReader.collectedTemps.length > 0) { root.cpuTemp = Math.round(Math.max(...cpuThermalZoneReader.collectedTemps)); } else { root.cpuTemp = 0; } root.pushCpuTempHistory(); return; } cpuThermalZoneReader.path = root.cpuThermalZonePaths[cpuThermalZoneReader.currentZoneIndex]; cpuThermalZoneReader.reload(); } // Thermal zone reader for GPU: reads single zone (gpu-avg-thermal) or max of gpu[N] zones FileView { id: gpuThermalZoneReader property int currentZoneIndex: 0 property var collectedTemps: [] printErrors: false onLoaded: { const temp = parseInt(text().trim()) / 1000.0; if (!isNaN(temp) && temp > 0) collectedTemps.push(temp); // If we have multiple GPU zones (no gpu-avg), iterate and take max if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) { currentZoneIndex++; if (currentZoneIndex < root.gpuThermalZonePaths.length) { Qt.callLater(() => { readNextGpuThermalZone(); }); return; } // All zones read, take max root.gpuTemp = Math.round(Math.max(...collectedTemps)); } else { // Single gpu-avg-thermal zone root.gpuTemp = Math.round(temp); } root.pushGpuHistory(); } } function readNextGpuThermalZone() { gpuThermalZoneReader.path = root.gpuThermalZonePaths[gpuThermalZoneReader.currentZoneIndex]; gpuThermalZoneReader.reload(); } // Property to store multiple GPU thermal zone paths (when no gpu-avg is available) property var gpuThermalZonePaths: [] // -------------------------------------------- // -------------------------------------------- // GPU Temperature // On dual-GPU systems (e.g., Intel iGPU + NVIDIA dGPU, or AMD APU + AMD dGPU), // we scan all hwmon entries, then select the best GPU based on priority. // ---- // #1 - Scan all hwmon entries to find GPU sensors FileView { id: gpuTempNameReader property int currentIndex: 0 printErrors: false function checkNext() { if (currentIndex >= 16) { // Finished scanning all hwmon entries // Only check nvidia-smi if user has explicitly enabled dGPU monitoring (opt-in) // because nvidia-smi wakes up the dGPU on laptops, draining battery if (Settings.data.systemMonitor.enableDgpuMonitoring) { Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), checking nvidia-smi (dGPU opt-in enabled)`); nvidiaSmiCheck.running = true; } else { Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), skipping nvidia-smi (dGPU opt-in disabled)`); root.gpuVramCheckIndex = 0; checkNextGpuVram(); } return; } gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`; gpuTempNameReader.reload(); } onLoaded: { const name = text().trim(); if (root.supportedTempGpuSensorNames.includes(name)) { // Collect this GPU sensor, don't stop - continue scanning for more const hwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`; const gpuType = name === "amdgpu" ? "amd" : "intel"; root.foundGpuSensors.push({ "hwmonPath": hwmonPath, "type": gpuType, "hasDedicatedVram": false // Will be checked later for AMD }); Logger.d("SystemStat", `Found ${name} GPU sensor at ${hwmonPath}`); } // Continue scanning regardless of whether we found a match currentIndex++; Qt.callLater(() => { checkNext(); }); } onLoadFailed: function (error) { currentIndex++; Qt.callLater(() => { checkNext(); }); } } // ---- // #2 - Read GPU sensor value (AMD/Intel via sysfs) FileView { id: gpuTempReader printErrors: false onLoaded: { const data = text().trim(); root.gpuTemp = Math.round(parseInt(data) / 1000.0); root.pushGpuHistory(); } } // ---- // #3 - Check if nvidia-smi is available (for NVIDIA GPUs) Process { id: nvidiaSmiCheck command: ["sh", "-c", "command -v nvidia-smi"] running: false stdout: StdioCollector { onStreamFinished: { if (text.trim().length > 0) { // Add NVIDIA as a GPU option (always discrete, highest priority) root.foundGpuSensors.push({ "hwmonPath": "", "type": "nvidia", "hasDedicatedVram": true // NVIDIA is always discrete }); Logger.d("SystemStat", "Found NVIDIA GPU (nvidia-smi available)"); } // After NVIDIA check, check VRAM for AMD GPUs to distinguish dGPU from iGPU root.gpuVramCheckIndex = 0; checkNextGpuVram(); } } } // ---- // #4 - Check VRAM for AMD GPUs to distinguish dGPU from iGPU // dGPUs have dedicated VRAM, iGPUs don't (use system RAM) FileView { id: gpuVramChecker printErrors: false onLoaded: { // File exists and has content = dGPU with dedicated VRAM const vramSize = parseInt(text().trim()); if (vramSize > 0) { root.foundGpuSensors[root.gpuVramCheckIndex].hasDedicatedVram = true; Logger.d("SystemStat", `GPU at ${root.foundGpuSensors[root.gpuVramCheckIndex].hwmonPath} has dedicated VRAM (dGPU)`); } root.gpuVramCheckIndex++; Qt.callLater(() => { checkNextGpuVram(); }); } onLoadFailed: function (error) { // File doesn't exist = iGPU (no dedicated VRAM) // hasDedicatedVram is already false by default root.gpuVramCheckIndex++; Qt.callLater(() => { checkNextGpuVram(); }); } } // ---- // #4 - Read GPU temperature via nvidia-smi (NVIDIA only) Process { id: nvidiaTempProcess command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] running: false stdout: StdioCollector { onStreamFinished: { const temp = parseInt(text.trim()); if (!isNaN(temp)) { root.gpuTemp = temp; root.pushGpuHistory(); } } } } // ------------------------------------------------------- // ------------------------------------------------------- // Parse ZFS ARC stats from /proc/spl/kstat/zfs/arcstats function parseZfsArcStats(text) { if (!text) return; const lines = text.split('\n'); // The file format is: name type data // We need to find the lines with "size" and "c_min" and extract the values (third column) let foundSize = false; let foundCmin = false; for (const line of lines) { const parts = line.trim().split(/\s+/); if (parts.length >= 3) { if (parts[0] === 'size') { // The value is in bytes, convert to KB const arcSizeBytes = parseInt(parts[2]) || 0; root.zfsArcSizeKb = Math.floor(arcSizeBytes / 1024); foundSize = true; } else if (parts[0] === 'c_min') { // The value is in bytes, convert to KB const arcCminBytes = parseInt(parts[2]) || 0; root.zfsArcCminKb = Math.floor(arcCminBytes / 1024); foundCmin = true; } // If we found both, we can return early if (foundSize && foundCmin) { return; } } } // If fields not found, set to 0 if (!foundSize) { root.zfsArcSizeKb = 0; } if (!foundCmin) { root.zfsArcCminKb = 0; } } // ------------------------------------------------------- // Parse load average from /proc/loadavg function parseLoadAverage(text) { if (!text) return; const parts = text.trim().split(/\s+/); if (parts.length >= 3) { root.loadAvg1 = parseFloat(parts[0]); root.loadAvg5 = parseFloat(parts[1]); root.loadAvg15 = parseFloat(parts[2]); } } // ------------------------------------------------------- // Parse memory info from /proc/meminfo function parseMemoryInfo(text) { if (!text) return; const lines = text.split('\n'); let memTotal = 0; let memAvailable = 0; let swapTotal = 0; let swapFree = 0; for (const line of lines) { if (line.startsWith('MemTotal:')) { memTotal = parseInt(line.split(/\s+/)[1]) || 0; } else if (line.startsWith('MemAvailable:')) { memAvailable = parseInt(line.split(/\s+/)[1]) || 0; } else if (line.startsWith('SwapTotal:')) { swapTotal = parseInt(line.split(/\s+/)[1]) || 0; } else if (line.startsWith('SwapFree:')) { swapFree = parseInt(line.split(/\s+/)[1]) || 0; } } if (memTotal > 0) { // Calculate usage, adjusting for ZFS ARC cache if present let usageKb = memTotal - memAvailable; if (root.zfsArcSizeKb > 0) { usageKb = Math.max(0, usageKb - root.zfsArcSizeKb + root.zfsArcCminKb); } root.memGb = (usageKb / 1048576).toFixed(1); // 1024*1024 = 1048576 root.memPercent = Math.round((usageKb / memTotal) * 100); root.memTotalGb = (memTotal / 1048576).toFixed(1); root.pushMemHistory(); } // Swap usage root.swapTotalGb = (swapTotal / 1048576).toFixed(1); if (swapTotal > 0) { const swapUsedKb = swapTotal - swapFree; root.swapGb = (swapUsedKb / 1048576).toFixed(1); root.swapPercent = Math.round((swapUsedKb / swapTotal) * 100); } else { root.swapGb = 0; root.swapPercent = 0; } } // ------------------------------------------------------- // Calculate CPU usage from /proc/stat function calculateLineUsage(line) { const parts = line.split(/\s+/); const stats = { "user": parseInt(parts[1]) || 0, "nice": parseInt(parts[2]) || 0, "system": parseInt(parts[3]) || 0, "idle": parseInt(parts[4]) || 0, "iowait": parseInt(parts[5]) || 0, "irq": parseInt(parts[6]) || 0, "softirq": parseInt(parts[7]) || 0, "steal": parseInt(parts[8]) || 0, "guest": parseInt(parts[9]) || 0, "guestNice": parseInt(parts[10]) || 0 }; return stats; } function computeUsage(prev, curr) { if (!prev || !curr) return -1; const currTotalIdle = curr.idle + curr.iowait; const currTotal = Object.values(curr).reduce((sum, val) => sum + val, 0); const prevTotalIdle = prev.idle + prev.iowait; const prevTotal = Object.values(prev).reduce((sum, val) => sum + val, 0); const diffTotal = currTotal - prevTotal; const diffIdle = currTotalIdle - prevTotalIdle; if (diffTotal > 0) { return (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1); } return -1; } function calculateCpuUsage(text) { if (!text) return; const lines = text.split('\n'); const cpuLine = lines[0]; // First line is total CPU if (!cpuLine.startsWith('cpu ')) return; const currCpuStats = calculateLineUsage(cpuLine); const usage = computeUsage(root.prevCpuStats, currCpuStats); if (usage >= 0) { root.cpuUsage = usage; root.pushCpuHistory(); } root.prevCpuStats = currCpuStats; // Find the number of CPU cores let nbCores = 0; for (let i = 1; i < lines.length; i++) { if (!lines[i].startsWith('cpu')) break; nbCores++; } // Fallback if we did not find any cores if (nbCores === 0) return; // If we found more cores than before, we reset our stats if (root.coresUsage.length < nbCores) root.coresUsage = new Array(nbCores).fill(0); let coresStats = []; let newCoresUsage = root.coresUsage.slice(); for (let i = 0; i < nbCores; i++) { const coreCpuLine = lines[i + 1]; const currCoreStats = calculateLineUsage(coreCpuLine); const coreUsage = computeUsage(root.prevCpuCoresStats?.[i], currCoreStats); if (coreUsage >= 0) { newCoresUsage[i] = coreUsage; } coresStats.push(currCoreStats); } root.coresUsage = newCoresUsage; root.prevCpuCoresStats = coresStats; } // ------------------------------------------------------- // Check whether a network interface is virtual/tunnel/bridge. // Only physical interfaces (eth*, en*, wl*, ww*) are kept so // that traffic routed through VPNs, Docker bridges, etc. is // not double-counted. readonly property var _virtualPrefixes: ["lo", "docker", "veth", "br-", "virbr", "vnet", "tun", "tap", "wg", "tailscale", "nordlynx", "proton", "mullvad", "flannel", "cni", "cali", "vxlan", "genev", "gre", "sit", "ip6tnl", "dummy", "ifb", "nlmon", "bond"] function isVirtualInterface(name) { for (let i = 0; i < _virtualPrefixes.length; ++i) { if (name.startsWith(_virtualPrefixes[i])) return true; } return false; } // ------------------------------------------------------- // Calculate RX and TX speed from /proc/net/dev // Sums speeds of all physical interfaces function calculateNetworkSpeed(text) { if (!text) { return; } const currentTime = Date.now() / 1000; const lines = text.split('\n'); let totalRx = 0; let totalTx = 0; for (var i = 2; i < lines.length; i++) { const line = lines[i].trim(); if (!line) { continue; } const colonIndex = line.indexOf(':'); if (colonIndex === -1) { continue; } const iface = line.substring(0, colonIndex).trim(); if (isVirtualInterface(iface)) { continue; } const statsLine = line.substring(colonIndex + 1).trim(); const stats = statsLine.split(/\s+/); const rxBytes = parseInt(stats[0], 10) || 0; const txBytes = parseInt(stats[8], 10) || 0; totalRx += rxBytes; totalTx += txBytes; } // Compute only if we have a previous run to compare to. if (root.prevTime > 0) { const timeDiff = currentTime - root.prevTime; // Avoid division by zero if time hasn't passed. if (timeDiff > 0) { let rxDiff = totalRx - root.prevRxBytes; let txDiff = totalTx - root.prevTxBytes; // Handle counter resets (e.g., WiFi reconnect), which would cause a negative value. if (rxDiff < 0) { rxDiff = 0; } if (txDiff < 0) { txDiff = 0; } root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s root.txSpeed = Math.round(txDiff / timeDiff); } } root.prevRxBytes = totalRx; root.prevTxBytes = totalTx; root.prevTime = currentTime; // Update network history after speeds are computed root.pushNetworkHistory(); } // ------------------------------------------------------- // Helper function to format network speeds function formatSpeed(bytesPerSecond) { const units = ["KB", "MB", "GB"]; let value = bytesPerSecond / 1000; let unitIndex = 0; while (value >= 1000 && unitIndex < units.length - 1) { value /= 1000; unitIndex++; } const unit = units[unitIndex]; const shortUnit = unit[0]; const numStr = value < 10 ? value.toFixed(1) : Math.round(value).toString(); return (numStr + unit).length > 5 ? numStr + shortUnit : numStr + unit; } // ------------------------------------------------------- // Compact speed formatter for vertical bar display function formatCompactSpeed(bytesPerSecond) { if (!bytesPerSecond || bytesPerSecond <= 0) return "0"; const units = ["", "K", "M", "G"]; let value = bytesPerSecond; let unitIndex = 0; while (value >= 1000 && unitIndex < units.length - 1) { value = value / 1000.0; unitIndex++; } // Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded) if (unitIndex < units.length - 1 && value >= 100) { value = value / 1000.0; unitIndex++; } const display = Math.round(value).toString(); return display + units[unitIndex]; } // ------------------------------------------------------- // Smart formatter for memory values (GB) - max 4 chars // Uses decimal for < 10GB, integer otherwise function formatGigabytes(memGb) { const value = parseFloat(memGb); if (isNaN(value)) return "0G"; if (value < 10) return value.toFixed(1) + "G"; // "0.0G" to "9.9G" return Math.round(value) + "G"; // "10G" to "999G" } // ------------------------------------------------------- // Formatting gigabytes with optional padding function formatGigabytesDisplay(memGb, maxGb = null) { const value = formatGigabytes(memGb === null ? 0 : memGb); if (maxGb !== null) { const padding = Math.max(4, formatGigabytes(maxGb).length); return value.padStart(padding, " "); } return value; } // ------------------------------------------------------- // Formatting percentage with optional padding function formatPercentageDisplay(value, padding = false) { return `${Math.round(value === null ? 0 : value)}%`.padStart(padding ? 4 : 0, " "); } // ------------------------------------------------------- // Formatting disk usage function formatDiskDisplay(diskPath, { percent = false, available = false, padding = false } = {}) { if (percent) { const raw = available ? root.diskAvailPercents[diskPath] : root.diskPercents[diskPath]; return formatPercentageDisplay(raw, padding); } else { const rawGb = available ? root.diskAvailableGb[diskPath] : root.diskUsedGb[diskPath]; const maxGb = padding ? root.diskSizeGb[diskPath] : null; return formatGigabytesDisplay(rawGb, maxGb); } } // ------------------------------------------------------- // Formatting ram usage function formatRamDisplay({ swap = false, percent = false, padding = false } = {}) { if (percent) { const raw = swap ? swapPercent : memPercent; return formatPercentageDisplay(raw, padding); } else { const rawGb = swap ? swapGb : memGb; const maxGb = padding ? (swap ? swapTotalGb : memTotalGb) : null; return formatGigabytesDisplay(rawGb, maxGb); } } // ------------------------------------------------------- // Function to start fetching and computing the cpu temperature function updateCpuTemperature() { // For AMD sensors (k10temp and zenpower), only use Tctl sensor // temp1_input corresponds to Tctl (Temperature Control) on these sensors if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") { cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`; cpuTempReader.reload(); } // For Intel coretemp, start averaging all available sensors/cores else if (root.cpuTempSensorName === "coretemp") { root.intelTempValues = []; root.intelTempFilesChecked = 0; checkNextIntelTemp(); } // For thermal_zone fallback (ARM SoCs, SCMI, etc.), read all CPU zones and take max else if (root.cpuTempSensorName === "thermal_zone") { cpuThermalZoneReader.currentZoneIndex = 0; cpuThermalZoneReader.collectedTemps = []; readNextCpuThermalZone(); } } // ------------------------------------------------------- // Function to check next Intel temperature sensor function checkNextIntelTemp() { if (root.intelTempFilesChecked >= root.intelTempMaxFiles) { // Calculate average of all found temperatures if (root.intelTempValues.length > 0) { let sum = 0; for (var i = 0; i < root.intelTempValues.length; i++) { sum += root.intelTempValues[i]; } root.cpuTemp = Math.round(sum / root.intelTempValues.length); root.pushCpuTempHistory(); //Logger.i("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) } else { Logger.w("SystemStat", "No temperature sensors found for coretemp"); root.cpuTemp = 0; root.pushCpuTempHistory(); } return; } // Check next temperature file root.intelTempFilesChecked++; cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`; cpuTempReader.reload(); } // ------------------------------------------------------- // Function to check VRAM for each AMD GPU to determine if it's a dGPU function checkNextGpuVram() { // Skip non-AMD GPUs (NVIDIA and Intel Arc are always discrete) while (root.gpuVramCheckIndex < root.foundGpuSensors.length) { const gpu = root.foundGpuSensors[root.gpuVramCheckIndex]; if (gpu.type === "amd") { // Check for dedicated VRAM at hwmonPath/device/mem_info_vram_total gpuVramChecker.path = `${gpu.hwmonPath}/device/mem_info_vram_total`; gpuVramChecker.reload(); return; } // Skip non-AMD GPUs root.gpuVramCheckIndex++; } // All VRAM checks complete, now select the best GPU selectBestGpu(); } // ------------------------------------------------------- // Function to select the best GPU based on priority // Priority (when dGPU monitoring enabled): NVIDIA > AMD dGPU > Intel Arc > AMD iGPU // Priority (when dGPU monitoring disabled): AMD iGPU only (discrete GPUs skipped to preserve D3cold) function selectBestGpu() { const dgpuEnabled = Settings.data.systemMonitor.enableDgpuMonitoring; if (root.foundGpuSensors.length === 0) { // No hwmon GPU sensors found, try thermal_zone fallback if (dgpuEnabled && root.gpuThermalZonePath === "" && root.gpuThermalZonePaths.length === 0) { // Thermal zone scanner hasn't found GPU zones yet; start a scan thermalZoneScanner.startScan(); } return; } let best = null; for (var i = 0; i < root.foundGpuSensors.length; i++) { const gpu = root.foundGpuSensors[i]; // NVIDIA is always highest priority (always discrete) - skip if dGPU monitoring disabled if (gpu.type === "nvidia") { if (dgpuEnabled) { best = gpu; break; } continue; } // AMD dGPU is second priority - skip if dGPU monitoring disabled (preserves D3cold power state) if (gpu.type === "amd" && gpu.hasDedicatedVram) { if (dgpuEnabled) { best = gpu; break; } continue; } // Intel Arc is third priority (always discrete) - skip if dGPU monitoring disabled if (gpu.type === "intel" && !best) { if (dgpuEnabled) { best = gpu; } continue; } // AMD iGPU is lowest priority (fallback) - always allowed (no D3cold issue) if (gpu.type === "amd" && !gpu.hasDedicatedVram && !best) { best = gpu; } } if (best) { root.gpuTempHwmonPath = best.hwmonPath; root.gpuType = best.type; root.gpuAvailable = true; const gpuDesc = best.type === "nvidia" ? "NVIDIA" : (best.type === "intel" ? "Intel Arc" : (best.hasDedicatedVram ? "AMD dGPU" : "AMD iGPU")); Logger.i("SystemStat", `Selected ${gpuDesc} for temperature monitoring at ${best.hwmonPath || "nvidia-smi"}`); } else if (!dgpuEnabled) { Logger.d("SystemStat", "No iGPU found and dGPU monitoring is disabled"); } } // ------------------------------------------------------- // Function to update GPU temperature function updateGpuTemperature() { if (root.gpuType === "nvidia") { nvidiaTempProcess.running = true; } else if (root.gpuType === "amd" || root.gpuType === "intel") { gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input`; gpuTempReader.reload(); } else if (root.gpuType === "thermal_zone") { if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) { // Multiple GPU zones (no gpu-avg), read all and take max gpuThermalZoneReader.currentZoneIndex = 0; gpuThermalZoneReader.collectedTemps = []; readNextGpuThermalZone(); } else { // Single gpu-avg-thermal zone gpuThermalZoneReader.path = root.gpuThermalZonePath; gpuThermalZoneReader.reload(); } } } }