From dfb4e3bf5508b59dfbf6277612fbc3932e723c70 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Sun, 20 Apr 2025 05:05:42 +0800 Subject: [PATCH] fix(tui/host): ignore errors when collecting host metrics (#163) --- CHANGELOG.md | 2 +- nvitop/tui/library/__init__.py | 22 +++---- nvitop/tui/library/host.py | 101 +++++++++++++++++++++++++++++ nvitop/tui/library/messagebox.py | 2 +- nvitop/tui/library/process.py | 25 ++----- nvitop/tui/library/selection.py | 3 +- nvitop/tui/library/utils.py | 40 ++++++++---- nvitop/tui/screens/main/device.py | 6 +- nvitop/tui/screens/main/host.py | 10 +-- nvitop/tui/screens/main/process.py | 17 ++--- nvitop/tui/screens/metrics.py | 8 +-- pyproject.toml | 9 +++ 12 files changed, 174 insertions(+), 71 deletions(-) create mode 100644 nvitop/tui/library/host.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1321e13..44610b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- +- Ignore errors when collecting host metrics for host panel by [@XuehaiPan](https://github.com/XuehaiPan) in [#163](https://github.com/XuehaiPan/nvitop/pull/163). ### Removed diff --git a/nvitop/tui/library/__init__.py b/nvitop/tui/library/__init__.py index 0a63df1..5566794 100644 --- a/nvitop/tui/library/__init__.py +++ b/nvitop/tui/library/__init__.py @@ -3,7 +3,8 @@ # pylint: disable=missing-module-docstring -from nvitop.tui.library.device import NA, Device +from nvitop.tui.library import host +from nvitop.tui.library.device import Device from nvitop.tui.library.displayable import Displayable, DisplayableContainer from nvitop.tui.library.history import BufferedHistoryGraph, HistoryGraph from nvitop.tui.library.keybinding import ( @@ -19,26 +20,25 @@ from nvitop.tui.library.keybinding import ( from nvitop.tui.library.libcurses import libcurses, setlocale_utf8 from nvitop.tui.library.messagebox import MessageBox, send_signal from nvitop.tui.library.mouse import MouseEvent -from nvitop.tui.library.process import ( - GiB, - GpuProcess, - HostProcess, - Snapshot, - bytes2human, - host, - timedelta2human, -) +from nvitop.tui.library.process import GpuProcess, HostProcess from nvitop.tui.library.selection import Selection from nvitop.tui.library.utils import ( HOSTNAME, LARGE_INTEGER, + NA, SUPERUSER, - USERCONTEXT, + USER_CONTEXT, USERNAME, + WINDOWS, + WSL, + GiB, + Snapshot, + bytes2human, colored, cut_string, make_bar, set_color, + timedelta2human, ttl_cache, ) from nvitop.tui.library.widestring import WideString, wcslen diff --git a/nvitop/tui/library/host.py b/nvitop/tui/library/host.py new file mode 100644 index 0000000..b92e892 --- /dev/null +++ b/nvitop/tui/library/host.py @@ -0,0 +1,101 @@ +# This file is part of nvitop, the interactive NVIDIA-GPU process viewer. +# License: GNU GPL version 3. + +# pylint: disable=missing-module-docstring,missing-function-docstring + +from types import MappingProxyType +from typing import TYPE_CHECKING, NamedTuple + +from nvitop.api import NA, host +from nvitop.api.host import WINDOWS, WSL, AccessDenied, PsutilError + + +__all__ = [ + 'WINDOWS', + 'WSL', + 'AccessDenied', + 'PsutilError', + 'cpu_percent', + 'getuser', + 'hostname', + 'load_average', + 'reverse_ppid_map', + 'swap_memory', + 'uptime', + 'virtual_memory', +] + + +def ignore_error(*, fallback): + """Ignore errors in the function.""" + + def wrapper(func): + def wrapped(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + return fallback + + return wrapped + + return wrapper + + +class VirtualMemory(NamedTuple): # pylint: disable=missing-class-docstring + total: int + available: int + percent: int + used: int + free: int + + +@ignore_error(fallback=VirtualMemory(NA, NA, NA, NA, NA)) +def virtual_memory(): + vm = host.virtual_memory() + return VirtualMemory( + total=vm.total, + available=vm.available, + percent=vm.percent, + used=vm.used, + free=vm.free, + ) + + +class SwapMemory(NamedTuple): # pylint: disable=missing-class-docstring + total: int + used: int + free: int + percent: float + sin: int + sout: int + + +@ignore_error(fallback=SwapMemory(NA, NA, NA, NA, NA, NA)) +def swap_memory(): + sm = host.swap_memory() + return SwapMemory( + total=sm.total, + used=sm.used, + free=sm.free, + percent=sm.percent, + sin=sm.sin, + sout=sm.sout, + ) + + +@ignore_error(fallback=(NA, NA, NA)) +def load_average(): + la = host.load_average() + if la is None: + return (NA, NA, NA) + return la + + +if TYPE_CHECKING: + from nvitop.api.host import cpu_percent, getuser, hostname, reverse_ppid_map, uptime +else: + cpu_percent = ignore_error(fallback=NA)(host.cpu_percent) + getuser = ignore_error(fallback=NA)(host.getuser) + hostname = ignore_error(fallback=NA)(host.hostname) + uptime = ignore_error(fallback=NA)(host.uptime) + reverse_ppid_map = ignore_error(fallback=MappingProxyType({}))(host.reverse_ppid_map) diff --git a/nvitop/tui/library/messagebox.py b/nvitop/tui/library/messagebox.py index a8da955..bf4b0b7 100644 --- a/nvitop/tui/library/messagebox.py +++ b/nvitop/tui/library/messagebox.py @@ -10,9 +10,9 @@ import threading import time from functools import partial +from nvitop.tui.library import host from nvitop.tui.library.displayable import Displayable from nvitop.tui.library.keybinding import normalize_keybinding -from nvitop.tui.library.process import host from nvitop.tui.library.utils import cut_string from nvitop.tui.library.widestring import WideString diff --git a/nvitop/tui/library/process.py b/nvitop/tui/library/process.py index e12fbd0..1f26820 100644 --- a/nvitop/tui/library/process.py +++ b/nvitop/tui/library/process.py @@ -3,29 +3,12 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring -from nvitop.api import ( - NA, - GiB, - HostProcess, - Snapshot, - bytes2human, - host, - timedelta2human, - utilization2string, -) +from nvitop.api import NA, HostProcess, Snapshot, utilization2string from nvitop.api import GpuProcess as GpuProcessBase +from nvitop.api.host import WINDOWS, WSL -__all__ = [ - 'host', - 'HostProcess', - 'GpuProcess', - 'NA', - 'Snapshot', - 'bytes2human', - 'GiB', - 'timedelta2human', -] +__all__ = ['HostProcess', 'GpuProcess'] class GpuProcess(GpuProcessBase): @@ -63,7 +46,7 @@ class GpuProcess(GpuProcessBase): snapshot = super().as_snapshot(host_process_snapshot_cache=host_process_snapshot_cache) snapshot.type = snapshot.type.replace('C+G', 'X') - if snapshot.gpu_memory_human is NA and (host.WINDOWS or host.WSL): + if snapshot.gpu_memory_human is NA and (WINDOWS or WSL): snapshot.gpu_memory_human = 'WDDM:N/A' snapshot.cpu_percent_string = snapshot.host.cpu_percent_string diff --git a/nvitop/tui/library/selection.py b/nvitop/tui/library/selection.py index 8329b7f..0f934ec 100644 --- a/nvitop/tui/library/selection.py +++ b/nvitop/tui/library/selection.py @@ -7,7 +7,8 @@ import signal import time from weakref import WeakValueDictionary -from nvitop.api import NA, Snapshot, host +from nvitop.api import NA, Snapshot +from nvitop.tui.library import host from nvitop.tui.library.utils import LARGE_INTEGER, SUPERUSER, USERNAME diff --git a/nvitop/tui/library/utils.py b/nvitop/tui/library/utils.py index e6824e6..f7ad973 100644 --- a/nvitop/tui/library/utils.py +++ b/nvitop/tui/library/utils.py @@ -7,32 +7,44 @@ import contextlib import math import os -from nvitop.api import NA, colored, host, set_color, ttl_cache +from nvitop.api import ( + NA, + GiB, + Snapshot, + bytes2human, + colored, + set_color, + timedelta2human, + ttl_cache, +) +from nvitop.tui.library.host import WINDOWS, WSL, getuser, hostname from nvitop.tui.library.widestring import WideString __all__ = [ - 'NA', - 'USERNAME', 'HOSTNAME', - 'SUPERUSER', - 'USERCONTEXT', 'LARGE_INTEGER', - 'ttl_cache', + 'NA', + 'SUPERUSER', + 'USER_CONTEXT', + 'USERNAME', + 'GiB', + 'Snapshot', + 'bytes2human', 'colored', - 'set_color', 'cut_string', 'make_bar', + 'set_color', + 'timedelta2human', + 'ttl_cache', ] -USERNAME = 'N/A' -with contextlib.suppress(ImportError, OSError): - USERNAME = host.getuser() +USERNAME = getuser() SUPERUSER = False with contextlib.suppress(AttributeError, OSError): - if host.WINDOWS: + if WINDOWS: import ctypes SUPERUSER = bool(ctypes.windll.shell32.IsUserAnAdmin()) @@ -42,11 +54,11 @@ with contextlib.suppress(AttributeError, OSError): except AttributeError: SUPERUSER = os.getuid() == 0 -HOSTNAME = host.hostname() -if host.WSL: +HOSTNAME = hostname() +if WSL: HOSTNAME = f'{HOSTNAME} (WSL)' -USERCONTEXT = f'{USERNAME}@{HOSTNAME}' +USER_CONTEXT = f'{USERNAME}@{HOSTNAME}' LARGE_INTEGER = 65536 diff --git a/nvitop/tui/screens/main/device.py b/nvitop/tui/screens/main/device.py index f326308..363d44a 100644 --- a/nvitop/tui/screens/main/device.py +++ b/nvitop/tui/screens/main/device.py @@ -8,11 +8,11 @@ import time from nvitop.tui.library import ( NA, + WINDOWS, Device, Displayable, colored, cut_string, - host, make_bar, ttl_cache, ) @@ -90,7 +90,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes '│ {memory_usage:>20} │ BAR1: {bar1_memory_used_human:>8} / {bar1_memory_percent_string:>3} │', ] - if host.WINDOWS: + if WINDOWS: self.formats_full[0] = self.formats_full[0].replace( 'persistence_mode', 'current_driver_model', @@ -218,7 +218,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes '│ Fan Temp Perf Pwr:Usage/Cap│ Memory-Usage │ GPU-Util Compute M. │', ), ) - if host.WINDOWS: + if WINDOWS: header[-2] = header[-2].replace('Persistence-M', ' TCC/WDDM ') if self.support_mig: header[-2] = header[-2].replace('Volatile Uncorr. ECC', 'MIG M. Uncorr. ECC') diff --git a/nvitop/tui/screens/main/host.py b/nvitop/tui/screens/main/host.py index 16f9cd1..f0bc1cf 100644 --- a/nvitop/tui/screens/main/host.py +++ b/nvitop/tui/screens/main/host.py @@ -254,13 +254,9 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes def draw(self): # pylint: disable=too-many-locals,too-many-branches,too-many-statements self.color_reset() - if self.load_average is not None: - load_average = tuple( - f'{value:5.2f}'[:5] if value < 10000.0 else '9999+' for value in self.load_average - ) - else: - load_average = (NA,) * 3 - load_average = 'Load Average: {} {} {}'.format(*load_average) + load_average = 'Load Average: {} {} {}'.format( + *(f'{value:5.2f}'[:5] if value < 10000.0 else '9999+' for value in self.load_average), + ) if self.compact: width_right = len(load_average) + 4 diff --git a/nvitop/tui/screens/main/process.py b/nvitop/tui/screens/main/process.py index 95b24b5..5535b3b 100644 --- a/nvitop/tui/screens/main/process.py +++ b/nvitop/tui/screens/main/process.py @@ -15,8 +15,10 @@ from nvitop.tui.library import ( HOSTNAME, LARGE_INTEGER, SUPERUSER, - USERCONTEXT, + USER_CONTEXT, USERNAME, + WINDOWS, + WSL, Displayable, GpuProcess, MouseEvent, @@ -24,7 +26,6 @@ from nvitop.tui.library import ( WideString, colored, cut_string, - host, ttl_cache, wcslen, ) @@ -330,7 +331,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes ] if len(self.snapshots) == 0: if self.has_snapshots: - message = ' No running processes found{} '.format(' (in WSL)' if host.WSL else '') + message = ' No running processes found{} '.format(' (in WSL)' if WSL else '') else: message = ' Gathering process status...' header.extend( @@ -390,13 +391,13 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes for y, line in enumerate(self.header_lines(), start=self.y + 1): self.addstr(y, self.x, line) - context_width = wcslen(USERCONTEXT) - if not host.WINDOWS or len(USERCONTEXT) == context_width: + context_width = wcslen(USER_CONTEXT) + if not WINDOWS or len(USER_CONTEXT) == context_width: # Do not support windows-curses with wide characters username_width = wcslen(USERNAME) hostname_width = wcslen(HOSTNAME) offset = self.x + self.width - context_width - 2 - self.addstr(self.y + 2, self.x + offset, USERCONTEXT) + self.addstr(self.y + 2, self.x + offset, USER_CONTEXT) self.color_at(self.y + 2, self.x + offset, width=context_width, attr='bold') self.color_at( self.y + 2, @@ -561,7 +562,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes self.selection.clear() elif self.has_snapshots: - message = ' No running processes found{} '.format(' (in WSL)' if host.WSL else '') + message = ' No running processes found{} '.format(' (in WSL)' if WSL else '') self.addstr(self.y + 5, self.x, f'│ {message.ljust(self.width - 4)} │') text_offset = self.x + self.width - 47 @@ -599,7 +600,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes lines = ['', *self.header_lines()] lines[2] = ''.join( ( - lines[2][: -2 - wcslen(USERCONTEXT)], + lines[2][: -2 - wcslen(USER_CONTEXT)], colored(USERNAME, color=('yellow' if SUPERUSER else 'magenta'), attrs=('bold',)), colored('@', attrs=('bold',)), colored(HOSTNAME, color='green', attrs=('bold',)), diff --git a/nvitop/tui/screens/metrics.py b/nvitop/tui/screens/metrics.py index 05f12fc..f2de09b 100644 --- a/nvitop/tui/screens/metrics.py +++ b/nvitop/tui/screens/metrics.py @@ -13,7 +13,7 @@ from nvitop.tui.library import ( HOSTNAME, NA, SUPERUSER, - USERCONTEXT, + USER_CONTEXT, USERNAME, BufferedHistoryGraph, Displayable, @@ -350,13 +350,13 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at for y, line in enumerate(self.frame_lines(), start=self.y): self.addstr(y, self.x, line) - context_width = wcslen(USERCONTEXT) - if not host.WINDOWS or len(USERCONTEXT) == context_width: + context_width = wcslen(USER_CONTEXT) + if not host.WINDOWS or len(USER_CONTEXT) == context_width: # Do not support windows-curses with wide characters username_width = wcslen(USERNAME) hostname_width = wcslen(HOSTNAME) offset = self.x + self.width - context_width - 2 - self.addstr(self.y + 1, self.x + offset, USERCONTEXT) + self.addstr(self.y + 1, self.x + offset, USER_CONTEXT) self.color_at(self.y + 1, self.x + offset, width=context_width, attr='bold') self.color_at( self.y + 1, diff --git a/pyproject.toml b/pyproject.toml index 917c001..511bbe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,6 +197,12 @@ ignore = [ "ANN", # flake8-annotations "RUF012", # mutable-class-default ] +"!nvitop/tui/**/*.py" = [ + "TID251", # banned-api +] +"nvitop/tui/library/*.py" = [ + "TID251", # banned-api +] "docs/source/conf.py" = [ "D", # pydocstyle "INP001", # flake8-no-pep420 @@ -220,3 +226,6 @@ inline-quotes = "single" [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"nvitop.api".msg = "Use `nvitop.tui.library` instead of `nvitop.api` in `nvitop.tui`."