sane-sysload: implement CPU measurement

This commit is contained in:
Colin 2024-06-19 01:58:21 +00:00
parent 91c2b04ab4
commit b3c5e53156

View File

@ -16,15 +16,19 @@ formatstr is a Python format string.
variables available for formatting:
- {bat_icon}
- {bat_time}
- {cpu_icon}
- {cpu_pct}
- {mem_icon}
- {mem_pct}
and some presets, encapsulating the above:
- {bat}
- {cpu}
- {mem}
"""
import argparse
import logging
import time
from dataclasses import dataclass
from enum import Enum
@ -34,6 +38,7 @@ logger = logging.getLogger(__name__)
# these icons may only render in nerdfonts
ICON_BAT_CHG = ["󰢟", "󱊤", "󱊥", "󰂅"]
ICON_BAT_DIS = ["󰂎", "󱊡", "󱊢", "󱊣"]
ICON_CPU=""
ICON_MEM="☵"
SUFFIX_ICON = "" # thin space
SUFFIX_PERCENT = "%"
@ -82,6 +87,9 @@ class Formatter:
def render_charge_icon(self, direction: ChargeDirection, percentage: float) -> str:
return f"{self._choose_icon(direction, percentage)}{self.suffix_icon}"
def render_cpu_icon(self) -> str:
return f"{ICON_CPU}{self.suffix_icon}"
def render_mem_icon(self) -> str:
return f"{ICON_MEM}{self.suffix_icon}"
@ -147,6 +155,83 @@ class MemInfo:
logger.debug(f"/proc/meminfo: {entry}={v}")
return v
class ProcStat:
"""
reads vaues from /proc/stat, mostly CPU-related.
"""
# /proc/stat format is documented here: <https://www.linuxhowtos.org/System/procstat.htm>
# these are AGGREGATES SINCE SYSTEM BOOT
# to measure current CPU usage, need to take multiple samples
# cpu <user> <system> <nice> <idle> <iowait> <irg> <softirq> 0 0 0
# (what are the last three fields?)
# where:
# measurements are in units of jiffies or USER_HZ
# user: normal processes executing in user mode
# nice: niced processes executing in user mode
# system: processes executing in kernel mode
# idle: twiddling thumbs
# iowait: waiting for I/O to complete
# irq: servicing interrupts
# softirq: servicing softirqs
def __init__(self, entries=None):
if entries is not None:
self.entries = entries
return
# else, read from procfs...
try:
lines = open("/proc/stat").readlines()
except Exception as e:
logger.info(f"failed to open /proc/stat: {e}")
lines = []
self.entries = {}
for l in lines:
pieces = l.strip().split(" ")
name, values = pieces[0], [p for p in pieces[1:] if p]
if name:
self.entries[name] = [int(v) for v in values]
@staticmethod
def sample(seconds: float = 1.0):
sample1 = ProcStat()
time.sleep(seconds)
sample2 = ProcStat()
return sample2 - sample1
def __sub__(self, other: 'ProcStat') -> 'ProcStat':
entries = {}
for k in self.entries:
entries[k] = [i - j for i, j in zip(self.entries[k], other.entries[k])]
return ProcStat(entries)
@property
def cpu_user(self) -> int:
return self.entries["cpu"][0]
@property
def cpu_system(self) -> int:
return self.entries["cpu"][1]
@property
def cpu_nice(self) -> int:
return self.entries["cpu"][2]
@property
def cpu_idle(self) -> int:
return self.entries["cpu"][3]
@property
def cpu_iowait(self) -> int:
return self.entries["cpu"][4]
@property
def cpu_irq(self) -> int:
return self.entries["cpu"][5]
@property
def cpu_softirq(self) -> int:
return self.entries["cpu"][6]
@property
def cpu_total(self) -> int:
# TODO: not sure if i'm supposed to include irq stuff here?
return self.cpu_user + self.cpu_system + self.cpu_nice + self.cpu_idle + self.cpu_iowait + self.cpu_irq + self.cpu_softirq
class PowerSupply:
"""
reads values from /sys/class/power_supply/$dev/ API
@ -273,16 +358,39 @@ def try_all_batteries() -> BatteryInfo | None:
@dataclass
class AllInfo:
_fmt: Formatter
_mem: MemInfo | None
_bat: BatteryInfo | None
__bat: BatteryInfo | None = None
__cpu: ProcStat | None = None
__mem: MemInfo | None = None
# lazy-loading
@property
def _bat(self):
if self.__bat is None:
self.__bat = try_all_batteries()
return self.__bat
@property
def _cpu(self):
if self.__cpu is None:
self.__cpu = ProcStat.sample()
return self.__cpu
@property
def _mem(self):
if self.__mem is None:
self.__mem = MemInfo()
return self.__mem
# user-facing format shorthands
@property
def bat(self) -> str:
return f"{self.bat_icon}{self.bat_time}"
@property
def cpu(self) -> str:
return f"{self.cpu_icon}{self.cpu_pct}"
@property
def mem(self) -> str:
return f"{self.mem_icon}{self.mem_pct}"
# manual/low-level fields
@property
def mem_icon(self) -> str:
if self._mem is None: return ""
@ -324,6 +432,23 @@ class AllInfo:
else:
return self._fmt.render_percent(self._bat.percent_charged)
@property
def cpu_icon(self) -> str:
if self._cpu is None: return ""
return self._fmt.render_cpu_icon()
@property
def cpu_pct(self) -> str:
if self._cpu is None:
return ""
idle = self._cpu.cpu_idle + self._cpu.cpu_iowait
total = self._cpu.cpu_total
cpu_use_pct = int((total - idle) / total * 100)
return self._fmt.render_percent(cpu_use_pct)
class LazyFormatter:
def __init__(self, obj: object, attr: str):
self.obj = obj
@ -357,15 +482,14 @@ def main() -> None:
f.suffix_hr = args.hour_suffix
f.suffix_min = args.minute_suffix
info = AllInfo(
f,
MemInfo(),
try_all_batteries(),
)
info = AllInfo(f)
print(args.formatstr.format(
bat=LazyFormatter(info, "bat"),
bat_icon=LazyFormatter(info, "bat_icon"),
bat_time=LazyFormatter(info, "bat_time"),
cpu=LazyFormatter(info, "cpu"),
cpu_icon=LazyFormatter(info, "cpu_icon"),
cpu_pct=LazyFormatter(info, "cpu_pct"),
mem=LazyFormatter(info, "mem"),
mem_icon=LazyFormatter(info, "mem_icon"),
mem_pct=LazyFormatter(info, "mem_pct"),