fix: cpu_percent - initialization timer bugs

fixes #2859
This commit is contained in:
Bharath Vignesh J K 2024-07-01 09:33:05 +05:30
parent 9b853d857b
commit 2801add7e2
2 changed files with 78 additions and 59 deletions

View File

@ -8,16 +8,42 @@
"""CPU percent stats shared between CPU and Quicklook plugins.""" """CPU percent stats shared between CPU and Quicklook plugins."""
from typing import List, Optional, TypedDict
import psutil import psutil
from glances.logger import logger from glances.logger import logger
from glances.timer import Timer from glances.timer import Timer
__all__ = ["cpu_percent"]
class CpuInfo(TypedDict):
cpu_name: str
cpu_hz: Optional[float]
cpu_hz_current: Optional[float]
class PerCpuPercentInfo(TypedDict):
key: str
cpu_number: int
total: float
user: float
system: float
idle: float
nice: Optional[float]
iowait: Optional[float]
irq: Optional[float]
softirq: Optional[float]
steal: Optional[float]
guest: Optional[float]
guest_nice: Optional[float]
class CpuPercent: class CpuPercent:
"""Get and store the CPU percent.""" """Get and store the CPU percent."""
def __init__(self, cached_timer_cpu=2): def __init__(self, cached_timer_cpu: int = 2):
# cached_timer_cpu is the minimum time interval between stats updates # cached_timer_cpu is the minimum time interval between stats updates
# since last update is passed (will retrieve old cached info instead) # since last update is passed (will retrieve old cached info instead)
self.cached_timer_cpu = cached_timer_cpu self.cached_timer_cpu = cached_timer_cpu
@ -27,21 +53,21 @@ class CpuPercent:
# Get CPU name # Get CPU name
self.timer_cpu_info = Timer(0) self.timer_cpu_info = Timer(0)
self.cpu_info = {'cpu_name': self.__get_cpu_name(), 'cpu_hz_current': None, 'cpu_hz': None} self.cpu_info: CpuInfo = {'cpu_name': self.__get_cpu_name(), 'cpu_hz_current': None, 'cpu_hz': None}
# Warning from PsUtil documentation # Warning from PsUtil documentation
# The first time this function is called with interval = 0.0 or None # The first time this function is called with interval = 0.0 or None
# it will return a meaningless 0.0 value which you are supposed to ignore. # it will return a meaningless 0.0 value which you are supposed to ignore.
self.timer_cpu = Timer(0) self.timer_cpu = Timer(0)
self.cpu_percent = self.get_cpu() self.cpu_percent = self._compute_cpu()
self.timer_percpu = Timer(0) self.timer_percpu = Timer(0)
self.percpu_percent = self.get_percpu() self.percpu_percent = self._compute_percpu()
def get_key(self): def get_key(self):
"""Return the key of the per CPU list.""" """Return the key of the per CPU list."""
return 'cpu_number' return 'cpu_number'
def get_info(self): def get_info(self) -> CpuInfo:
"""Get additional information about the CPU""" """Get additional information about the CPU"""
# Never update more than 1 time per cached_timer_cpu_info # Never update more than 1 time per cached_timer_cpu_info
if self.timer_cpu_info.finished() and hasattr(psutil, 'cpu_freq'): if self.timer_cpu_info.finished() and hasattr(psutil, 'cpu_freq'):
@ -63,70 +89,67 @@ class CpuPercent:
self.timer_cpu_info.reset(duration=self.cached_timer_cpu_info) self.timer_cpu_info.reset(duration=self.cached_timer_cpu_info)
return self.cpu_info return self.cpu_info
def __get_cpu_name(self): @staticmethod
def __get_cpu_name() -> str:
# Get the CPU name once from the /proc/cpuinfo file # Get the CPU name once from the /proc/cpuinfo file
# Read the first line with the "model name" ("Model" for Raspberry Pi) # Read the first line with the "model name" ("Model" for Raspberry Pi)
ret = None
try: try:
cpuinfo_file = open('/proc/cpuinfo').readlines() cpuinfo_lines = open('/proc/cpuinfo').readlines()
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
pass logger.debug("No permission to read '/proc/cpuinfo'")
else: return 'CPU'
for line in cpuinfo_file:
if line.startswith('model name') or line.startswith('Model') or line.startswith('cpu model'):
ret = line.split(':')[1].strip()
break
return ret if ret else 'CPU'
def get_cpu(self): for line in cpuinfo_lines:
if line.startswith('model name') or line.startswith('Model') or line.startswith('cpu model'):
return line.split(':')[1].strip()
return 'CPU'
def get_cpu(self) -> float:
"""Update and/or return the CPU using the psutil library.""" """Update and/or return the CPU using the psutil library."""
# Never update more than 1 time per cached_timer_cpu # Never update more than 1 time per cached_timer_cpu
if self.timer_cpu.finished(): if self.timer_cpu.finished():
# Reset timer for cache # Reset timer for cache
self.timer_cpu.reset(duration=self.cached_timer_cpu) self.timer_cpu.reset(duration=self.cached_timer_cpu)
# Update the stats # Update the stats
self.cpu_percent = psutil.cpu_percent(interval=0.0) self.cpu_percent = self._compute_cpu()
return self.cpu_percent return self.cpu_percent
def get_percpu(self): @staticmethod
def _compute_cpu() -> float:
return psutil.cpu_percent(interval=0.0)
def get_percpu(self) -> List[PerCpuPercentInfo]:
"""Update and/or return the per CPU list using the psutil library.""" """Update and/or return the per CPU list using the psutil library."""
# Never update more than 1 time per cached_timer_cpu # Never update more than 1 time per cached_timer_cpu
if self.timer_percpu.finished(): if self.timer_percpu.finished():
# Reset timer for cache # Reset timer for cache
self.timer_percpu.reset(duration=self.cached_timer_cpu) self.timer_percpu.reset(duration=self.cached_timer_cpu)
# Get stats
percpu_percent = []
psutil_percpu = enumerate(psutil.cpu_times_percent(interval=0.0, percpu=True))
for cpu_number, cputimes in psutil_percpu:
cpu = {
'key': self.get_key(),
'cpu_number': cpu_number,
'total': round(100 - cputimes.idle, 1),
'user': cputimes.user,
'system': cputimes.system,
'idle': cputimes.idle,
}
# The following stats are for API purposes only
if hasattr(cputimes, 'nice'):
cpu['nice'] = cputimes.nice
if hasattr(cputimes, 'iowait'):
cpu['iowait'] = cputimes.iowait
if hasattr(cputimes, 'irq'):
cpu['irq'] = cputimes.irq
if hasattr(cputimes, 'softirq'):
cpu['softirq'] = cputimes.softirq
if hasattr(cputimes, 'steal'):
cpu['steal'] = cputimes.steal
if hasattr(cputimes, 'guest'):
cpu['guest'] = cputimes.guest
if hasattr(cputimes, 'guest_nice'):
cpu['guest_nice'] = cputimes.guest_nice
# Append new CPU to the list
percpu_percent.append(cpu)
# Update stats # Update stats
self.percpu_percent = percpu_percent self.percpu_percent = self._compute_percpu()
return self.percpu_percent return self.percpu_percent
def _compute_percpu(self) -> List[PerCpuPercentInfo]:
psutil_percpu = enumerate(psutil.cpu_times_percent(interval=0.0, percpu=True))
return [
{
'key': self.get_key(),
'cpu_number': cpu_number,
'total': round(100 - cpu_times.idle, 1),
'user': cpu_times.user,
'system': cpu_times.system,
'idle': cpu_times.idle,
'nice': cpu_times.nice if hasattr(cpu_times, 'nice') else None,
'iowait': cpu_times.iowait if hasattr(cpu_times, 'iowait') else None,
'irq': cpu_times.irq if hasattr(cpu_times, 'irq') else None,
'softirq': cpu_times.softirq if hasattr(cpu_times, 'softirq') else None,
'steal': cpu_times.steal if hasattr(cpu_times, 'steal') else None,
'guest': cpu_times.guest if hasattr(cpu_times, 'guest') else None,
'guest_nice': cpu_times.steal if hasattr(cpu_times, 'guest_nice') else None,
}
for cpu_number, cpu_times in psutil_percpu
]
# CpuPercent instance shared between plugins # CpuPercent instance shared between plugins
cpu_percent = CpuPercent() cpu_percent = CpuPercent()

View File

@ -78,7 +78,6 @@ guest operating systems under the control of the Linux kernel.',
}, },
} }
# Define the history items list # Define the history items list
items_history_list = [ items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'}, {'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
@ -142,7 +141,10 @@ class PluginModel(GlancesPluginModel):
return ret return ret
# Define the default header # Define the default header
header = ['user', 'system', 'idle', 'iowait', 'steal'] all_headers = ['user', 'system', 'idle', 'iowait', 'steal']
# Determine applicable headers
header = [h for h in all_headers if self.stats[0].get(h) is not None]
# Build the string message # Build the string message
if self.is_disabled('quicklook'): if self.is_disabled('quicklook'):
@ -152,8 +154,6 @@ class PluginModel(GlancesPluginModel):
# Per CPU stats displayed per line # Per CPU stats displayed per line
for stat in header: for stat in header:
if stat not in self.stats[0]:
continue
msg = f'{stat:>7}' msg = f'{stat:>7}'
ret.append(self.curse_add_line(msg)) ret.append(self.curse_add_line(msg))
@ -179,8 +179,6 @@ class PluginModel(GlancesPluginModel):
msg = '{:4} '.format('?') msg = '{:4} '.format('?')
ret.append(self.curse_add_line(msg)) ret.append(self.curse_add_line(msg))
for stat in header: for stat in header:
if stat not in self.stats[0]:
continue
try: try:
msg = f'{cpu[stat]:6.1f}%' msg = f'{cpu[stat]:6.1f}%'
except TypeError: except TypeError:
@ -192,12 +190,10 @@ class PluginModel(GlancesPluginModel):
ret.append(self.curse_new_line()) ret.append(self.curse_new_line())
if self.is_disabled('quicklook'): if self.is_disabled('quicklook'):
ret.append(self.curse_add_line('CPU* ')) ret.append(self.curse_add_line('CPU* '))
for stat in header: for stat in header:
if stat not in self.stats[0]: percpu_stats = [i[stat] for i in percpu_list[0 : self.max_cpu_display]]
continue cpu_stat = sum(percpu_stats) / len(percpu_stats)
cpu_stat = sum([i[stat] for i in percpu_list[0 : self.max_cpu_display]]) / len(
[i[stat] for i in percpu_list[0 : self.max_cpu_display]]
)
try: try:
msg = f'{cpu_stat:6.1f}%' msg = f'{cpu_stat:6.1f}%'
except TypeError: except TypeError: