diff --git a/shell/applets/CpuApplet.qml b/shell/applets/CpuApplet.qml index 76b3ab3..6b2123e 100644 --- a/shell/applets/CpuApplet.qml +++ b/shell/applets/CpuApplet.qml @@ -4,9 +4,6 @@ import "../services" as S Column { id: root - required property var cores - required property var coreMaxFreq - required property var coreTypes required property var processes required property color accentColor @@ -16,34 +13,32 @@ Column { onActiveChanged: { if (active && !_coreActive) { _coreActive = true; - S.SystemStats.coreConsumers++; + S.CpuService.coreConsumers++; } else if (!active && _coreActive) { _coreActive = false; - S.SystemStats.coreConsumers--; + S.CpuService.coreConsumers--; } - if (active) - _cpuHistory = []; } Component.onDestruction: if (_coreActive) - S.SystemStats.coreConsumers-- + S.CpuService.coreConsumers-- // Per-core rows Repeater { - model: root.cores.length + model: S.CpuService.cores.length delegate: Item { required property int index width: root.width - readonly property int _u: root.cores[index]?.usage ?? 0 - readonly property real _f: root.cores[index]?.freq_ghz ?? 0 + readonly property int _u: S.CpuService.cores[index]?.usage ?? 0 + readonly property real _f: S.CpuService.cores[index]?.freq_ghz ?? 0 readonly property color _barColor: S.Theme.loadColor(_u) readonly property bool _throttled: { - const maxF = root.coreMaxFreq[index] ?? 0; + const maxF = S.CpuService.coreMaxFreq[index] ?? 0; return maxF > 0 && _f < maxF * 0.85 && _u >= 60; } readonly property bool _isFirstECore: { - const types = root.coreTypes; + const types = S.CpuService.coreTypes; if (!types.length || index >= types.length) return false; if (types[index] !== "Efficiency") @@ -118,7 +113,7 @@ Column { anchors.verticalCenter: parent.verticalCenter width: 32 height: 10 - history: root.cores[parent.parent.index]?.history ?? [] + history: S.CpuService.cores[parent.parent.index]?.history ?? [] strokeColor: parent.parent._barColor colorAt: v => S.Theme.loadColor(v) active: root.active @@ -145,8 +140,8 @@ Column { // Overall CPU utilization InfoRow { label: "Total" - value: S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" - valueColor: S.Theme.loadColor(S.SystemStats.cpuUsage) + value: S.CpuService.usage + "% @ " + S.CpuService.freqGhz.toFixed(2) + " GHz" + valueColor: S.Theme.loadColor(S.CpuService.usage) } SparklineCanvas { @@ -155,23 +150,12 @@ Column { anchors.right: parent.right anchors.rightMargin: 12 height: 32 - history: root._cpuHistory + history: S.CpuService.history strokeColor: root.accentColor colorAt: v => S.Theme.loadColor(v) active: root.active } - property var _cpuHistory: [] - Connections { - target: S.SystemStats - function onCpuUsageChanged() { - if (!root.active) - return; - const h = root._cpuHistory.concat([S.SystemStats.cpuUsage]); - root._cpuHistory = h.length > 60 ? h.slice(h.length - 60) : h; - } - } - // Process list - hidden on lock screen (exposes running process names) Column { visible: !S.LockService.locked diff --git a/shell/dock/AppletDock.qml b/shell/dock/AppletDock.qml index 0601279..c0f1acc 100644 --- a/shell/dock/AppletDock.qml +++ b/shell/dock/AppletDock.qml @@ -186,9 +186,6 @@ PanelWindow { C.CpuApplet { width: parent.width - cores: S.SystemStats.cpuCores - coreMaxFreq: S.SystemStats.cpuCoreMaxFreq - coreTypes: S.SystemStats.cpuCoreTypes processes: _cpuProcs.processes accentColor: root._accent active: _cpuCard.expanded diff --git a/shell/modules/CpuModule.qml b/shell/modules/CpuModule.qml index 5290486..3fa14d5 100644 --- a/shell/modules/CpuModule.qml +++ b/shell/modules/CpuModule.qml @@ -8,25 +8,18 @@ M.BarModule { id: root active: S.Modules.cpu.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) - tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" + tooltip: "CPU: " + S.CpuService.usage + "% @ " + S.CpuService.freqGhz.toFixed(2) + " GHz" panelNamespace: "nova-cpu" panelContentWidth: 260 panelComponent: Component { C.CpuApplet { width: parent.width - cores: root._cores - coreMaxFreq: root._coreMaxFreq - coreTypes: root._coreTypes processes: root._procs.processes accentColor: root.accentColor active: root._showPanel } } - readonly property var _cores: S.SystemStats.cpuCores - readonly property var _coreMaxFreq: S.SystemStats.cpuCoreMaxFreq - readonly property var _coreTypes: S.SystemStats.cpuCoreTypes - property M.ProcessList _procs: M.ProcessList { sortBy: "cpu" active: root._showPanel @@ -38,7 +31,7 @@ M.BarModule { anchors.verticalCenter: parent.verticalCenter } M.BarLabel { - label: S.SystemStats.cpuUsage.toString().padStart(2) + "%@" + S.SystemStats.cpuFreqGhz.toFixed(2) + label: S.CpuService.usage.toString().padStart(2) + "%@" + S.CpuService.freqGhz.toFixed(2) minText: "99%@9.99" anchors.verticalCenter: parent.verticalCenter } diff --git a/shell/services/CpuService.qml b/shell/services/CpuService.qml new file mode 100644 index 0000000..bb5a8a1 --- /dev/null +++ b/shell/services/CpuService.qml @@ -0,0 +1,160 @@ +pragma Singleton + +import QtQuick +import Quickshell.Io +import "." as S + +QtObject { + id: root + + property int usage: 0 + property real freqGhz: 0 + property var cores: [] // [{usage, freq_ghz, history:[]}] - history only while coreConsumers > 0 + property var coreMaxFreq: [] + property var coreTypes: [] + + // Overall CPU history (60 samples) + property var history: [] + + // Per-core data gating - applets increment/decrement + property int coreConsumers: 0 + onCoreConsumersChanged: { + if (coreConsumers > 0) + _coreGraceTimer.stop(); + else + _coreGraceTimer.start(); + } + + property Timer _coreGraceTimer: Timer { + interval: 30000 + onTriggered: root.cores = root.cores.map(() => ({ + "usage": 0, + "freq_ghz": 0, + "history": [] + })) + } + + // nova-stats process (cpu only) + property Process _proc: Process { + running: true + command: { + const ms = S.Modules.statsDaemon.interval; + return ms > 0 ? ["nova-stats", "--types", "cpu", "--interval", ms.toString()] : ["nova-stats", "--types", "cpu"]; + } + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + try { + const ev = JSON.parse(line); + if (ev.type !== "cpu") + return; + root.usage = ev.usage; + root.freqGhz = ev.freq_ghz; + + const h = root.history.concat([ev.usage]); + root.history = h.length > 60 ? h.slice(h.length - 60) : h; + + if (root.coreConsumers > 0) { + const histLen = 16; + const prev = root.cores; + root.cores = ev.cores.map((c, i) => { + const oldHist = prev[i]?.history ?? []; + const hist = oldHist.concat([c.usage]); + return { + usage: c.usage, + freq_ghz: c.freq_ghz, + history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist + }; + }); + } else if (root.cores.length !== ev.cores.length) { + root.cores = ev.cores.map(c => ({ + "usage": c.usage, + "freq_ghz": c.freq_ghz, + "history": [] + })); + } + } catch (e) {} + } + } + } + + // One-time: per-core max freq + property Process _maxFreqProc: Process { + running: true + command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"] + stdout: StdioCollector { + onStreamFinished: { + root.coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); + } + } + } + + // One-time: P/E-core topology + property Process _coreTypesProc: Process { + running: true + command: ["sh", "-c", String.raw` + if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then + core=$(cat /sys/devices/cpu_core/cpus) + atom=$(cat /sys/devices/cpu_atom/cpus) + echo "hybrid:$core:$atom" + exit 0 + fi + ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do + f="$d/topology/core_type" + [ -f "$f" ] && cat "$f" + done + `] + stdout: StdioCollector { + onStreamFinished: { + const out = text.trim(); + if (!out) + return; + if (out.startsWith("hybrid:")) { + const parts = out.split(":"); + const coreRange = parts[1]; + const atomRange = parts[2]; + function expandRange(s) { + const cpus = new Set(); + for (const part of s.split(",")) { + if (part.includes("-")) { + const [a, b] = part.split("-").map(Number); + for (let i = a; i <= b; i++) + cpus.add(i); + } else { + cpus.add(Number(part)); + } + } + return cpus; + } + const pCores = expandRange(coreRange); + const eCores = expandRange(atomRange); + const maxCpu = Math.max(...pCores, ...eCores); + const types = []; + for (let i = 0; i <= maxCpu; i++) + types.push(eCores.has(i) ? "Efficiency" : "Performance"); + root.coreTypes = types; + } else { + const types = out.split("\n").filter(l => l).map(l => l.trim()); + if (types.length > 0) + root.coreTypes = types; + } + } + } + } + + // Fallback: infer P/E from max freq gap + function _inferCoreTypesFromFreq() { + if (coreTypes.length > 0 || coreMaxFreq.length < 2) + return; + const freqs = coreMaxFreq.filter(f => f > 0); + if (!freqs.length) + return; + const maxF = Math.max(...freqs); + const minF = Math.min(...freqs); + if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) { + const threshold = (maxF + minF) / 2; + coreTypes = coreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency"); + } + } + onCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq) +} diff --git a/shell/services/SystemStats.qml b/shell/services/SystemStats.qml index b2c67f0..bae2550 100644 --- a/shell/services/SystemStats.qml +++ b/shell/services/SystemStats.qml @@ -7,29 +7,6 @@ import "." as M QtObject { id: root - // ── CPU ────────────────────────────────────────────────────────────── - property int cpuUsage: 0 - property real cpuFreqGhz: 0 - property var cpuCores: [] // [{usage, freq_ghz, history:[]}] — only rebuilt while coreConsumers > 0 - property int coreConsumers: 0 - onCoreConsumersChanged: { - if (coreConsumers > 0) - _coreGraceTimer.stop(); - else - _coreGraceTimer.start(); - } - - property var _coreGraceTimer: Timer { - interval: 30000 - onTriggered: root.cpuCores = root.cpuCores.map(() => ({ - "usage": 0, - "freq_ghz": 0, - "history": [] - })) - } - property var cpuCoreMaxFreq: [] - property var cpuCoreTypes: [] - // ── Temperature ────────────────────────────────────────────────────── property int tempCelsius: 0 property var tempHistory: [] // 150 samples @ 4s each ≈ 10 min @@ -57,42 +34,19 @@ QtObject { property var diskMounts: [] property int diskRootPct: 0 - // nova-stats stream (cpu + mem) + // nova-stats stream (mem, temp, gpu - cpu handled by CpuService) property var _statsProc: Process { running: true command: { const ms = M.Modules.statsDaemon.interval; - return ms > 0 ? ["nova-stats", "--interval", ms.toString()] : ["nova-stats"]; + return ms > 0 ? ["nova-stats", "--types", "mem,temp,gpu", "--interval", ms.toString()] : ["nova-stats", "--types", "mem,temp,gpu"]; } 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; - if (root.coreConsumers > 0) { - const histLen = 16; - const prev = root.cpuCores; - root.cpuCores = ev.cores.map((c, i) => { - const oldHist = prev[i]?.history ?? []; - const hist = oldHist.concat([c.usage]); - return { - usage: c.usage, - freq_ghz: c.freq_ghz, - history: hist.length > histLen ? hist.slice(hist.length - histLen) : hist - }; - }); - } else if (root.cpuCores.length !== ev.cores.length) { - // Keep count in sync so panel can size correctly before consumers activate - root.cpuCores = ev.cores.map(c => ({ - "usage": c.usage, - "freq_ghz": c.freq_ghz, - "history": [] - })); - } - } else if (ev.type === "temp") { + if (ev.type === "temp") { root.tempCelsius = ev.celsius; const th = root.tempHistory.concat([ev.celsius]); root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; @@ -122,91 +76,6 @@ QtObject { } } - // One-time: per-core max freq (numerically sorted) - property var _maxFreqProc: Process { - running: true - command: ["sh", "-c", "ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do f=\"$d/cpufreq/cpuinfo_max_freq\"; [ -f \"$f\" ] && cat \"$f\" || echo 0; done"] - stdout: StdioCollector { - onStreamFinished: { - root.cpuCoreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); - } - } - } - - // One-time: P/E-core topology - // Priority: cpu_core/cpu_atom sysfs > topology/core_type > freq-gap heuristic - property var _coreTypesProc: Process { - running: true - command: ["sh", "-c", String.raw` - # Intel hybrid: /sys/devices/cpu_core/cpus and cpu_atom/cpus give CPU ranges - if [ -f /sys/devices/cpu_core/cpus ] && [ -f /sys/devices/cpu_atom/cpus ]; then - core=$(cat /sys/devices/cpu_core/cpus) - atom=$(cat /sys/devices/cpu_atom/cpus) - echo "hybrid:$core:$atom" - exit 0 - fi - # Fallback: per-core topology/core_type - ls -d /sys/devices/system/cpu/cpu[0-9]* 2>/dev/null | sort -V | while read d; do - f="$d/topology/core_type" - [ -f "$f" ] && cat "$f" - done - `] - stdout: StdioCollector { - onStreamFinished: { - const out = text.trim(); - if (!out) - return; - if (out.startsWith("hybrid:")) { - // Parse cpu_core/cpu_atom ranges into per-cpu type array - const parts = out.split(":"); - const coreRange = parts[1]; - const atomRange = parts[2]; - function expandRange(s) { - const cpus = new Set(); - for (const part of s.split(",")) { - if (part.includes("-")) { - const [a, b] = part.split("-").map(Number); - for (let i = a; i <= b; i++) - cpus.add(i); - } else { - cpus.add(Number(part)); - } - } - return cpus; - } - const pCores = expandRange(coreRange); - const eCores = expandRange(atomRange); - const maxCpu = Math.max(...pCores, ...eCores); - const types = []; - for (let i = 0; i <= maxCpu; i++) - types.push(eCores.has(i) ? "Efficiency" : "Performance"); - root.cpuCoreTypes = types; - } else { - // topology/core_type output - const types = out.split("\n").filter(l => l).map(l => l.trim()); - if (types.length > 0) - root.cpuCoreTypes = types; - } - } - } - } - - // Fallback: infer P/E from max freq gap when no sysfs topology is available - function _inferCoreTypesFromFreq() { - if (cpuCoreTypes.length > 0 || cpuCoreMaxFreq.length < 2) - return; - const freqs = cpuCoreMaxFreq.filter(f => f > 0); - if (!freqs.length) - return; - const maxF = Math.max(...freqs); - const minF = Math.min(...freqs); - if (maxF > 0 && minF > 0 && (maxF - minF) / maxF > 0.15) { - const threshold = (maxF + minF) / 2; - cpuCoreTypes = cpuCoreMaxFreq.map(f => f >= threshold ? "Performance" : "Efficiency"); - } - } - onCpuCoreMaxFreqChanged: Qt.callLater(_inferCoreTypesFromFreq) - // Disk via df property var _diskProc: Process { id: diskProc diff --git a/shell/services/qmldir b/shell/services/qmldir index 38e7fd6..42a29ec 100644 --- a/shell/services/qmldir +++ b/shell/services/qmldir @@ -4,6 +4,7 @@ NotifItem 1.0 NotifItem.qml singleton BacklightService 1.0 BacklightService.qml singleton BatteryService 1.0 BatteryService.qml singleton BluetoothService 1.0 BluetoothService.qml +singleton CpuService 1.0 CpuService.qml singleton DockState 1.0 DockState.qml singleton IdleInhibitService 1.0 IdleInhibitService.qml singleton LockService 1.0 LockService.qml diff --git a/stats-daemon/src/main.rs b/stats-daemon/src/main.rs index d54847d..582e7f4 100644 --- a/stats-daemon/src/main.rs +++ b/stats-daemon/src/main.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::io::{self, Write}; use std::thread; use std::time::{Duration, Instant}; @@ -21,36 +22,69 @@ fn parse_interval_ms() -> u64 { 1000 } +fn parse_types() -> HashSet { + let args: Vec = std::env::args().collect(); + let mut i = 1; + while i < args.len() { + if args[i] == "--types" { + if let Some(list) = args.get(i + 1) { + return list.split(',').map(|s| s.trim().to_string()).collect(); + } + } + i += 1; + } + HashSet::new() // empty = all types +} + fn main() { let interval = Duration::from_millis(parse_interval_ms()); + let types = parse_types(); let stdout = io::stdout(); let mut out = io::BufWriter::new(stdout.lock()); let mut prev: Vec = vec![]; let mut freqs: Vec = vec![]; - let mut gpu = gpu::detect_gpu(); let mut tick = 0u64; + let emit_cpu = types.is_empty() || types.contains("cpu"); + let emit_mem = types.is_empty() || types.contains("mem"); + let emit_temp = types.is_empty() || types.contains("temp"); + let emit_graphics = types.is_empty() || types.contains("gpu"); + + let mut gpu = if emit_graphics { + gpu::detect_gpu() + } else { + gpu::GpuBackend::None + }; + loop { let t0 = Instant::now(); - let curr = cpu::read_stat(); - if tick.is_multiple_of(2) { - freqs = cpu::read_core_freqs(); + if emit_cpu { + let curr = cpu::read_stat(); + if tick.is_multiple_of(2) { + freqs = cpu::read_core_freqs(); + } + cpu::emit_cpu(&mut out, &prev, &curr, &freqs); + prev = curr; + } + + if emit_mem && tick.is_multiple_of(2) { mem::emit_mem(&mut out); } - cpu::emit_cpu(&mut out, &prev, &curr, &freqs); - prev = curr; if tick.is_multiple_of(4) { - temp::emit_temp(&mut out); - // AMD/Intel read sysfs (instant); NVIDIA shells out so runs less often - let emit = match &gpu { - gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, - gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), - gpu::GpuBackend::None => false, - }; - if emit { - gpu::emit_gpu(&mut out, &mut gpu); + if emit_temp { + temp::emit_temp(&mut out); + } + if emit_graphics { + let emit = match &gpu { + gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, + gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), + gpu::GpuBackend::None => false, + }; + if emit { + gpu::emit_gpu(&mut out, &mut gpu); + } } }