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

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