mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
1040 lines
34 KiB
QML
1040 lines
34 KiB
QML
pragma Singleton
|
|
import Qt.labs.folderlistmodel
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// Configuration
|
|
readonly property int minimumIntervalMs: 250
|
|
readonly property int defaultIntervalMs: 3000
|
|
|
|
function normalizeInterval(value) {
|
|
return Math.max(minimumIntervalMs, value || defaultIntervalMs);
|
|
}
|
|
|
|
// Public values
|
|
property real cpuUsage: 0
|
|
property real cpuTemp: 0
|
|
property real gpuTemp: 0
|
|
property bool gpuAvailable: false
|
|
property string gpuType: "" // "amd", "intel", "nvidia"
|
|
property real memGb: 0
|
|
property real memPercent: 0
|
|
property var diskPercents: ({})
|
|
property var diskUsedGb: ({}) // Used 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
|
|
|
|
// Network max speed tracking (learned over time, cached for 7 days)
|
|
readonly property real rxMaxSpeed: {
|
|
const peaks = networkStatsAdapter.rxPeaks || [];
|
|
return peaks.length > 0 ? Math.max(...peaks.map(p => p.speed)) : 0;
|
|
}
|
|
readonly property real txMaxSpeed: {
|
|
const peaks = networkStatsAdapter.txPeaks || [];
|
|
return peaks.length > 0 ? Math.max(...peaks.map(p => p.speed)) : 0;
|
|
}
|
|
|
|
// Ready-to-use ratios based on learned 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 diskWarningThreshold: Settings.data.systemMonitor.diskWarningThreshold
|
|
readonly property int diskCriticalThreshold: Settings.data.systemMonitor.diskCriticalThreshold
|
|
|
|
// 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
|
|
|
|
// Helper functions for disk (disk path is dynamic)
|
|
function isDiskWarning(diskPath) {
|
|
return (diskPercents[diskPath] || 0) >= diskWarningThreshold;
|
|
}
|
|
|
|
function isDiskCritical(diskPath) {
|
|
return (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)
|
|
|
|
function getDiskColor(diskPath) {
|
|
return isDiskCritical(diskPath) ? criticalColor : (isDiskWarning(diskPath) ? warningColor : Color.mPrimary);
|
|
}
|
|
|
|
// Helper function for color resolution based on value and thresholds
|
|
function getStatColor(value, warningThreshold, criticalThreshold) {
|
|
if (value >= criticalThreshold)
|
|
return criticalColor;
|
|
if (value >= warningThreshold)
|
|
return warningColor;
|
|
return Color.mPrimary;
|
|
}
|
|
|
|
// Internal state for CPU calculation
|
|
property var prevCpuStats: 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
|
|
|
|
// 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
|
|
|
|
// --------------------------------------------
|
|
// Network speed stats cache (7-day rolling window)
|
|
property string networkStatsFile: Settings.cacheDir + "network_stats.json"
|
|
|
|
FileView {
|
|
id: networkStatsView
|
|
path: root.networkStatsFile
|
|
printErrors: false
|
|
|
|
JsonAdapter {
|
|
id: networkStatsAdapter
|
|
property var rxPeaks: []
|
|
property var txPeaks: []
|
|
}
|
|
|
|
onLoadFailed: {
|
|
networkStatsAdapter.rxPeaks = [];
|
|
networkStatsAdapter.txPeaks = [];
|
|
}
|
|
|
|
onLoaded: {
|
|
root.pruneExpiredPeaks();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: networkStatsSaveDebounce
|
|
interval: 1000
|
|
onTriggered: networkStatsView.writeAdapter()
|
|
}
|
|
|
|
function pruneExpiredPeaks() {
|
|
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
const cutoff = Date.now() - sevenDaysMs;
|
|
const rxBefore = (networkStatsAdapter.rxPeaks || []).length;
|
|
const txBefore = (networkStatsAdapter.txPeaks || []).length;
|
|
|
|
networkStatsAdapter.rxPeaks = (networkStatsAdapter.rxPeaks || []).filter(p => p.timestamp > cutoff);
|
|
networkStatsAdapter.txPeaks = (networkStatsAdapter.txPeaks || []).filter(p => p.timestamp > cutoff);
|
|
|
|
// Save if any were pruned
|
|
if (networkStatsAdapter.rxPeaks.length !== rxBefore || networkStatsAdapter.txPeaks.length !== txBefore) {
|
|
networkStatsSaveDebounce.restart();
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------
|
|
Component.onCompleted: {
|
|
Logger.i("SystemStat", "Service started with custom polling intervals");
|
|
|
|
// Kickoff the cpu name detection for temperature
|
|
cpuTempNameReader.checkNext();
|
|
|
|
// Kickoff the gpu sensor detection for temperature
|
|
gpuTempNameReader.checkNext();
|
|
|
|
// Check for ZFS ARC stats on startup
|
|
zfsArcStatsFile.reload();
|
|
|
|
// Get nproc on startup
|
|
nprocProcess.running = true;
|
|
|
|
// Get initial load average
|
|
loadAvgFile.reload();
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
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
|
|
Timer {
|
|
id: cpuUsageTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.cpuPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: cpuStatFile.reload()
|
|
}
|
|
|
|
// Timer for load average
|
|
Timer {
|
|
id: loadAvgTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.loadAvgPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: loadAvgFile.reload()
|
|
}
|
|
|
|
// Timer for CPU temperature
|
|
Timer {
|
|
id: cpuTempTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.tempPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: updateCpuTemperature()
|
|
}
|
|
|
|
// Timer for memory stats
|
|
Timer {
|
|
id: memoryTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.memPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: {
|
|
memInfoFile.reload();
|
|
zfsArcStatsFile.reload();
|
|
}
|
|
}
|
|
|
|
// Timer for disk usage
|
|
Timer {
|
|
id: diskTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.diskPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: dfProcess.running = true
|
|
}
|
|
|
|
// Timer for network speeds
|
|
Timer {
|
|
id: networkTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.networkPollingInterval)
|
|
repeat: true
|
|
running: true
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
onTriggered: netDevFile.reload()
|
|
}
|
|
|
|
// Timer for GPU temperature
|
|
Timer {
|
|
id: gpuTempTimer
|
|
interval: root.normalizeInterval(Settings.data.systemMonitor.gpuPollingInterval)
|
|
repeat: true
|
|
running: root.gpuAvailable
|
|
triggeredOnStart: true
|
|
onIntervalChanged: {
|
|
if (running) {
|
|
restart();
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------
|
|
// Process to fetch disk usage (percent, used, size)
|
|
// 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
|
|
Process {
|
|
id: dfProcess
|
|
command: ["df", "--output=target,pcent,used,size", "--block-size=1", "-x", "efivarfs"]
|
|
running: false
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
const lines = text.trim().split('\n');
|
|
const newPercents = {};
|
|
const newUsedGb = {};
|
|
const newSizeGb = {};
|
|
const bytesPerGb = 1024 * 1024 * 1024;
|
|
// Start from line 1 (skip header)
|
|
for (var i = 1; i < lines.length; i++) {
|
|
const parts = lines[i].trim().split(/\s+/);
|
|
if (parts.length >= 4) {
|
|
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;
|
|
newPercents[target] = percent;
|
|
newUsedGb[target] = usedBytes / bytesPerGb;
|
|
newSizeGb[target] = sizeBytes / bytesPerGb;
|
|
}
|
|
}
|
|
root.diskPercents = newPercents;
|
|
root.diskUsedGb = newUsedGb;
|
|
root.diskSizeGb = newSizeGb;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process to get number of processors
|
|
Process {
|
|
id: nprocProcess
|
|
command: ["nproc"]
|
|
running: false
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
root.nproc = parseInt(text.trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------
|
|
// --------------------------------------------
|
|
// 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) {
|
|
// Check up to hwmon10
|
|
Logger.w("No supported temperature sensor found");
|
|
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);
|
|
}
|
|
}
|
|
onLoadFailed: function (error) {
|
|
Qt.callLater(() => {
|
|
// Qt.callLater is mandatory
|
|
checkNextIntelTemp();
|
|
});
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------
|
|
// --------------------------------------------
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// ----
|
|
// #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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// -------------------------------------------------------
|
|
// 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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// Calculate CPU usage from /proc/stat
|
|
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 parts = cpuLine.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
|
|
};
|
|
const totalIdle = stats.idle + stats.iowait;
|
|
const total = Object.values(stats).reduce((sum, val) => sum + val, 0);
|
|
|
|
if (root.prevCpuStats) {
|
|
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait;
|
|
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0);
|
|
|
|
const diffTotal = total - prevTotal;
|
|
const diffIdle = totalIdle - prevTotalIdle;
|
|
|
|
if (diffTotal > 0) {
|
|
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1);
|
|
}
|
|
}
|
|
|
|
root.prevCpuStats = stats;
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// Calculate RX and TX speed from /proc/net/dev
|
|
// Average speed of all interfaces excepted 'lo'
|
|
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 (iface === 'lo') {
|
|
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);
|
|
|
|
// Record new peaks if higher than current max (for adaptive ratio calculation)
|
|
const now = Date.now();
|
|
if (root.rxSpeed > root.rxMaxSpeed) {
|
|
networkStatsAdapter.rxPeaks = [...(networkStatsAdapter.rxPeaks || []),
|
|
{
|
|
speed: root.rxSpeed,
|
|
timestamp: now
|
|
}
|
|
];
|
|
networkStatsSaveDebounce.restart();
|
|
}
|
|
if (root.txSpeed > root.txMaxSpeed) {
|
|
networkStatsAdapter.txPeaks = [...(networkStatsAdapter.txPeaks || []),
|
|
{
|
|
speed: root.txSpeed,
|
|
timestamp: now
|
|
}
|
|
];
|
|
networkStatsSaveDebounce.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
root.prevRxBytes = totalRx;
|
|
root.prevTxBytes = totalTx;
|
|
root.prevTime = currentTime;
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// Helper function to format network speeds
|
|
function formatSpeed(bytesPerSecond) {
|
|
const units = ["KB", "MB", "GB"];
|
|
let value = bytesPerSecond / 1024;
|
|
let unitIndex = 0;
|
|
|
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
value /= 1024;
|
|
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 >= 1024 && unitIndex < units.length - 1) {
|
|
value = value / 1024.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 / 1024.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 formatMemoryGb(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"
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------
|
|
// 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);
|
|
//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;
|
|
}
|
|
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() {
|
|
if (root.foundGpuSensors.length === 0) {
|
|
Logger.d("SystemStat", "No GPU temperature sensor found");
|
|
return;
|
|
}
|
|
|
|
const dgpuEnabled = Settings.data.systemMonitor.enableDgpuMonitoring;
|
|
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();
|
|
}
|
|
}
|
|
}
|