From 35b8e4372ed42c7a4a20ff3f4da137a010a15ca5 Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Mon, 1 Jun 2026 14:05:35 +0200 Subject: [PATCH] quickshell: read system metrics natively and harden update polling Replace the per-tick sh -c subprocess reads in the system popout with Quickshell FileView reads (/proc/stat, /proc/meminfo, and once-resolved hwmon/battery sysfs paths), dropping the 5s tick from ~7 process spawns to ~2. Wrap checkupdates in timeout 120 and guard against restarting an in-flight run so a slow sync can no longer thrash or stall. --- .../quickshell/bar/popouts/SystemPopout.qml | 128 +++++++++++++----- 1 file changed, 94 insertions(+), 34 deletions(-) diff --git a/dot_config/quickshell/bar/popouts/SystemPopout.qml b/dot_config/quickshell/bar/popouts/SystemPopout.qml index 7b79cce..dc75098 100644 --- a/dot_config/quickshell/bar/popouts/SystemPopout.qml +++ b/dot_config/quickshell/bar/popouts/SystemPopout.qml @@ -924,29 +924,91 @@ Item { // DATA FETCHING // ═══════════════════════════════════════ + // ─── Native sysfs/proc readers (no per-tick subprocess) ─────────────────── + // Fixed paths read directly; hwmon/battery paths resolved once by + // pathDiscovery below. FileView.reload()+text() is synchronous (verified). + + FileView { id: statFile; path: "/proc/stat"; blockLoading: true } + FileView { id: meminfoFile; path: "/proc/meminfo"; blockLoading: true } + FileView { id: cpuTempFile; blockLoading: true } + FileView { id: nvmeTempFile; blockLoading: true } + FileView { id: batCapFile; blockLoading: true } + FileView { id: batStatusFile; blockLoading: true } + property real prevCpuActive: -1 property real prevCpuTotal: -1 - Process { id: cpuProc; command: ["sh", "-c", "awk '/^cpu / {print $2+$4, $2+$4+$5}' /proc/stat"]; stdout: StdioCollector { - onStreamFinished: { - let p = this.text.trim().split(" "); - let active = parseFloat(p[0]), total = parseFloat(p[1]); - if (!isNaN(active) && root.prevCpuTotal > 0) { - let da = active - root.prevCpuActive; - let dt = total - root.prevCpuTotal; - if (dt > 0) { root.cpuVal = (da / dt) * 100; root.cpuHistory = root.pushHistory(root.cpuHistory, root.cpuVal / 100); } - } - root.prevCpuActive = active; - root.prevCpuTotal = total; + function readCpu() { + statFile.reload(); + let p = statFile.text().split("\n")[0].trim().split(/\s+/); // cpu user nice system idle … + let active = parseFloat(p[1]) + parseFloat(p[3]); // user + system + let total = active + parseFloat(p[4]); // + idle + if (!isNaN(active) && root.prevCpuTotal > 0) { + let da = active - root.prevCpuActive, dt = total - root.prevCpuTotal; + if (dt > 0) { root.cpuVal = (da / dt) * 100; root.cpuHistory = root.pushHistory(root.cpuHistory, root.cpuVal / 100); } } - }} + root.prevCpuActive = active; + root.prevCpuTotal = total; + } - Process { id: memProc; command: ["sh", "-c", "free -m | awk '/Mem:/ {printf \"%.1f/%.0fG %.4f\", $3/1024, $2/1024, $3/$2}'"]; stdout: StdioCollector { - onStreamFinished: { let p = this.text.trim().split(" "); if (p.length >= 2) { root.memText = p[0]; root.memVal = parseFloat(p[1]) || 0; root.memHistory = root.pushHistory(root.memHistory, root.memVal); } } - }} + function readMem() { + meminfoFile.reload(); + let t = meminfoFile.text(); + let total = parseInt((t.match(/MemTotal:\s+(\d+)/) || [])[1]); // kB + let avail = parseInt((t.match(/MemAvailable:\s+(\d+)/) || [])[1]); // kB + if (!isNaN(total) && !isNaN(avail) && total > 0) { + let used = total - avail; + root.memText = (used / 1048576).toFixed(1) + "/" + (total / 1048576).toFixed(0) + "G"; + root.memVal = used / total; + root.memHistory = root.pushHistory(root.memHistory, root.memVal); + } + } - Process { id: tempProc; command: ["sh", "-c", "sensors 2>/dev/null | awk '/Tctl:|Tdie:|Package id 0:/ {gsub(/\\+|°C/,\"\",$2); printf \"%d\", $2; exit}'"]; stdout: StdioCollector { - onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v)) { root.tempVal = v; root.tempText = v + "°C"; } } - }} + function readTemp() { + if (!cpuTempFile.path) return; + cpuTempFile.reload(); + let v = parseInt(cpuTempFile.text().trim()); // millidegrees + if (!isNaN(v)) { root.tempVal = Math.round(v / 1000); root.tempText = root.tempVal + "°C"; } + } + + function readNvme() { + if (!nvmeTempFile.path) return; + nvmeTempFile.reload(); + let v = parseInt(nvmeTempFile.text().trim()); + if (!isNaN(v) && v > 0) { root.nvmeTempVal = Math.round(v / 1000); root.nvmeTempText = root.nvmeTempVal + "°C"; } + } + + function readBattery() { + if (!batCapFile.path) return; + batCapFile.reload(); batStatusFile.reload(); + let c = parseInt(batCapFile.text().trim()); + let s = batStatusFile.text().trim(); + if (!isNaN(c)) root.batteryPercent = c; + if (s) root.batteryState = s; + } + + // Resolve hwmon/battery sysfs paths once per popout open, then read. + Process { + id: pathDiscovery + command: ["sh", "-c", + "for h in /sys/class/hwmon/hwmon*; do n=$(cat \"$h/name\" 2>/dev/null); " + + "case \"$n\" in k10temp|zenpower|coretemp) sel=''; for l in \"$h\"/temp*_label; do [ -r \"$l\" ] || continue; " + + "case \"$(cat \"$l\")\" in Tctl|Tdie|'Package id 0') sel=\"${l%_label}_input\"; break;; esac; done; " + + "[ -z \"$sel\" ] && sel=\"$h/temp1_input\"; echo \"cpuTemp=$sel\";; " + + "nvme) echo \"nvmeTemp=$h/temp1_input\";; esac; done; " + + "b=$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -1); [ -n \"$b\" ] && echo \"batDir=$b\" || true" + ] + stdout: StdioCollector { onStreamFinished: { + for (let ln of this.text.trim().split("\n")) { + let i = ln.indexOf("="); if (i < 0) continue; + let k = ln.slice(0, i), v = ln.slice(i + 1); + if (k === "cpuTemp") cpuTempFile.path = v; + else if (k === "nvmeTemp") nvmeTempFile.path = v; + else if (k === "batDir") { batCapFile.path = v + "/capacity"; batStatusFile.path = v + "/status"; } + } + root.readTemp(); root.readNvme(); + if (Shared.Config.hasBattery) root.readBattery(); + }} + } Process { id: gpuProc; command: [Shared.Config.gpuScript]; stdout: StdioCollector { onStreamFinished: { try { @@ -967,10 +1029,6 @@ Item { } catch(e) {} } }} - Process { id: nvmeTempProc; command: ["sh", "-c", "for d in /sys/class/hwmon/hwmon*; do if grep -q nvme \"$d/name\" 2>/dev/null; then awk '{printf \"%d\", $1/1000}' \"$d/temp1_input\" 2>/dev/null; break; fi; done"]; stdout: StdioCollector { - onStreamFinished: { let v = parseInt(this.text.trim()); if (!isNaN(v) && v > 0) { root.nvmeTempVal = v; root.nvmeTempText = v + "°C"; } } - }} - Process { id: diskProc; command: ["sh", "-c", "df -h " + Shared.Config.diskMount1 + " --output=pcent,size 2>/dev/null | tail -1 && " + "df -h " + Shared.Config.diskMount2 + " --output=pcent,size 2>/dev/null | tail -1 && " + @@ -986,7 +1044,7 @@ Item { } }} - Process { id: updateProc; command: ["bash", "-c", "checkupdates 2>/dev/null; echo \":$?\""]; stdout: StdioCollector { + Process { id: updateProc; command: ["bash", "-c", "timeout 120 checkupdates 2>/dev/null; echo \":$?\""]; stdout: StdioCollector { onStreamFinished: { let lines = this.text.trim().split("\n"); let exitMatch = lines[lines.length - 1].match(/:(\d+)$/); @@ -1020,10 +1078,7 @@ Item { onStreamFinished: { let l = this.text.trim(); if (l) { let p = l.split(":"); root.networkIface = p[0] || "--"; root.networkIp = p[1] || "--"; } else { root.networkIface = "Network"; root.networkIp = "Offline"; } } }} - // Battery & power profile (laptop) - Process { id: batteryProc; command: ["sh", "-c", "cat /sys/class/power_supply/BAT*/capacity /sys/class/power_supply/BAT*/status 2>/dev/null"]; stdout: StdioCollector { - onStreamFinished: { let l = this.text.trim().split("\n"); if (l.length >= 2) { root.batteryPercent = parseInt(l[0]) || -1; root.batteryState = l[1].trim(); } } - }} + // Power profile (laptop) — battery is read natively via readBattery(). Process { id: ppGetProc; command: ["powerprofilesctl", "get"]; stdout: StdioCollector { onStreamFinished: root.powerProfile = this.text.trim() }} @@ -1043,16 +1098,21 @@ Item { function rerun(proc) { proc.running = false; proc.running = true; } Timer { interval: 5000; running: true; repeat: true; onTriggered: { - rerun(cpuProc); rerun(memProc); rerun(tempProc); rerun(nvmeTempProc); rerun(idleProc); + root.readCpu(); root.readMem(); root.readTemp(); root.readNvme(); rerun(idleProc); if (Shared.Config.hasDiscreteGpu) rerun(gpuProc); - if (Shared.Config.hasBattery) rerun(batteryProc); + if (Shared.Config.hasBattery) root.readBattery(); }} Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); if (Shared.Config.hasPowerProfiles) rerun(ppGetProc); } } - Timer { interval: 300000; running: true; repeat: true; onTriggered: { rerun(updateProc); rerun(alhpProc); } } + // checkupdates can be slow/flaky; don't restart it while a run is still in flight. + Timer { interval: 300000; running: true; repeat: true; onTriggered: { + if (!updateProc.running) updateProc.running = true; + if (!alhpProc.running) alhpProc.running = true; + }} - // Stagger initial launches to avoid many concurrent process spawns + // Native reads are synchronous; stagger only the remaining process spawns. Component.onCompleted: { - rerun(cpuProc); rerun(memProc); rerun(tempProc); + pathDiscovery.running = true; + root.readCpu(); root.readMem(); staggerTimer.running = true; } Timer { @@ -1060,8 +1120,8 @@ Item { interval: 200 onTriggered: { if (Shared.Config.hasDiscreteGpu) rerun(gpuProc); - rerun(nvmeTempProc); rerun(idleProc); rerun(diskProc); rerun(netProc); rerun(updateProc); rerun(alhpProc); - if (Shared.Config.hasBattery) rerun(batteryProc); + rerun(idleProc); rerun(diskProc); rerun(netProc); + updateProc.running = true; alhpProc.running = true; if (Shared.Config.hasPowerProfiles) rerun(ppGetProc); } }