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.
This commit is contained in:
2026-06-01 14:05:35 +02:00
parent 11e4e94ee8
commit 35b8e4372e
@@ -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);
}
}