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 {
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

View file

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

View file

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

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 {
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

View file

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