system-stats: many optimizations, removed GUI settings to control polling as it's too risky, disable all when on the lockscreen.

This commit is contained in:
Lemmy
2026-02-13 14:34:35 -05:00
parent 8c776b5504
commit 1a75b0b3f2
11 changed files with 148 additions and 292 deletions
-6
View File
@@ -311,13 +311,7 @@
"diskAvailCriticalThreshold": 10,
"batteryWarningThreshold": 20,
"batteryCriticalThreshold": 5,
"cpuPollingInterval": 1000,
"gpuPollingInterval": 3000,
"enableDgpuMonitoring": false,
"memPollingInterval": 1000,
"diskPollingInterval": 30000,
"networkPollingInterval": 1000,
"loadAvgPollingInterval": 3000,
"useCustomColors": false,
"warningColor": "",
"criticalColor": "",
+2 -10
View File
@@ -5,7 +5,7 @@ QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration47", "Removing network_stats.json cache and updating polling intervals");
logger.i("Migration47", "Removing network_stats.json cache");
// Remove the network_stats.json cache file (no longer used - autoscaling from history now)
const shellName = "noctalia";
@@ -13,15 +13,7 @@ QtObject {
const networkStatsFile = cacheDir + "network_stats.json";
Quickshell.execDetached(["rm", "-f", networkStatsFile]);
// Update polling intervals to 1000ms for smoother graphs (only if currently slower)
if (adapter.systemMonitor.cpuPollingInterval > 1000)
adapter.systemMonitor.cpuPollingInterval = 1000;
if (adapter.systemMonitor.memPollingInterval > 1000)
adapter.systemMonitor.memPollingInterval = 1000;
if (adapter.systemMonitor.networkPollingInterval > 1000)
adapter.systemMonitor.networkPollingInterval = 1000;
logger.d("Migration47", "Removed network_stats.json and adjusted polling intervals");
logger.d("Migration47", "Removed network_stats.json");
return true;
}
-6
View File
@@ -510,13 +510,7 @@ Singleton {
property int diskAvailCriticalThreshold: 10
property int batteryWarningThreshold: 20
property int batteryCriticalThreshold: 5
property int cpuPollingInterval: 1000
property int gpuPollingInterval: 3000
property bool enableDgpuMonitoring: false // Opt-in: reading dGPU sysfs/nvidia-smi wakes it from D3cold, draining battery
property int memPollingInterval: 1000
property int diskPollingInterval: 30000
property int networkPollingInterval: 1000
property int loadAvgPollingInterval: 3000
property bool useCustomColors: false
property string warningColor: ""
property string criticalColor: ""
+3
View File
@@ -81,6 +81,9 @@ Item {
implicitWidth: contentWidth
implicitHeight: contentHeight
Component.onCompleted: SystemStatService.registerComponent("bar-sysmon:" + (screen?.name || "unknown"))
Component.onDestruction: SystemStatService.unregisterComponent("bar-sysmon:" + (screen?.name || "unknown"))
function openExternalMonitor() {
Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]);
}
+3
View File
@@ -10,6 +10,9 @@ import qs.Widgets
NBox {
id: root
Component.onCompleted: SystemStatService.registerComponent("card-sysmonitor")
Component.onDestruction: SystemStatService.unregisterComponent("card-sysmonitor")
readonly property string diskPath: Settings.data.controlCenter.diskPath || "/"
readonly property real contentScale: 0.95 * Style.uiScaleRatio
@@ -158,6 +158,9 @@ DraggableDesktopWidget {
}
}
Component.onCompleted: SystemStatService.registerComponent("desktop-sysstat:" + root.statType)
Component.onDestruction: SystemStatService.unregisterComponent("desktop-sysstat:" + root.statType)
implicitWidth: Math.round(240 * widgetScale)
implicitHeight: Math.round(120 * widgetScale)
width: implicitWidth
@@ -167,15 +170,15 @@ DraggableDesktopWidget {
readonly property int graphUpdateInterval: {
switch (root.statType) {
case "CPU":
return Settings.data.systemMonitor.cpuPollingInterval;
return SystemStatService.cpuIntervalMs;
case "GPU":
return Settings.data.systemMonitor.gpuPollingInterval;
return SystemStatService.gpuIntervalMs;
case "Memory":
return Settings.data.systemMonitor.memPollingInterval;
return SystemStatService.memIntervalMs;
case "Disk":
return Settings.data.systemMonitor.diskPollingInterval;
return SystemStatService.diskIntervalMs;
case "Network":
return Settings.data.systemMonitor.networkPollingInterval;
return SystemStatService.networkIntervalMs;
default:
return 1000;
}
@@ -30,6 +30,8 @@ import qs.Widgets
Item {
id: root
Component.onDestruction: SystemStatService.unregisterComponent("settings")
// Screen reference for child components
property var screen
@@ -389,6 +391,7 @@ Item {
}
Component.onCompleted: {
SystemStatService.registerComponent("settings");
// Restore sidebar state
sidebarExpanded = ShellState.getSettingsSidebarExpanded();
}
@@ -1,149 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services.System
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Layout.fillWidth: true
NLabel {
Layout.fillWidth: true
description: I18n.tr("panels.system-monitor.polling-section-description")
}
// CPU Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("bar.system-monitor.cpu-usage-label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.cpuPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.cpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.cpuPollingInterval = value
suffix: " ms"
}
}
// GPU Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
visible: SystemStatService.gpuAvailable
NText {
Layout.fillWidth: true
text: I18n.tr("panels.system-monitor.gpu-section-label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.gpuPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.gpuPollingInterval")
onValueChanged: Settings.data.systemMonitor.gpuPollingInterval = value
suffix: " ms"
}
}
// Load Average Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("bar.system-monitor.load-average-label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.loadAvgPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.loadAvgPollingInterval")
onValueChanged: Settings.data.systemMonitor.loadAvgPollingInterval = value
suffix: " ms"
}
}
// Memory Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("bar.system-monitor.memory-usage-label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.memPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.memPollingInterval")
onValueChanged: Settings.data.systemMonitor.memPollingInterval = value
suffix: " ms"
}
}
// Disk Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("panels.system-monitor.disk-section-label")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 1000
to: 60000
stepSize: 250
value: Settings.data.systemMonitor.diskPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.diskPollingInterval")
onValueChanged: Settings.data.systemMonitor.diskPollingInterval = value
suffix: " ms"
}
}
// Network Polling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NText {
Layout.fillWidth: true
text: I18n.tr("common.network")
pointSize: Style.fontSizeM
}
NSpinBox {
from: 250
to: 10000
stepSize: 250
value: Settings.data.systemMonitor.networkPollingInterval
defaultValue: Settings.getDefaultValue("systemMonitor.networkPollingInterval")
onValueChanged: Settings.data.systemMonitor.networkPollingInterval = value
suffix: " ms"
}
}
}
@@ -27,11 +27,6 @@ ColumnLayout {
tabIndex: 1
checked: subTabBar.currentIndex === 1
}
NTabButton {
text: I18n.tr("common.polling")
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
}
Item {
@@ -47,6 +42,5 @@ ColumnLayout {
screen: root.screen
}
ThresholdsSubTab {}
PollingSubTab {}
}
}
@@ -11,6 +11,9 @@ import qs.Widgets
SmartPanel {
id: root
Component.onCompleted: SystemStatService.registerComponent("panel-systemstats")
Component.onDestruction: SystemStatService.unregisterComponent("panel-systemstats")
preferredWidth: Math.round(440 * Style.uiScaleRatio)
panelContent: Item {
@@ -137,7 +140,7 @@ SmartPanel {
color2: Color.mSecondary
fill: true
fillOpacity: 0.15
updateInterval: Settings.data.systemMonitor.cpuPollingInterval
updateInterval: SystemStatService.cpuIntervalMs
edgeToEdge: true
}
}
@@ -193,7 +196,7 @@ SmartPanel {
color: Color.mPrimary
fill: true
fillOpacity: 0.15
updateInterval: Settings.data.systemMonitor.memPollingInterval
updateInterval: SystemStatService.memIntervalMs
edgeToEdge: true
}
}
@@ -267,7 +270,7 @@ SmartPanel {
color2: Color.mSecondary
fill: true
fillOpacity: 0.15
updateInterval: Settings.data.systemMonitor.networkPollingInterval
updateInterval: SystemStatService.networkIntervalMs
animateScale: true
edgeToEdge: true
}
+123 -107
View File
@@ -5,18 +5,37 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services.UI
Singleton {
id: root
// Configuration
readonly property int minimumIntervalMs: 250
readonly property int defaultIntervalMs: 3000
function normalizeInterval(value) {
return Math.max(minimumIntervalMs, value || defaultIntervalMs);
// Component registration - only poll when something needs system stat data
function registerComponent(componentId) {
root._registered[componentId] = true;
root._registered = Object.assign({}, root._registered);
Logger.d("SystemStat", "Component registered:", componentId, "- total:", root._registeredCount);
}
function unregisterComponent(componentId) {
delete root._registered[componentId];
root._registered = Object.assign({}, root._registered);
Logger.d("SystemStat", "Component unregistered:", componentId, "- total:", root._registeredCount);
}
property var _registered: ({})
readonly property int _registeredCount: Object.keys(_registered).length
readonly property bool _lockScreenActive: PanelService.lockScreen?.active ?? false
readonly property bool shouldRun: _registeredCount > 0 && !_lockScreenActive
// Polling intervals (hardcoded to sensible values per stat type)
readonly property int cpuIntervalMs: 3000
readonly property int memIntervalMs: 5000
readonly property int networkIntervalMs: 3000
readonly property int loadAvgIntervalMs: 10000
readonly property int diskIntervalMs: 30000
readonly property int gpuIntervalMs: 5000
// Public values
property real cpuUsage: 0
property real cpuTemp: 0
@@ -52,11 +71,11 @@ Singleton {
readonly property int historyDurationMs: (1 * 60 * 1000) // 1 minute
// Computed history lengths based on polling intervals
readonly property int cpuHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.cpuPollingInterval))
readonly property int gpuHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.gpuPollingInterval))
readonly property int memHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.memPollingInterval))
readonly property int diskHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.diskPollingInterval))
readonly property int networkHistoryLength: Math.ceil(historyDurationMs / normalizeInterval(Settings.data.systemMonitor.networkPollingInterval))
readonly property int cpuHistoryLength: Math.ceil(historyDurationMs / cpuIntervalMs)
readonly property int gpuHistoryLength: Math.ceil(historyDurationMs / gpuIntervalMs)
readonly property int memHistoryLength: Math.ceil(historyDurationMs / memIntervalMs)
readonly property int diskHistoryLength: Math.ceil(historyDurationMs / diskIntervalMs)
readonly property int networkHistoryLength: Math.ceil(historyDurationMs / networkIntervalMs)
property var cpuHistory: new Array(cpuHistoryLength).fill(0)
property var cpuTempHistory: new Array(cpuHistoryLength).fill(40) // Reasonable default temp
@@ -263,22 +282,38 @@ Singleton {
// --------------------------------------------
Component.onCompleted: {
Logger.i("SystemStat", "Service started with custom polling intervals");
Logger.i("SystemStat", "Service started (polling deferred until a consumer registers).");
// Kickoff the cpu name detection for temperature
// Kickoff the cpu name detection for temperature (one-time probes, not polling)
cpuTempNameReader.checkNext();
// Kickoff the gpu sensor detection for temperature
// Kickoff the gpu sensor detection for temperature (one-time probes, not polling)
gpuTempNameReader.checkNext();
// Check for ZFS ARC stats on startup
zfsArcStatsFile.reload();
// Get nproc on startup
// Get nproc on startup (one-time)
nprocProcess.running = true;
}
// Get initial load average
loadAvgFile.reload();
onShouldRunChanged: {
if (shouldRun) {
// Reset differential state so first readings after resume are clean
root.prevCpuStats = null;
root.prevTime = 0;
// Trigger initial reads
zfsArcStatsFile.reload();
loadAvgFile.reload();
// Start persistent disk shell
if (!dfShell.running) {
dfShell.running = true;
}
} else {
// Stop persistent disk shell
if (dfShell.running) {
dfShell.running = false;
}
}
}
// Re-run GPU detection when dGPU opt-in setting changes
@@ -318,18 +353,13 @@ Singleton {
// Timer for CPU usage, frequency, and temperature
Timer {
id: cpuTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.cpuPollingInterval)
interval: root.cpuIntervalMs
repeat: true
running: true
running: root.shouldRun
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
}
}
onTriggered: {
cpuStatFile.reload();
cpuFreqProcess.running = true;
cpuInfoFile.reload();
updateCpuTemperature();
}
}
@@ -337,30 +367,20 @@ Singleton {
// Timer for load average
Timer {
id: loadAvgTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.loadAvgPollingInterval)
interval: root.loadAvgIntervalMs
repeat: true
running: true
running: root.shouldRun
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
}
}
onTriggered: loadAvgFile.reload()
}
// Timer for memory stats
Timer {
id: memoryTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.memPollingInterval)
interval: root.memIntervalMs
repeat: true
running: true
running: root.shouldRun
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
}
}
onTriggered: {
memInfoFile.reload();
zfsArcStatsFile.reload();
@@ -370,45 +390,34 @@ Singleton {
// Timer for disk usage
Timer {
id: diskTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.diskPollingInterval)
interval: root.diskIntervalMs
repeat: true
running: true
running: root.shouldRun
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
onTriggered: {
if (dfShell.running) {
dfShell.write("df --output=target,pcent,used,size,avail --block-size=1 -x efivarfs 2>/dev/null; echo '@@DF_END@@'\n");
}
}
onTriggered: dfProcess.running = true
}
// Timer for network speeds
Timer {
id: networkTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.networkPollingInterval)
interval: root.networkIntervalMs
repeat: true
running: true
running: root.shouldRun
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
}
}
onTriggered: netDevFile.reload()
}
// Timer for GPU temperature
Timer {
id: gpuTempTimer
interval: root.normalizeInterval(Settings.data.systemMonitor.gpuPollingInterval)
interval: root.gpuIntervalMs
repeat: true
running: root.gpuAvailable
running: root.shouldRun && root.gpuAvailable
triggeredOnStart: true
onIntervalChanged: {
if (running) {
restart();
}
}
onTriggered: updateGpuTemperature()
}
@@ -452,17 +461,31 @@ Singleton {
}
// --------------------------------------------
// Process to fetch disk usage (percent, used, size, avail)
// Persistent shell for disk usage queries (avoids fork+exec of large Quickshell process every poll)
// Uses 'df' aka 'disk free'
// "-x efivarfs' skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering
// "-x efivarfs" skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering
// --block-size=1 gives us bytes for precise GB calculation
// Timer writes commands to stdin; SplitParser reads output delimited by @@DF_END@@
Process {
id: dfProcess
command: ["df", "--output=target,pcent,used,size,avail", "--block-size=1", "-x", "efivarfs"]
id: dfShell
command: ["sh"]
stdinEnabled: true
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split('\n');
onRunningChanged: {
if (!running && root.shouldRun) {
// Restart if it died unexpectedly while we still need it
Logger.w("SystemStat", "Disk shell exited unexpectedly, restarting");
Qt.callLater(() => {
dfShell.running = true;
});
}
}
stdout: SplitParser {
splitMarker: "@@DF_END@@"
onRead: data => {
const lines = data.trim().split('\n');
const newPercents = {};
const newAvailPercents = {};
const newUsedGb = {};
@@ -508,49 +531,42 @@ Singleton {
}
}
// Process to get avg cpu frquency
Process {
id: cpuFreqProcess
command: ["cat", "/proc/cpuinfo"]
running: false
stdout: StdioCollector {
onStreamFinished: {
let txt = text;
let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g);
if (matches && matches.length > 0) {
let totalFreq = 0.0;
for (let i = 0; i < matches.length; i++) {
totalFreq += parseFloat(matches[i].split(":")[1]);
}
let avgFreq = (totalFreq / matches.length) / 1000.0;
root.cpuFreq = avgFreq.toFixed(1) + "GHz";
cpuMaxFreqProcess.running = true;
if (avgFreq > root.cpuGlobalMaxFreq)
root.cpuGlobalMaxFreq = avgFreq;
if (root.cpuGlobalMaxFreq > 0) {
root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq);
}
// FileView to get avg cpu frequency (replaces subprocess spawn of `cat /proc/cpuinfo`)
FileView {
id: cpuInfoFile
path: "/proc/cpuinfo"
onLoaded: {
let txt = text();
let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g);
if (matches && matches.length > 0) {
let totalFreq = 0.0;
for (let i = 0; i < matches.length; i++) {
totalFreq += parseFloat(matches[i].split(":")[1]);
}
let avgFreq = (totalFreq / matches.length) / 1000.0;
root.cpuFreq = avgFreq.toFixed(1) + "GHz";
cpuMaxFreqFile.reload();
if (avgFreq > root.cpuGlobalMaxFreq)
root.cpuGlobalMaxFreq = avgFreq;
if (root.cpuGlobalMaxFreq > 0) {
root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq);
}
}
}
}
// Process to get maximum CPU frequency limit
// Uses sysfs 'scaling_max_freq' to respect power profiles (e.g. power-profiles-daemon)
// 'sort -nr | head -n1' ensures we get the highest limit across all cores
// '2>/dev/null' ignores errors if cpufreq driver is missing or cores are offline
Process {
id: cpuMaxFreqProcess
command: ["sh", "-c", "cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq 2>/dev/null | sort -nr | head -n1"]
running: false
stdout: StdioCollector {
onStreamFinished: {
let maxKHz = parseInt(text.trim());
if (!isNaN(maxKHz) && maxKHz > 0) {
let newMaxFreq = maxKHz / 1000000.0;
if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) {
root.cpuGlobalMaxFreq = newMaxFreq;
}
// FileView to get maximum CPU frequency limit (replaces subprocess spawn)
// Reads cpu0's scaling_max_freq as representative value
FileView {
id: cpuMaxFreqFile
path: "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq"
printErrors: false
onLoaded: {
let maxKHz = parseInt(text().trim());
if (!isNaN(maxKHz) && maxKHz > 0) {
let newMaxFreq = maxKHz / 1000000.0;
if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) {
root.cpuGlobalMaxFreq = newMaxFreq;
}
}
}