Add CPU util

This commit is contained in:
2026-03-02 02:24:32 +09:00
parent 4aa72bbf4d
commit 2943655575
4 changed files with 226 additions and 2 deletions

View File

@@ -0,0 +1,134 @@
import GLib from 'gi://GLib';
export default class CpuUsageUtil {
constructor(intervalSeconds = 2) {
this._updated = false;
this._readings = [];
this._prevSnapshot = null;
this._intervalUsec = 0;
this._lastUpdateUsec = 0;
this.interval = intervalSeconds;
// Take initial snapshot so the first real execute() can compute a delta
this._prevSnapshot = this._readProcStat();
}
get available() {
return this._prevSnapshot !== null;
}
get updated() {
return this._updated;
}
set updated(updated) {
this._updated = updated;
}
execute(callback) {
const nowUsec = GLib.get_monotonic_time();
const remaining = this._intervalUsec - (nowUsec - this._lastUpdateUsec);
if (this._intervalUsec > 0 && this._lastUpdateUsec !== 0 && remaining > 0) {
if (callback)
callback();
return;
}
this._updated = false;
try {
const snapshot = this._readProcStat();
if (!snapshot || !this._prevSnapshot) {
this._prevSnapshot = snapshot;
return;
}
this._readings = [];
for (const [cpu, fields] of snapshot) {
const prev = this._prevSnapshot.get(cpu);
if (!prev)
continue;
const idle = (fields.idle + fields.iowait) - (prev.idle + prev.iowait);
const total = fields.total - prev.total;
if (total <= 0)
continue;
const usage = 100.0 * (1.0 - idle / total);
this._readings.push({
label: cpu === 'cpu' ? 'CPU Total' : cpu.toUpperCase().replace('CPU', 'CPU '),
usage: Math.max(0, Math.min(100, usage)),
});
}
this._prevSnapshot = snapshot;
} catch (e) {
logError(e, '[FREON] Failed to read CPU usage');
this._readings = [];
} finally {
this._lastUpdateUsec = nowUsec;
this._updated = true;
if (callback)
callback();
}
}
get usage() {
return this._readings;
}
destroy() {
this._readings = [];
this._prevSnapshot = null;
this._lastUpdateUsec = 0;
}
set interval(seconds) {
const clamped = Math.max(1, seconds | 0);
this._intervalUsec = clamped * 1000000;
this._lastUpdateUsec = 0;
}
_readProcStat() {
let ok, contents;
try {
[ok, contents] = GLib.file_get_contents('/proc/stat');
} catch (e) {
return null;
}
if (!ok)
return null;
const text = new TextDecoder().decode(contents);
const result = new Map();
for (const line of text.split('\n')) {
if (!line.startsWith('cpu'))
continue;
const parts = line.trim().split(/\s+/);
const name = parts[0];
// user nice system idle iowait irq softirq steal
const user = parseInt(parts[1]) || 0;
const nice = parseInt(parts[2]) || 0;
const system = parseInt(parts[3]) || 0;
const idle = parseInt(parts[4]) || 0;
const iowait = parseInt(parts[5]) || 0;
const irq = parseInt(parts[6]) || 0;
const softirq = parseInt(parts[7]) || 0;
const steal = parseInt(parts[8]) || 0;
const total = user + nice + system + idle + iowait + irq + softirq + steal;
result.set(name, { idle, iowait, total });
}
return result.size > 0 ? result : null;
}
}

View File

@@ -25,6 +25,7 @@ import SmartctlUtil from './smartctlUtil.js';
import NvmecliUtil from './nvmecliUtil.js';
import BatteryUtil from './batteryUtil.js';
import WattdUtil from './wattdUtil.js';
import CpuUsageUtil from './cpuUsageUtil.js';
import FreonItem from './freonItem.js';
@@ -80,6 +81,7 @@ class FreonMenuButton extends PanelMenu.Button {
this._initLiquidctlUtility();
this._initBatteryUtility();
this._initWattdUtility();
this._initCpuUsageUtility();
this._initNvidiaUtility();
this._initBumblebeeNvidiaUtility();
@@ -104,6 +106,7 @@ class FreonMenuButton extends PanelMenu.Button {
'fan' : Gio.icon_new_for_string(path + '/icons/freon-fan-symbolic.svg'),
'power' : voltageIcon,
'battery' : batteryIcon,
'cpu-usage' : Gio.icon_new_for_string('utilities-system-monitor-symbolic'),
}
this._menuLayout = new St.BoxLayout();
@@ -145,6 +148,8 @@ class FreonMenuButton extends PanelMenu.Button {
this._addSettingChangedSignal('use-generic-lmsensors', this._sensorsUtilityChanged.bind(this));
this._addSettingChangedSignal('freeimpi-selected', this._freeipmiUtilityChanged.bind(this));
this._addSettingChangedSignal('use-generic-liquidctl', this._liquidctlUtilityChanged.bind(this));
this._addSettingChangedSignal('use-cpu-usage', this._cpuUsageUtilityChanged.bind(this));
this._addSettingChangedSignal('cpu-usage-update-time', this._cpuUsageUpdateTimeChanged.bind(this));
this._addSettingChangedSignal('use-generic-wattd', this._wattdUtilityChanged.bind(this));
this._addSettingChangedSignal('show-battery-stats', this._batteryUtilityChanged.bind(this));
@@ -161,6 +166,7 @@ class FreonMenuButton extends PanelMenu.Button {
this._addSettingChangedSignal('show-rotationrate', this._rerender.bind(this));
this._addSettingChangedSignal('show-voltage', this._rerender.bind(this));
this._addSettingChangedSignal('show-power', this._rerender.bind(this));
this._addSettingChangedSignal('show-cpu-usage', this._rerender.bind(this));
this._addSettingChangedSignal('group-temperature', this._rerender.bind(this))
@@ -392,6 +398,31 @@ class FreonMenuButton extends PanelMenu.Button {
this._querySensors();
}
_initCpuUsageUtility() {
if (this._settings.get_boolean('use-cpu-usage'))
this._utils.cpuUsage = new CpuUsageUtil(this._settings.get_int('cpu-usage-update-time'));
}
_destroyCpuUsageUtility() {
if (this._utils.cpuUsage) {
this._utils.cpuUsage.destroy();
delete this._utils.cpuUsage;
}
}
_cpuUsageUtilityChanged() {
this._destroyCpuUsageUtility();
this._initCpuUsageUtility();
this._querySensors();
this._updateUI(true);
}
_cpuUsageUpdateTimeChanged() {
if (this._utils.cpuUsage)
this._utils.cpuUsage.interval = this._settings.get_int('cpu-usage-update-time');
this._querySensors();
}
_initNvidiaUtility() {
if (this._settings.get_boolean('use-gpu-nvidia'))
this._utils.nvidia = new NvidiaUtil();
@@ -560,6 +591,7 @@ class FreonMenuButton extends PanelMenu.Button {
this._destroyBatteryUtility();
this._destroyWattdUtility();
this._destroyCpuUsageUtility();
GLib.Source.remove(this._timeoutId);
GLib.Source.remove(this._updateUITimeoutId);
@@ -628,6 +660,7 @@ class FreonMenuButton extends PanelMenu.Button {
let fanInfo = [];
let voltageInfo = [];
let powerInfo = [];
let cpuUsageInfo = [];
if (this._utils.sensors && this._utils.sensors.available) {
if (this._settings.get_boolean('show-temperature')) {
@@ -674,6 +707,10 @@ class FreonMenuButton extends PanelMenu.Button {
if (this._settings.get_boolean('show-power'))
powerInfo = powerInfo.concat(this._utils.wattd.power);
if (this._utils.cpuUsage && this._utils.cpuUsage.available)
if (this._settings.get_boolean('show-cpu-usage'))
cpuUsageInfo = cpuUsageInfo.concat(this._utils.cpuUsage.usage);
if (this._utils.nvidia && this._utils.nvidia.available)
if (this._settings.get_boolean('show-temperature'))
gpuTempInfo = gpuTempInfo.concat(this._utils.nvidia.temp);
@@ -708,12 +745,14 @@ class FreonMenuButton extends PanelMenu.Button {
fanInfo.sort(comparator);
voltageInfo.sort(comparator);
powerInfo.sort(comparator);
cpuUsageInfo.sort(comparator);
let tempInfo = gpuTempInfo.concat(sensorsTempInfo).concat(driveTempInfo);
if (tempInfo.length == 0
&& fanInfo.length == 0
&& voltageInfo.length == 0) {
&& voltageInfo.length == 0
&& cpuUsageInfo.length == 0) {
this._sensorMenuItems = {};
this.menu.removeAll();
@@ -848,6 +887,18 @@ class FreonMenuButton extends PanelMenu.Button {
});
}
if (cpuUsageInfo.length > 0 && (fanInfo.length > 0 || voltageInfo.length > 0 || powerInfo.length > 0))
sensors.push({type : 'separator'});
for (let cpu of cpuUsageInfo){
sensors.push({
icon: 'cpu-usage',
type: 'cpu-usage',
label: cpu.label,
value: _("%.1f%%").format(cpu.usage)
});
}
this._fixNames(sensors);
for (let k in this._hotLabels)

View File

@@ -110,6 +110,26 @@ export default class FreonPreferences extends ExtensionPreferences {
width_request: 320,
});
const cpuUsageSwitch = this._addSwitch("CPU Usage", "use-cpu-usage", "Read CPU usage from /proc/stat");
group.add(cpuUsageSwitch);
const cpuUsageInterval = new Adw.SpinRow({
title: _('CPU Usage Polling Interval'),
subtitle: _('Seconds between CPU usage updates'),
adjustment: new Gtk.Adjustment({
lower: 1,
upper: 60,
value: this._settings.get_int('cpu-usage-update-time'),
step_increment: 1,
}),
});
this._settings.bind('cpu-usage-update-time', cpuUsageInterval, 'value', Gio.SettingsBindFlags.DEFAULT);
cpuUsageInterval.sensitive = this._settings.get_boolean('use-cpu-usage');
this._settings.connect('changed::use-cpu-usage', () => {
cpuUsageInterval.sensitive = this._settings.get_boolean('use-cpu-usage');
});
group.add(cpuUsageInterval);
group.add(this._addSwitch("lm-sensors", "use-generic-lmsensors", "Read sensors from lm-sensors"));
group.add(this._addSwitch("liquidctl", "use-generic-liquidctl", "Read sensors from liquidctl (v1.7.0+)"));
const wattdSwitch = this._addSwitch("wattd", "use-generic-wattd", "Read power data from wattd");
@@ -180,6 +200,7 @@ export default class FreonPreferences extends ExtensionPreferences {
group.add(this._addSwitch("Voltage", "show-voltage"));
group.add(this._addSwitch("Power", "show-power"));
group.add(this._addSwitch("Battery", "show-battery-stats"));
group.add(this._addSwitch("CPU Usage", "show-cpu-usage"));
return group
}

View File

@@ -105,6 +105,24 @@
<description>Read sensors from (requires liquidctl v1.7.0 or later)</description>
</key>
<key type="b" name="use-cpu-usage">
<default>true</default>
<summary>Enable CPU usage sensor</summary>
<description>Read CPU usage from /proc/stat</description>
</key>
<key type="b" name="show-cpu-usage">
<default>true</default>
<summary>Show CPU usage</summary>
<description>Display CPU usage percentage</description>
</key>
<key type="i" name="cpu-usage-update-time">
<default>2</default>
<summary>Seconds between CPU usage updates</summary>
<description>Polling interval for CPU usage sensor</description>
</key>
<key type="b" name="use-generic-wattd">
<default>false</default>
<summary>Read power data from wattd</summary>