fix(tui/host): ignore errors when collecting host metrics (#163)

This commit is contained in:
Xuehai Pan 2025-04-20 05:05:42 +08:00 committed by GitHub
parent 57b48e6a3a
commit dfb4e3bf55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 174 additions and 71 deletions

View file

@ -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

View file

@ -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

101
nvitop/tui/library/host.py Normal file
View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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',)),

View file

@ -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,

View file

@ -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`."