extract cpu service from system stats, add --types filter to nova-stats
This commit is contained in:
parent
8628b4b27b
commit
29e06eadb5
7 changed files with 227 additions and 189 deletions
160
shell/services/CpuService.qml
Normal file
160
shell/services/CpuService.qml
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue