add nova-plugin: in-process cxx-qt stats plugin replacing nova-stats subprocess

This commit is contained in:
Damocles 2026-05-02 02:24:42 +02:00
parent 40cc681e9a
commit e39d47177b
19 changed files with 1893 additions and 233 deletions

View file

@ -1,160 +1,50 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import NovaStats as NS
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: []
readonly property int usage: NS.CpuService.usage
readonly property real freqGhz: NS.CpuService.freqGhz
// cores arrives as QList<QString> JSON; parse into [{usage, freq_ghz, history:[]}]
readonly property var cores: {
let raw = NS.CpuService.cores;
let out = [];
for (let i = 0; i < raw.length; i++)
out.push(JSON.parse(raw[i]));
return out;
}
readonly property var coreMaxFreq: NS.CpuService.coreMaxFreq
readonly property var coreTypes: NS.CpuService.coreTypes
// Overall CPU history (60 samples)
property var history: []
readonly property var history: NS.CpuService.history
// Per-core data gating - applets increment/decrement
// Gate per-core history: applets increment/decrement coreConsumers
property int coreConsumers: 0
onCoreConsumersChanged: {
if (coreConsumers > 0)
_coreGraceTimer.stop();
else
NS.CpuService.enableCoreHistory = coreConsumers > 0;
if (coreConsumers <= 0)
_coreGraceTimer.start();
else
_coreGraceTimer.stop();
}
property Timer _coreGraceTimer: Timer {
interval: 30000
onTriggered: root.cores = root.cores.map(() => ({
"usage": 0,
"freq_ghz": 0,
"history": []
}))
onTriggered: NS.CpuService.clearCoreHistory()
}
// nova-stats process (cpu only)
property Process _proc: Process {
running: true
command: {
property Timer _pollTimer: Timer {
interval: {
const ms = S.Modules.statsDaemon.interval;
return ms > 0 ? ["nova-stats", "--types", "cpu", "--interval", ms.toString()] : ["nova-stats", "--types", "cpu"];
return ms > 0 ? ms : 4000;
}
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);
}
}
repeat: true
triggeredOnStart: true
onTriggered: NS.CpuService.poll()
}
// 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

@ -1,117 +1,71 @@
pragma Singleton
import QtQuick
import Quickshell.Io
import NovaStats as NS
import "." as M
QtObject {
id: root
// Temperature
property int tempCelsius: 0
property var tempHistory: [] // 150 samples @ 4s each 10 min
property var tempDevices: [] // [{name, celsius}] sorted hottest-first
readonly property int tempCelsius: NS.SystemStatsService.tempCelsius
readonly property var tempHistory: NS.SystemStatsService.tempHistory
// tempDevices arrives as QList<QString> JSON; parse into [{name, celsius}]
readonly property var tempDevices: {
let raw = NS.SystemStatsService.tempDevices;
let out = [];
for (let i = 0; i < raw.length; i++)
out.push(JSON.parse(raw[i]));
return out;
}
// GPU
property bool gpuAvailable: false
property string gpuVendor: ""
property int gpuUsage: 0
property real gpuVramUsedGb: 0
property real gpuVramTotalGb: 0
property int gpuTempC: 0
property var gpuHistory: [] // 60 samples @ ~4-8s each 4-8 min
readonly property bool gpuAvailable: NS.SystemStatsService.gpuAvailable
readonly property string gpuVendor: NS.SystemStatsService.gpuVendor
readonly property int gpuUsage: NS.SystemStatsService.gpuUsage
readonly property real gpuVramUsedGb: NS.SystemStatsService.gpuVramUsedGb
readonly property real gpuVramTotalGb: NS.SystemStatsService.gpuVramTotalGb
readonly property int gpuTempC: NS.SystemStatsService.gpuTempC
readonly property var gpuHistory: NS.SystemStatsService.gpuHistory
// Memory
property int memPercent: 0
property real memUsedGb: 0
property real memTotalGb: 0
property real memAvailGb: 0
property real memCachedGb: 0
property real memBuffersGb: 0
property var memHistory: []
readonly property int memPercent: NS.SystemStatsService.memPercent
readonly property real memUsedGb: NS.SystemStatsService.memUsedGb
readonly property real memTotalGb: NS.SystemStatsService.memTotalGb
readonly property real memAvailGb: NS.SystemStatsService.memAvailGb
readonly property real memCachedGb: NS.SystemStatsService.memCachedGb
readonly property real memBuffersGb: NS.SystemStatsService.memBuffersGb
readonly property var memHistory: NS.SystemStatsService.memHistory
// Disk
property var diskMounts: []
property int diskRootPct: 0
// diskMounts arrives as QList<QString> JSON; parse into [{target, pct, usedBytes, totalBytes}]
readonly property var diskMounts: {
let raw = NS.SystemStatsService.diskMounts;
let out = [];
for (let i = 0; i < raw.length; i++)
out.push(JSON.parse(raw[i]));
return out;
}
readonly property int diskRootPct: NS.SystemStatsService.diskRootPct
// nova-stats stream (mem, temp, gpu - cpu handled by CpuService)
property var _statsProc: Process {
running: true
command: {
// Polling
// Drive the Rust service from QML timers; both intervals read from Modules config.
property Timer _statsTimer: Timer {
interval: {
const ms = M.Modules.statsDaemon.interval;
return ms > 0 ? ["nova-stats", "--types", "mem,temp,gpu", "--interval", ms.toString()] : ["nova-stats", "--types", "mem,temp,gpu"];
return ms > 0 ? ms : 4000;
}
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
try {
const ev = JSON.parse(line);
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;
if (ev.devices)
root.tempDevices = ev.devices;
} else if (ev.type === "gpu") {
root.gpuAvailable = true;
root.gpuVendor = ev.vendor;
root.gpuUsage = ev.usage;
root.gpuVramUsedGb = ev.vram_used_gb;
root.gpuVramTotalGb = ev.vram_total_gb;
root.gpuTempC = ev.temp_c;
const gh = root.gpuHistory.concat([ev.usage]);
root.gpuHistory = gh.length > 60 ? gh.slice(gh.length - 60) : gh;
} else if (ev.type === "mem") {
root.memPercent = ev.percent;
root.memUsedGb = ev.used_gb;
root.memTotalGb = ev.total_gb;
root.memAvailGb = ev.avail_gb;
root.memCachedGb = ev.cached_gb;
root.memBuffersGb = ev.buffers_gb;
const h = root.memHistory.concat([ev.percent]);
root.memHistory = h.length > 30 ? h.slice(h.length - 30) : h;
}
} catch (e) {}
}
}
}
// Disk via df
property var _diskProc: Process {
id: diskProc
running: true
command: ["sh", "-c", "df -x tmpfs -x devtmpfs -x squashfs -x efivarfs -x overlay -B1 --output=target,size,used 2>/dev/null | awk 'NR>1 && $2+0>0 {print $1\"|\"$2\"|\"$3}'"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n").filter(l => l);
const mounts = [];
for (const line of lines) {
const parts = line.split("|");
if (parts.length < 3)
continue;
const total = parseInt(parts[1]);
const used = parseInt(parts[2]);
if (total <= 0)
continue;
mounts.push({
"target": parts[0],
"pct": Math.round(used / total * 100),
"usedBytes": used,
"totalBytes": total
});
}
root.diskMounts = mounts;
const rm = mounts.find(m => m.target === "/");
if (rm)
root.diskRootPct = rm.pct;
}
}
repeat: true
triggeredOnStart: true
onTriggered: NS.SystemStatsService.poll()
}
property var _diskTimer: Timer {
property Timer _diskTimer: Timer {
interval: M.Modules.disk.interval || 30000
running: true
repeat: true
onTriggered: diskProc.running = true
triggeredOnStart: true
onTriggered: NS.SystemStatsService.pollDisk()
}
}