extract cpu service from system stats, add --types filter to nova-stats

This commit is contained in:
Damocles 2026-04-27 19:04:28 +02:00
parent 8628b4b27b
commit 29e06eadb5
7 changed files with 227 additions and 189 deletions

View file

@ -4,9 +4,6 @@ import "../services" as S
Column { Column {
id: root id: root
required property var cores
required property var coreMaxFreq
required property var coreTypes
required property var processes required property var processes
required property color accentColor required property color accentColor
@ -16,34 +13,32 @@ Column {
onActiveChanged: { onActiveChanged: {
if (active && !_coreActive) { if (active && !_coreActive) {
_coreActive = true; _coreActive = true;
S.SystemStats.coreConsumers++; S.CpuService.coreConsumers++;
} else if (!active && _coreActive) { } else if (!active && _coreActive) {
_coreActive = false; _coreActive = false;
S.SystemStats.coreConsumers--; S.CpuService.coreConsumers--;
} }
if (active)
_cpuHistory = [];
} }
Component.onDestruction: if (_coreActive) Component.onDestruction: if (_coreActive)
S.SystemStats.coreConsumers-- S.CpuService.coreConsumers--
// Per-core rows // Per-core rows
Repeater { Repeater {
model: root.cores.length model: S.CpuService.cores.length
delegate: Item { delegate: Item {
required property int index required property int index
width: root.width width: root.width
readonly property int _u: root.cores[index]?.usage ?? 0 readonly property int _u: S.CpuService.cores[index]?.usage ?? 0
readonly property real _f: root.cores[index]?.freq_ghz ?? 0 readonly property real _f: S.CpuService.cores[index]?.freq_ghz ?? 0
readonly property color _barColor: S.Theme.loadColor(_u) readonly property color _barColor: S.Theme.loadColor(_u)
readonly property bool _throttled: { 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; return maxF > 0 && _f < maxF * 0.85 && _u >= 60;
} }
readonly property bool _isFirstECore: { readonly property bool _isFirstECore: {
const types = root.coreTypes; const types = S.CpuService.coreTypes;
if (!types.length || index >= types.length) if (!types.length || index >= types.length)
return false; return false;
if (types[index] !== "Efficiency") if (types[index] !== "Efficiency")
@ -118,7 +113,7 @@ Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 32 width: 32
height: 10 height: 10
history: root.cores[parent.parent.index]?.history ?? [] history: S.CpuService.cores[parent.parent.index]?.history ?? []
strokeColor: parent.parent._barColor strokeColor: parent.parent._barColor
colorAt: v => S.Theme.loadColor(v) colorAt: v => S.Theme.loadColor(v)
active: root.active active: root.active
@ -145,8 +140,8 @@ Column {
// Overall CPU utilization // Overall CPU utilization
InfoRow { InfoRow {
label: "Total" label: "Total"
value: S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" value: S.CpuService.usage + "% @ " + S.CpuService.freqGhz.toFixed(2) + " GHz"
valueColor: S.Theme.loadColor(S.SystemStats.cpuUsage) valueColor: S.Theme.loadColor(S.CpuService.usage)
} }
SparklineCanvas { SparklineCanvas {
@ -155,23 +150,12 @@ Column {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
height: 32 height: 32
history: root._cpuHistory history: S.CpuService.history
strokeColor: root.accentColor strokeColor: root.accentColor
colorAt: v => S.Theme.loadColor(v) colorAt: v => S.Theme.loadColor(v)
active: root.active 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) // Process list - hidden on lock screen (exposes running process names)
Column { Column {
visible: !S.LockService.locked visible: !S.LockService.locked

View file

@ -186,9 +186,6 @@ PanelWindow {
C.CpuApplet { C.CpuApplet {
width: parent.width width: parent.width
cores: S.SystemStats.cpuCores
coreMaxFreq: S.SystemStats.cpuCoreMaxFreq
coreTypes: S.SystemStats.cpuCoreTypes
processes: _cpuProcs.processes processes: _cpuProcs.processes
accentColor: root._accent accentColor: root._accent
active: _cpuCard.expanded active: _cpuCard.expanded

View file

@ -8,25 +8,18 @@ M.BarModule {
id: root id: root
active: S.Modules.cpu.enable active: S.Modules.cpu.enable
spacing: Math.max(1, S.Theme.moduleSpacing - 2) 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" panelNamespace: "nova-cpu"
panelContentWidth: 260 panelContentWidth: 260
panelComponent: Component { panelComponent: Component {
C.CpuApplet { C.CpuApplet {
width: parent.width width: parent.width
cores: root._cores
coreMaxFreq: root._coreMaxFreq
coreTypes: root._coreTypes
processes: root._procs.processes processes: root._procs.processes
accentColor: root.accentColor accentColor: root.accentColor
active: root._showPanel 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 { property M.ProcessList _procs: M.ProcessList {
sortBy: "cpu" sortBy: "cpu"
active: root._showPanel active: root._showPanel
@ -38,7 +31,7 @@ M.BarModule {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
M.BarLabel { 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" minText: "99%@9.99"
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View file

@ -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)
}

View file

@ -7,29 +7,6 @@ import "." as M
QtObject { QtObject {
id: root 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 // Temperature
property int tempCelsius: 0 property int tempCelsius: 0
property var tempHistory: [] // 150 samples @ 4s each 10 min property var tempHistory: [] // 150 samples @ 4s each 10 min
@ -57,42 +34,19 @@ QtObject {
property var diskMounts: [] property var diskMounts: []
property int diskRootPct: 0 property int diskRootPct: 0
// nova-stats stream (cpu + mem) // nova-stats stream (mem, temp, gpu - cpu handled by CpuService)
property var _statsProc: Process { property var _statsProc: Process {
running: true running: true
command: { command: {
const ms = M.Modules.statsDaemon.interval; 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 { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: line => { onRead: line => {
try { try {
const ev = JSON.parse(line); const ev = JSON.parse(line);
if (ev.type === "cpu") { if (ev.type === "temp") {
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") {
root.tempCelsius = ev.celsius; root.tempCelsius = ev.celsius;
const th = root.tempHistory.concat([ev.celsius]); const th = root.tempHistory.concat([ev.celsius]);
root.tempHistory = th.length > 150 ? th.slice(th.length - 150) : th; 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 // Disk via df
property var _diskProc: Process { property var _diskProc: Process {
id: diskProc id: diskProc

View file

@ -4,6 +4,7 @@ NotifItem 1.0 NotifItem.qml
singleton BacklightService 1.0 BacklightService.qml singleton BacklightService 1.0 BacklightService.qml
singleton BatteryService 1.0 BatteryService.qml singleton BatteryService 1.0 BatteryService.qml
singleton BluetoothService 1.0 BluetoothService.qml singleton BluetoothService 1.0 BluetoothService.qml
singleton CpuService 1.0 CpuService.qml
singleton DockState 1.0 DockState.qml singleton DockState 1.0 DockState.qml
singleton IdleInhibitService 1.0 IdleInhibitService.qml singleton IdleInhibitService 1.0 IdleInhibitService.qml
singleton LockService 1.0 LockService.qml singleton LockService 1.0 LockService.qml

View file

@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::io::{self, Write}; use std::io::{self, Write};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -21,29 +22,61 @@ fn parse_interval_ms() -> u64 {
1000 1000
} }
fn parse_types() -> HashSet<String> {
let args: Vec<String> = 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() { fn main() {
let interval = Duration::from_millis(parse_interval_ms()); let interval = Duration::from_millis(parse_interval_ms());
let types = parse_types();
let stdout = io::stdout(); let stdout = io::stdout();
let mut out = io::BufWriter::new(stdout.lock()); let mut out = io::BufWriter::new(stdout.lock());
let mut prev: Vec<cpu::Sample> = vec![]; let mut prev: Vec<cpu::Sample> = vec![];
let mut freqs: Vec<f64> = vec![]; let mut freqs: Vec<f64> = vec![];
let mut gpu = gpu::detect_gpu();
let mut tick = 0u64; 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 { loop {
let t0 = Instant::now(); let t0 = Instant::now();
if emit_cpu {
let curr = cpu::read_stat(); let curr = cpu::read_stat();
if tick.is_multiple_of(2) { if tick.is_multiple_of(2) {
freqs = cpu::read_core_freqs(); freqs = cpu::read_core_freqs();
mem::emit_mem(&mut out);
} }
cpu::emit_cpu(&mut out, &prev, &curr, &freqs); cpu::emit_cpu(&mut out, &prev, &curr, &freqs);
prev = curr; prev = curr;
}
if emit_mem && tick.is_multiple_of(2) {
mem::emit_mem(&mut out);
}
if tick.is_multiple_of(4) { if tick.is_multiple_of(4) {
if emit_temp {
temp::emit_temp(&mut out); temp::emit_temp(&mut out);
// AMD/Intel read sysfs (instant); NVIDIA shells out so runs less often }
if emit_graphics {
let emit = match &gpu { let emit = match &gpu {
gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true, gpu::GpuBackend::Amd { .. } | gpu::GpuBackend::Intel { .. } => true,
gpu::GpuBackend::Nvidia => tick.is_multiple_of(8), gpu::GpuBackend::Nvidia => tick.is_multiple_of(8),
@ -53,6 +86,7 @@ fn main() {
gpu::emit_gpu(&mut out, &mut gpu); gpu::emit_gpu(&mut out, &mut gpu);
} }
} }
}
let _ = out.flush(); let _ = out.flush();
tick += 1; tick += 1;