From 2801add7e2bffbbea258b07183840df9b855f2d3 Mon Sep 17 00:00:00 2001 From: Bharath Vignesh J K <52282402+RazCrimson@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:33:05 +0530 Subject: [PATCH] fix: cpu_percent - initialization timer bugs fixes #2859 --- glances/cpu_percent.py | 121 +++++++++++++++++------------ glances/plugins/percpu/__init__.py | 18 ++--- 2 files changed, 79 insertions(+), 60 deletions(-) diff --git a/glances/cpu_percent.py b/glances/cpu_percent.py index baa3b359a7..4ae0f1b0b5 100644 --- a/glances/cpu_percent.py +++ b/glances/cpu_percent.py @@ -8,16 +8,42 @@ """CPU percent stats shared between CPU and Quicklook plugins.""" +from typing import List, Optional, TypedDict + import psutil from glances.logger import logger 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: """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 # since last update is passed (will retrieve old cached info instead) self.cached_timer_cpu = cached_timer_cpu @@ -27,21 +53,21 @@ def __init__(self, cached_timer_cpu=2): # Get CPU name 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 # 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. self.timer_cpu = Timer(0) - self.cpu_percent = self.get_cpu() + self.cpu_percent = self._compute_cpu() self.timer_percpu = Timer(0) - self.percpu_percent = self.get_percpu() + self.percpu_percent = self._compute_percpu() def get_key(self): """Return the key of the per CPU list.""" return 'cpu_number' - def get_info(self): + def get_info(self) -> CpuInfo: """Get additional information about the CPU""" # Never update more than 1 time per cached_timer_cpu_info if self.timer_cpu_info.finished() and hasattr(psutil, 'cpu_freq'): @@ -63,70 +89,67 @@ def get_info(self): self.timer_cpu_info.reset(duration=self.cached_timer_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 # Read the first line with the "model name" ("Model" for Raspberry Pi) - ret = None try: - cpuinfo_file = open('/proc/cpuinfo').readlines() + cpuinfo_lines = open('/proc/cpuinfo').readlines() except (FileNotFoundError, PermissionError): - pass - else: - 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): + logger.debug("No permission to read '/proc/cpuinfo'") + return 'CPU' + + 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.""" # Never update more than 1 time per cached_timer_cpu if self.timer_cpu.finished(): # Reset timer for cache self.timer_cpu.reset(duration=self.cached_timer_cpu) # Update the stats - self.cpu_percent = psutil.cpu_percent(interval=0.0) + self.cpu_percent = self._compute_cpu() 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.""" # Never update more than 1 time per cached_timer_cpu if self.timer_percpu.finished(): # Reset timer for cache 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 - self.percpu_percent = percpu_percent + self.percpu_percent = self._compute_percpu() 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 cpu_percent = CpuPercent() diff --git a/glances/plugins/percpu/__init__.py b/glances/plugins/percpu/__init__.py index bf52446d29..285ca44062 100644 --- a/glances/plugins/percpu/__init__.py +++ b/glances/plugins/percpu/__init__.py @@ -78,7 +78,6 @@ }, } - # Define the history items list items_history_list = [ {'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'}, @@ -142,7 +141,10 @@ def msg_curse(self, args=None, max_width=None): return ret # 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 if self.is_disabled('quicklook'): @@ -152,8 +154,6 @@ def msg_curse(self, args=None, max_width=None): # Per CPU stats displayed per line for stat in header: - if stat not in self.stats[0]: - continue msg = f'{stat:>7}' ret.append(self.curse_add_line(msg)) @@ -179,8 +179,6 @@ def msg_curse(self, args=None, max_width=None): msg = '{:4} '.format('?') ret.append(self.curse_add_line(msg)) for stat in header: - if stat not in self.stats[0]: - continue try: msg = f'{cpu[stat]:6.1f}%' except TypeError: @@ -192,12 +190,10 @@ def msg_curse(self, args=None, max_width=None): ret.append(self.curse_new_line()) if self.is_disabled('quicklook'): ret.append(self.curse_add_line('CPU* ')) + for stat in header: - if stat not in self.stats[0]: - continue - 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]] - ) + percpu_stats = [i[stat] for i in percpu_list[0 : self.max_cpu_display]] + cpu_stat = sum(percpu_stats) / len(percpu_stats) try: msg = f'{cpu_stat:6.1f}%' except TypeError: