refactor: add SystemStats singleton + nova-stats daemon for cpu/mem polling
This commit is contained in:
parent
71a843e0f3
commit
136ff53cb5
13 changed files with 371 additions and 196 deletions
119
modules/Cpu.qml
119
modules/Cpu.qml
|
|
@ -1,5 +1,4 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
|
|
@ -7,7 +6,7 @@ M.BarSection {
|
|||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property int usage: 0
|
||||
property int usage: M.SystemStats.cpuUsage
|
||||
Behavior on usage {
|
||||
NumberAnimation {
|
||||
duration: 400
|
||||
|
|
@ -15,7 +14,7 @@ M.BarSection {
|
|||
}
|
||||
}
|
||||
|
||||
property real freqGhz: 0
|
||||
property real freqGhz: M.SystemStats.cpuFreqGhz
|
||||
Behavior on freqGhz {
|
||||
NumberAnimation {
|
||||
duration: 400
|
||||
|
|
@ -23,115 +22,11 @@ M.BarSection {
|
|||
}
|
||||
}
|
||||
|
||||
property var _prev: null
|
||||
property var _corePrev: []
|
||||
property var _coreUsage: []
|
||||
property var _coreFreq: []
|
||||
property var _coreHistory: [] // array of arrays, last 16 samples per core
|
||||
property var _coreMaxFreq: [] // max freq in GHz per core, from cpufreq
|
||||
property var _coreTypes: [] // "Performance" or "Efficiency" per core
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
path: "/proc/stat"
|
||||
onLoaded: {
|
||||
const lines = text().split("\n");
|
||||
|
||||
// Aggregate
|
||||
const agg = lines[0].trim().split(/\s+/).slice(1).map(Number);
|
||||
const idle = agg[3] + agg[4];
|
||||
const total = agg.reduce((a, b) => a + b, 0);
|
||||
if (root._prev) {
|
||||
const dIdle = idle - root._prev.idle;
|
||||
const dTotal = total - root._prev.total;
|
||||
if (dTotal > 0)
|
||||
root.usage = Math.round((1 - dIdle / dTotal) * 100);
|
||||
}
|
||||
root._prev = {
|
||||
idle,
|
||||
total
|
||||
};
|
||||
|
||||
// Per-core
|
||||
const coreLines = lines.filter(l => /^cpu\d+\s/.test(l));
|
||||
const newUsage = [];
|
||||
const newPrev = root._corePrev.length === coreLines.length ? root._corePrev : Array(coreLines.length).fill(null);
|
||||
for (let i = 0; i < coreLines.length; i++) {
|
||||
const f = coreLines[i].trim().split(/\s+/).slice(1).map(Number);
|
||||
const ci = f[3] + f[4];
|
||||
const ct = f.reduce((a, b) => a + b, 0);
|
||||
if (newPrev[i]) {
|
||||
const di = ci - newPrev[i].idle;
|
||||
const dt = ct - newPrev[i].total;
|
||||
newUsage.push(dt > 0 ? Math.round((1 - di / dt) * 100) : 0);
|
||||
} else {
|
||||
newUsage.push(0);
|
||||
}
|
||||
newPrev[i] = {
|
||||
idle: ci,
|
||||
total: ct
|
||||
};
|
||||
}
|
||||
root._coreUsage = newUsage;
|
||||
root._corePrev = newPrev;
|
||||
|
||||
// Update sparkline history
|
||||
const histLen = 16;
|
||||
const oldH = root._coreHistory;
|
||||
const newH = [];
|
||||
for (let i = 0; i < newUsage.length; i++) {
|
||||
const prev = i < oldH.length ? oldH[i] : [];
|
||||
const next = prev.concat([newUsage[i]]);
|
||||
newH.push(next.length > histLen ? next.slice(next.length - histLen) : next);
|
||||
}
|
||||
root._coreHistory = newH;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuinfo
|
||||
path: "/proc/cpuinfo"
|
||||
onLoaded: {
|
||||
const lines = text().split("\n").filter(l => l.startsWith("cpu MHz"));
|
||||
if (lines.length === 0)
|
||||
return;
|
||||
const freqs = lines.map(l => parseFloat(l.split(":")[1]) / 1000);
|
||||
root.freqGhz = freqs.reduce((a, b) => a + b, 0) / freqs.length;
|
||||
root._coreFreq = freqs;
|
||||
}
|
||||
}
|
||||
|
||||
// Read per-core max freq once at init
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root._coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read P/E-core topology once at init
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root._coreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: M.Modules.cpu.interval || 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
cpuinfo.reload();
|
||||
}
|
||||
}
|
||||
readonly property var _coreUsage: M.SystemStats.cpuCoreUsage
|
||||
readonly property var _coreFreq: M.SystemStats.cpuCoreFreq
|
||||
readonly property var _coreHistory: M.SystemStats.cpuCoreHistory
|
||||
readonly property var _coreMaxFreq: M.SystemStats.cpuCoreMaxFreq
|
||||
readonly property var _coreTypes: M.SystemStats.cpuCoreTypes
|
||||
|
||||
property bool _pinned: false
|
||||
readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
|
|
@ -7,46 +6,8 @@ M.BarSection {
|
|||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property var _mounts: []
|
||||
property int _rootPct: 0
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
running: true
|
||||
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split("\n").filter(l => l);
|
||||
const mounts = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split("|");
|
||||
if (parts.length < 3)
|
||||
continue;
|
||||
const total = parseInt(parts[1]);
|
||||
const used = parseInt(parts[2]);
|
||||
if (total <= 0)
|
||||
continue;
|
||||
mounts.push({
|
||||
"target": parts[0],
|
||||
"pct": Math.round(used / total * 100),
|
||||
"usedBytes": used,
|
||||
"totalBytes": total
|
||||
});
|
||||
}
|
||||
root._mounts = mounts;
|
||||
const rm = mounts.find(m => m.target === "/");
|
||||
if (rm)
|
||||
root._rootPct = rm.pct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: M.Modules.disk.interval || 30000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: proc.running = true
|
||||
}
|
||||
property var _mounts: M.SystemStats.diskMounts
|
||||
property int _rootPct: M.SystemStats.diskRootPct
|
||||
|
||||
function _fmt(bytes) {
|
||||
if (bytes >= 1e12)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
M.BarSection {
|
||||
|
|
@ -7,45 +6,12 @@ M.BarSection {
|
|||
spacing: Math.max(1, M.Theme.moduleSpacing - 2)
|
||||
tooltip: ""
|
||||
|
||||
property int percent: 0
|
||||
property real usedGb: 0
|
||||
property real totalGb: 0
|
||||
property real availGb: 0
|
||||
property real cachedGb: 0
|
||||
property real buffersGb: 0
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: {
|
||||
const m = {};
|
||||
text().split("\n").forEach(l => {
|
||||
const [k, v] = l.split(":");
|
||||
if (v)
|
||||
m[k.trim()] = parseInt(v.trim());
|
||||
});
|
||||
const total = m.MemTotal || 0;
|
||||
const avail = m.MemAvailable || 0;
|
||||
const buffers = m.Buffers || 0;
|
||||
const cached = (m.Cached || 0) + (m.SReclaimable || 0);
|
||||
const used = total - avail;
|
||||
if (total > 0) {
|
||||
root.percent = Math.round(used / total * 100);
|
||||
root.usedGb = used / 1048576;
|
||||
root.totalGb = total / 1048576;
|
||||
root.availGb = avail / 1048576;
|
||||
root.cachedGb = cached / 1048576;
|
||||
root.buffersGb = buffers / 1048576;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: M.Modules.memory.interval || 2000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: meminfo.reload()
|
||||
}
|
||||
property int percent: M.SystemStats.memPercent
|
||||
property real usedGb: M.SystemStats.memUsedGb
|
||||
property real totalGb: M.SystemStats.memTotalGb
|
||||
property real availGb: M.SystemStats.memAvailGb
|
||||
property real cachedGb: M.SystemStats.memCachedGb
|
||||
property real buffersGb: M.SystemStats.memBuffersGb
|
||||
|
||||
function _fmt(gb) {
|
||||
return gb >= 10 ? gb.toFixed(1) + "G" : gb.toFixed(2) + "G";
|
||||
|
|
|
|||
127
modules/SystemStats.qml
Normal file
127
modules/SystemStats.qml
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import "." as M
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// ── CPU ──────────────────────────────────────────────────────────────
|
||||
property int cpuUsage: 0
|
||||
property real cpuFreqGhz: 0
|
||||
property var cpuCoreUsage: []
|
||||
property var cpuCoreFreq: []
|
||||
property var cpuCoreHistory: []
|
||||
property var cpuCoreMaxFreq: []
|
||||
property var cpuCoreTypes: []
|
||||
|
||||
// ── Memory ───────────────────────────────────────────────────────────
|
||||
property int memPercent: 0
|
||||
property real memUsedGb: 0
|
||||
property real memTotalGb: 0
|
||||
property real memAvailGb: 0
|
||||
property real memCachedGb: 0
|
||||
property real memBuffersGb: 0
|
||||
|
||||
// ── Disk ─────────────────────────────────────────────────────────────
|
||||
property var diskMounts: []
|
||||
property int diskRootPct: 0
|
||||
|
||||
// nova-stats stream (cpu + mem)
|
||||
property var _statsProc: Process {
|
||||
running: true
|
||||
command: ["nova-stats"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
try {
|
||||
const ev = JSON.parse(line);
|
||||
if (ev.type === "cpu") {
|
||||
root.cpuUsage = ev.usage;
|
||||
root.cpuFreqGhz = ev.freq_ghz;
|
||||
root.cpuCoreUsage = ev.core_usage;
|
||||
root.cpuCoreFreq = ev.core_freq_ghz;
|
||||
const histLen = 16;
|
||||
const oldH = root.cpuCoreHistory;
|
||||
const newH = [];
|
||||
for (let i = 0; i < ev.core_usage.length; i++) {
|
||||
const prev = i < oldH.length ? oldH[i] : [];
|
||||
const next = prev.concat([ev.core_usage[i]]);
|
||||
newH.push(next.length > histLen ? next.slice(next.length - histLen) : next);
|
||||
}
|
||||
root.cpuCoreHistory = newH;
|
||||
} else if (ev.type === "mem") {
|
||||
root.memPercent = ev.percent;
|
||||
root.memUsedGb = ev.used_gb;
|
||||
root.memTotalGb = ev.total_gb;
|
||||
root.memAvailGb = ev.avail_gb;
|
||||
root.memCachedGb = ev.cached_gb;
|
||||
root.memBuffersGb = ev.buffers_gb;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One-time: per-core max freq
|
||||
property var _maxFreqProc: Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One-time: P/E-core topology
|
||||
property var _coreTypesProc: Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.cpuCoreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disk via df
|
||||
property var _diskProc: Process {
|
||||
id: diskProc
|
||||
running: true
|
||||
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split("\n").filter(l => l);
|
||||
const mounts = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split("|");
|
||||
if (parts.length < 3)
|
||||
continue;
|
||||
const total = parseInt(parts[1]);
|
||||
const used = parseInt(parts[2]);
|
||||
if (total <= 0)
|
||||
continue;
|
||||
mounts.push({
|
||||
"target": parts[0],
|
||||
"pct": Math.round(used / total * 100),
|
||||
"usedBytes": used,
|
||||
"totalBytes": total
|
||||
});
|
||||
}
|
||||
root.diskMounts = mounts;
|
||||
const rm = mounts.find(m => m.target === "/");
|
||||
if (rm)
|
||||
root.diskRootPct = rm.pct;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var _diskTimer: Timer {
|
||||
interval: M.Modules.disk.interval || 30000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: diskProc.running = true
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ PowerProfile 1.0 PowerProfile.qml
|
|||
IdleInhibitor 1.0 IdleInhibitor.qml
|
||||
Notifications 1.0 Notifications.qml
|
||||
singleton NiriIpc 1.0 NiriIpc.qml
|
||||
singleton SystemStats 1.0 SystemStats.qml
|
||||
singleton ProcessList 1.0 ProcessList.qml
|
||||
singleton NotifService 1.0 NotifService.qml
|
||||
NotifItem 1.0 NotifItem.qml
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue