feat: ruff integration

This commit is contained in:
Xuehai Pan 2023-03-15 09:18:14 +00:00
parent c5ce570c72
commit 80b9d6f75c
32 changed files with 524 additions and 312 deletions

View file

@ -20,6 +20,9 @@ per-file-ignores =
# F401: module imported but unused # F401: module imported but unused
# intentionally unused imports # intentionally unused imports
__init__.py: F401 __init__.py: F401
# SIM113: use enumarate
# false positive
nvitop/gui/screens/main/process.py: SIM113
exclude = exclude =
.git, .git,
.vscode, .vscode,

View file

@ -23,6 +23,12 @@ repos:
- id: detect-private-key - id: detect-private-key
- id: debug-statements - id: debug-statements
- id: double-quote-string-fixer - id: double-quote-string-fixer
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.256
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
stages: [commit, push, manual]
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.12.0 rev: 5.12.0
hooks: hooks:

View file

@ -173,7 +173,7 @@ def take_snapshots(
else: else:
leaf_devices = devices = list(devices) leaf_devices = devices = list(devices)
gpu_processes = list( gpu_processes = list(
itertools.chain.from_iterable(device.processes().values() for device in leaf_devices) itertools.chain.from_iterable(device.processes().values() for device in leaf_devices),
) )
devices = [device.as_snapshot() for device in devices] devices = [device.as_snapshot() for device in devices]
@ -430,7 +430,9 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
self._tags = set() self._tags = set()
self._daemon = threading.Thread( self._daemon = threading.Thread(
name='gpu_metric_collector_daemon', target=self._target, daemon=True name='gpu_metric_collector_daemon',
target=self._target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
@ -489,7 +491,7 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
tag = self._metric_buffer.tag tag = self._metric_buffer.tag
elif tag not in self._tags: elif tag not in self._tags:
raise RuntimeError( raise RuntimeError(
f'Resource metric collector has not been started with tag "{tag}".' f'Resource metric collector has not been started with tag "{tag}".',
) )
buffer = self._metric_buffer buffer = self._metric_buffer
@ -568,7 +570,7 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
tag = self._metric_buffer.tag tag = self._metric_buffer.tag
elif tag not in self._tags: elif tag not in self._tags:
raise RuntimeError( raise RuntimeError(
f'Resource metric collector has not been started with tag "{tag}".' f'Resource metric collector has not been started with tag "{tag}".',
) )
buffer = self._metric_buffer buffer = self._metric_buffer
@ -707,7 +709,7 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
'host/memory_percent (%)': host.memory_percent(), 'host/memory_percent (%)': host.memory_percent(),
'host/swap_percent (%)': host.swap_percent(), 'host/swap_percent (%)': host.swap_percent(),
'host/memory_used (GiB)': host.virtual_memory().used / GiB, 'host/memory_used (GiB)': host.virtual_memory().used / GiB,
} },
) )
load_average = host.load_average() load_average = host.load_average()
if load_average is not None: if load_average is not None:
@ -716,7 +718,7 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
'host/load_average (%) (1 min)': load_average[0], 'host/load_average (%) (1 min)': load_average[0],
'host/load_average (%) (5 min)': load_average[1], 'host/load_average (%) (5 min)': load_average[1],
'host/load_average (%) (15 min)': load_average[2], 'host/load_average (%) (15 min)': load_average[2],
} },
) )
device_identifiers = {} device_identifiers = {}
@ -755,7 +757,10 @@ class ResourceMetricCollector: # pylint: disable=too-many-instance-attributes
class _MetricBuffer: # pylint: disable=missing-class-docstring,missing-function-docstring,too-many-instance-attributes class _MetricBuffer: # pylint: disable=missing-class-docstring,missing-function-docstring,too-many-instance-attributes
def __init__( def __init__(
self, tag: str, collector: ResourceMetricCollector, prev: _MetricBuffer | None = None self,
tag: str,
collector: ResourceMetricCollector,
prev: _MetricBuffer | None = None,
) -> None: ) -> None:
self.collector = collector self.collector = collector
self.prev = prev self.prev = prev
@ -800,9 +805,7 @@ class _MetricBuffer: # pylint: disable=missing-class-docstring,missing-function
if key.endswith('host/running_time (min)/max'): if key.endswith('host/running_time (min)/max'):
metrics[key[:-4]] = metrics[key] metrics[key[:-4]] = metrics[key]
del metrics[key] del metrics[key]
elif key.endswith('host/running_time (min)/mean') or key.endswith( elif key.endswith(('host/running_time (min)/mean', 'host/running_time (min)/min')):
'host/running_time (min)/min'
):
del metrics[key] del metrics[key]
metrics[f'{self.key_prefix}/duration (s)'] = timer() - self.start_timestamp metrics[f'{self.key_prefix}/duration (s)'] = timer() - self.start_timestamp
metrics[f'{self.key_prefix}/timestamp'] = time.time() metrics[f'{self.key_prefix}/timestamp'] = time.time()

View file

@ -349,7 +349,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
@classmethod @classmethod
def from_indices( def from_indices(
cls, indices: int | Iterable[int | tuple[int, int]] | None = None cls,
indices: int | Iterable[int | tuple[int, int]] | None = None,
) -> list[PhysicalDevice | MigDevice]: ) -> list[PhysicalDevice | MigDevice]:
"""Return a list of devices of the given indices. """Return a list of devices of the given indices.
@ -412,7 +413,9 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
return cuda_devices return cuda_devices
@staticmethod @staticmethod
def from_cuda_indices(cuda_indices: int | Iterable[int] | None = None) -> list[CudaDevice]: def from_cuda_indices(
cuda_indices: int | Iterable[int] | None = None,
) -> list[CudaDevice]:
"""Return a list of CUDA devices of the given CUDA indices. """Return a list of CUDA devices of the given CUDA indices.
The CUDA ordinal will be enumerate from the ``CUDA_VISIBLE_DEVICES`` environment variable. The CUDA ordinal will be enumerate from the ``CUDA_VISIBLE_DEVICES`` environment variable.
@ -541,7 +544,7 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
if (index, uuid, bus_id).count(None) != 2: if (index, uuid, bus_id).count(None) != 2:
raise TypeError( raise TypeError(
f'Device(index=None, uuid=None, bus_id=None) takes 1 non-None arguments ' f'Device(index=None, uuid=None, bus_id=None) takes 1 non-None arguments '
f'but (index, uuid, bus_id) = {(index, uuid, bus_id)!r} were given' f'but (index, uuid, bus_id) = {(index, uuid, bus_id)!r} were given',
) )
if cls is not Device: if cls is not Device:
@ -560,14 +563,14 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
if not isinstance(index, tuple): if not isinstance(index, tuple):
raise TypeError( raise TypeError(
f'index must be an integer, or a tuple of two integers, or a valid UUID string, ' f'index must be an integer, or a tuple of two integers, or a valid UUID string, '
f'but index = {index!r} was given' f'but index = {index!r} was given',
) )
if not ( if not (
len(index) == 2 and isinstance(index[0], int) and isinstance(index[1], int) len(index) == 2 and isinstance(index[0], int) and isinstance(index[1], int)
): ):
raise TypeError( raise TypeError(
f'index for MIG device must be a tuple of two integers ' f'index for MIG device must be a tuple of two integers '
f'but index = {index!r} was given' f'but index = {index!r} was given',
) )
return super().__new__(MigDevice) return super().__new__(MigDevice)
elif uuid is not None and match is not None and match.group('MigMode') is not None: elif uuid is not None and match is not None and match.group('MigMode') is not None:
@ -616,7 +619,9 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
self._nvml_index = index self._nvml_index = index
try: try:
self._handle = libnvml.nvmlQuery( self._handle = libnvml.nvmlQuery(
'nvmlDeviceGetHandleByIndex', index, ignore_errors=False 'nvmlDeviceGetHandleByIndex',
index,
ignore_errors=False,
) )
except libnvml.NVMLError_GpuIsLost: except libnvml.NVMLError_GpuIsLost:
self._handle = None self._handle = None
@ -628,11 +633,15 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
try: try:
if uuid is not None: if uuid is not None:
self._handle = libnvml.nvmlQuery( self._handle = libnvml.nvmlQuery(
'nvmlDeviceGetHandleByUUID', uuid, ignore_errors=False 'nvmlDeviceGetHandleByUUID',
uuid,
ignore_errors=False,
) )
else: else:
self._handle = libnvml.nvmlQuery( self._handle = libnvml.nvmlQuery(
'nvmlDeviceGetHandleByPciBusId', bus_id, ignore_errors=False 'nvmlDeviceGetHandleByPciBusId',
bus_id,
ignore_errors=False,
) )
except libnvml.NVMLError_GpuIsLost: except libnvml.NVMLError_GpuIsLost:
self._handle = None self._handle = None
@ -655,7 +664,10 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
def __str__(self) -> str: def __str__(self) -> str:
"""Return a string representation of the device.""" """Return a string representation of the device."""
return '{}(index={}, name="{}", total_memory={})'.format( return '{}(index={}, name="{}", total_memory={})'.format(
self.__class__.__name__, self.index, self.name(), self.memory_total_human() self.__class__.__name__,
self.index,
self.name(),
self.memory_total_human(),
) )
__repr__ = __str__ __repr__ = __str__
@ -721,10 +733,14 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
func = getattr(libnvml, 'nvmlDeviceGet' + pascal_case + suffix) func = getattr(libnvml, 'nvmlDeviceGet' + pascal_case + suffix)
@ttl_cache(ttl=1.0) @ttl_cache(ttl=1.0)
def attribute(*args, **kwargs): def attribute(*args: Any, **kwargs: Any) -> Any:
try: try:
return libnvml.nvmlQuery( return libnvml.nvmlQuery(
func, self._handle, *args, **kwargs, ignore_errors=False func,
self._handle,
*args,
**kwargs,
ignore_errors=False,
) )
except libnvml.NVMLError_NotSupported: except libnvml.NVMLError_NotSupported:
return NA return NA
@ -788,7 +804,7 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
self._cuda_index = visible_device_indices.index(self.index) self._cuda_index = visible_device_indices.index(self.index)
except ValueError as ex: except ValueError as ex:
raise RuntimeError( raise RuntimeError(
f'CUDA Error: Device(index={self.index}) is not visible to CUDA applications' f'CUDA Error: Device(index={self.index}) is not visible to CUDA applications',
) from ex ) from ex
return self._cuda_index return self._cuda_index
@ -841,7 +857,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
""" """
if self._bus_id is NA: if self._bus_id is NA:
self._bus_id = libnvml.nvmlQuery( self._bus_id = libnvml.nvmlQuery(
lambda handle: libnvml.nvmlDeviceGetPciInfo(handle).busId, self.handle lambda handle: libnvml.nvmlDeviceGetPciInfo(handle).busId,
self.handle,
) )
return self._bus_id return self._bus_id
@ -952,7 +969,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
""" """
memory_info = self.memory_info() memory_info = self.memory_info()
if libnvml.nvmlCheckReturn(memory_info.used, int) and libnvml.nvmlCheckReturn( if libnvml.nvmlCheckReturn(memory_info.used, int) and libnvml.nvmlCheckReturn(
memory_info.total, int memory_info.total,
int,
): ):
return round(100.0 * memory_info.used / memory_info.total, 1) return round(100.0 * memory_info.used / memory_info.total, 1)
return NA return NA
@ -976,7 +994,9 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
memory_info = libnvml.nvmlQuery('nvmlDeviceGetBAR1MemoryInfo', self.handle) memory_info = libnvml.nvmlQuery('nvmlDeviceGetBAR1MemoryInfo', self.handle)
if libnvml.nvmlCheckReturn(memory_info): if libnvml.nvmlCheckReturn(memory_info):
return MemoryInfo( return MemoryInfo(
total=memory_info.bar1Total, free=memory_info.bar1Free, used=memory_info.bar1Used total=memory_info.bar1Total,
free=memory_info.bar1Free,
used=memory_info.bar1Used,
) )
return MemoryInfo(total=NA, free=NA, used=NA) return MemoryInfo(total=NA, free=NA, used=NA)
@ -1036,7 +1056,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
""" # pylint: disable=line-too-long """ # pylint: disable=line-too-long
memory_info = self.bar1_memory_info() memory_info = self.bar1_memory_info()
if libnvml.nvmlCheckReturn(memory_info.used, int) and libnvml.nvmlCheckReturn( if libnvml.nvmlCheckReturn(memory_info.used, int) and libnvml.nvmlCheckReturn(
memory_info.total, int memory_info.total,
int,
): ):
return round(100.0 * memory_info.used / memory_info.total, 1) return round(100.0 * memory_info.used / memory_info.total, 1)
return NA return NA
@ -1133,12 +1154,16 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
""" # pylint: disable=line-too-long """ # pylint: disable=line-too-long
return ClockInfos( return ClockInfos(
graphics=libnvml.nvmlQuery( graphics=libnvml.nvmlQuery(
'nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_GRAPHICS 'nvmlDeviceGetClockInfo',
self.handle,
libnvml.NVML_CLOCK_GRAPHICS,
), ),
sm=libnvml.nvmlQuery('nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_SM), sm=libnvml.nvmlQuery('nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_SM),
memory=libnvml.nvmlQuery('nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_MEM), memory=libnvml.nvmlQuery('nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_MEM),
video=libnvml.nvmlQuery( video=libnvml.nvmlQuery(
'nvmlDeviceGetClockInfo', self.handle, libnvml.NVML_CLOCK_VIDEO 'nvmlDeviceGetClockInfo',
self.handle,
libnvml.NVML_CLOCK_VIDEO,
), ),
) )
@ -1156,7 +1181,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
for name, clock in clock_infos.items(): for name, clock in clock_infos.items():
if clock is NA: if clock is NA:
clock_type = getattr( clock_type = getattr(
libnvml, 'NVML_CLOCK_{}'.format(name.replace('memory', 'mem').upper()) libnvml,
'NVML_CLOCK_{}'.format(name.replace('memory', 'mem').upper()),
) )
clock = libnvml.nvmlQuery('nvmlDeviceGetMaxClockInfo', self.handle, clock_type) clock = libnvml.nvmlQuery('nvmlDeviceGetMaxClockInfo', self.handle, clock_type)
clock_infos[name] = clock clock_infos[name] = clock
@ -1319,7 +1345,9 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=temperature.gpu nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=temperature.gpu
""" """
return libnvml.nvmlQuery( return libnvml.nvmlQuery(
'nvmlDeviceGetTemperature', self.handle, libnvml.NVML_TEMPERATURE_GPU 'nvmlDeviceGetTemperature',
self.handle,
libnvml.NVML_TEMPERATURE_GPU,
) )
@memoize_when_activated @memoize_when_activated
@ -1391,7 +1419,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=display_active nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=display_active
""" # pylint: disable=line-too-long """ # pylint: disable=line-too-long
return {0: 'Disabled', 1: 'Enabled'}.get( return {0: 'Disabled', 1: 'Enabled'}.get(
libnvml.nvmlQuery('nvmlDeviceGetDisplayActive', self.handle), NA libnvml.nvmlQuery('nvmlDeviceGetDisplayActive', self.handle),
NA,
) )
@ttl_cache(ttl=60.0) @ttl_cache(ttl=60.0)
@ -1412,7 +1441,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=display_mode nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=display_mode
""" # pylint: disable=line-too-long """ # pylint: disable=line-too-long
return {0: 'Disabled', 1: 'Enabled'}.get( return {0: 'Disabled', 1: 'Enabled'}.get(
libnvml.nvmlQuery('nvmlDeviceGetDisplayMode', self.handle), NA libnvml.nvmlQuery('nvmlDeviceGetDisplayMode', self.handle),
NA,
) )
@ttl_cache(ttl=60.0) @ttl_cache(ttl=60.0)
@ -1437,7 +1467,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=driver_model.current nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=driver_model.current
""" """
return {libnvml.NVML_DRIVER_WDDM: 'WDDM', libnvml.NVML_DRIVER_WDM: 'WDM'}.get( return {libnvml.NVML_DRIVER_WDDM: 'WDDM', libnvml.NVML_DRIVER_WDM: 'WDM'}.get(
libnvml.nvmlQuery('nvmlDeviceGetCurrentDriverModel', self.handle), NA libnvml.nvmlQuery('nvmlDeviceGetCurrentDriverModel', self.handle),
NA,
) )
driver_model = current_driver_model driver_model = current_driver_model
@ -1462,7 +1493,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=persistence_mode nvidia-smi --id=<IDENTIFIER> --format=csv,noheader,nounits --query-gpu=persistence_mode
""" # pylint: disable=line-too-long """ # pylint: disable=line-too-long
return {0: 'Disabled', 1: 'Enabled'}.get( return {0: 'Disabled', 1: 'Enabled'}.get(
libnvml.nvmlQuery('nvmlDeviceGetPersistenceMode', self.handle), NA libnvml.nvmlQuery('nvmlDeviceGetPersistenceMode', self.handle),
NA,
) )
@ttl_cache(ttl=5.0) @ttl_cache(ttl=5.0)
@ -1541,7 +1573,8 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
""" """
if self._cuda_compute_capability is None: if self._cuda_compute_capability is None:
self._cuda_compute_capability = libnvml.nvmlQuery( self._cuda_compute_capability = libnvml.nvmlQuery(
'nvmlDeviceGetCudaComputeCapability', self.handle 'nvmlDeviceGetCudaComputeCapability',
self.handle,
) )
return self._cuda_compute_capability return self._cuda_compute_capability
@ -1575,9 +1608,12 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
if self.is_mig_device(): if self.is_mig_device():
return NA return NA
mig_mode = libnvml.nvmlQuery( mig_mode, *_ = libnvml.nvmlQuery(
'nvmlDeviceGetMigMode', self.handle, default=(NA, NA), ignore_function_not_found=True 'nvmlDeviceGetMigMode',
)[0] self.handle,
default=(NA, NA),
ignore_function_not_found=True,
)
return {0: 'Disabled', 1: 'Enabled'}.get(mig_mode, NA) return {0: 'Disabled', 1: 'Enabled'}.get(mig_mode, NA)
def is_mig_mode_enabled(self) -> bool: def is_mig_mode_enabled(self) -> bool:
@ -1652,9 +1688,12 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
if len(processes) > 0: if len(processes) > 0:
samples = libnvml.nvmlQuery( samples = libnvml.nvmlQuery(
'nvmlDeviceGetProcessUtilization', self.handle, self._timestamp, default=() 'nvmlDeviceGetProcessUtilization',
self.handle,
self._timestamp,
default=(),
) )
self._timestamp = max(min((s.timeStamp for s in samples), default=0) - 2000000, 0) self._timestamp = max(min((s.timeStamp for s in samples), default=0) - 2_000_000, 0)
for s in samples: for s in samples:
try: try:
processes[s.pid].set_gpu_utilization(s.smUtil, s.memUtil, s.encUtil, s.decUtil) processes[s.pid].set_gpu_utilization(s.smUtil, s.memUtil, s.encUtil, s.decUtil)
@ -1720,7 +1759,7 @@ class Device: # pylint: disable=too-many-instance-attributes,too-many-public-me
# Modified from psutil (https://github.com/giampaolo/psutil) # Modified from psutil (https://github.com/giampaolo/psutil)
@contextlib.contextmanager @contextlib.contextmanager
def oneshot(self): def oneshot(self) -> contextlib.AbstractContextManager:
"""A utility context manager which considerably speeds up the retrieval of multiple device information at the same time. """A utility context manager which considerably speeds up the retrieval of multiple device information at the same time.
Internally different device info (e.g. memory_info, utilization_rates, ...) may be fetched Internally different device info (e.g. memory_info, utilization_rates, ...) may be fetched
@ -1804,7 +1843,10 @@ class PhysicalDevice(Device):
This method will return 0 if the device does not support MIG mode. This method will return 0 if the device does not support MIG mode.
""" """
return libnvml.nvmlQuery( return libnvml.nvmlQuery(
'nvmlDeviceGetMaxMigDeviceCount', self.handle, default=0, ignore_function_not_found=True 'nvmlDeviceGetMaxMigDeviceCount',
self.handle,
default=0,
ignore_function_not_found=True,
) )
@ttl_cache(ttl=60.0) @ttl_cache(ttl=60.0)
@ -1859,7 +1901,8 @@ class MigDevice(Device): # pylint: disable=too-many-instance-attributes
@classmethod @classmethod
def from_indices( # pylint: disable=signature-differs def from_indices( # pylint: disable=signature-differs
cls, indices: Iterable[tuple[int, int]] cls,
indices: Iterable[tuple[int, int]],
) -> list[MigDevice]: ) -> list[MigDevice]:
"""Return a list of MIG devices of the given indices. """Return a list of MIG devices of the given indices.
@ -1885,7 +1928,10 @@ class MigDevice(Device): # pylint: disable=too-many-instance-attributes
# pylint: disable-next=super-init-not-called # pylint: disable-next=super-init-not-called
def __init__( def __init__(
self, index: tuple[int, int] | str | None = None, *, uuid: str | None = None self,
index: tuple[int, int] | str | None = None,
*,
uuid: str | None = None,
) -> None: ) -> None:
"""Initialize the instance created by :meth:`__new__()`. """Initialize the instance created by :meth:`__new__()`.
@ -1941,10 +1987,14 @@ class MigDevice(Device): # pylint: disable=too-many-instance-attributes
else: else:
self._handle = libnvml.nvmlQuery('nvmlDeviceGetHandleByUUID', uuid, ignore_errors=False) self._handle = libnvml.nvmlQuery('nvmlDeviceGetHandleByUUID', uuid, ignore_errors=False)
parent_handle = libnvml.nvmlQuery( parent_handle = libnvml.nvmlQuery(
'nvmlDeviceGetDeviceHandleFromMigDeviceHandle', self.handle, ignore_errors=False 'nvmlDeviceGetDeviceHandleFromMigDeviceHandle',
self.handle,
ignore_errors=False,
) )
parent_index = libnvml.nvmlQuery( parent_index = libnvml.nvmlQuery(
'nvmlDeviceGetIndex', parent_handle, ignore_errors=False 'nvmlDeviceGetIndex',
parent_handle,
ignore_errors=False,
) )
self._parent = PhysicalDevice(index=parent_index) self._parent = PhysicalDevice(index=parent_index)
for mig_device in self.parent.mig_devices(): for mig_device in self.parent.mig_devices():
@ -1989,7 +2039,9 @@ class MigDevice(Device): # pylint: disable=too-many-instance-attributes
""" """
if self._gpu_instance_id is NA: if self._gpu_instance_id is NA:
self._gpu_instance_id = libnvml.nvmlQuery( self._gpu_instance_id = libnvml.nvmlQuery(
'nvmlDeviceGetGpuInstanceId', self.handle, default=0xFFFFFFFF 'nvmlDeviceGetGpuInstanceId',
self.handle,
default=0xFFFFFFFF,
) )
if self._gpu_instance_id == 0xFFFFFFFF: if self._gpu_instance_id == 0xFFFFFFFF:
self._gpu_instance_id = NA self._gpu_instance_id = NA
@ -2003,7 +2055,9 @@ class MigDevice(Device): # pylint: disable=too-many-instance-attributes
""" """
if self._compute_instance_id is NA: if self._compute_instance_id is NA:
self._compute_instance_id = libnvml.nvmlQuery( self._compute_instance_id = libnvml.nvmlQuery(
'nvmlDeviceGetComputeInstanceId', self.handle, default=0xFFFFFFFF 'nvmlDeviceGetComputeInstanceId',
self.handle,
default=0xFFFFFFFF,
) )
if self._compute_instance_id == 0xFFFFFFFF: if self._compute_instance_id == 0xFFFFFFFF:
self._compute_instance_id = NA self._compute_instance_id = NA
@ -2114,7 +2168,10 @@ class CudaDevice(Device):
return cls.from_indices() return cls.from_indices()
@classmethod @classmethod
def from_indices(cls, indices: int | Iterable[int] | None = None) -> list[CudaDevice]: def from_indices(
cls,
indices: int | Iterable[int] | None = None,
) -> list[CudaDevice]:
"""Return a list of CUDA devices of the given CUDA indices. """Return a list of CUDA devices of the given CUDA indices.
The CUDA ordinal will be enumerate from the ``CUDA_VISIBLE_DEVICES`` environment variable. The CUDA ordinal will be enumerate from the ``CUDA_VISIBLE_DEVICES`` environment variable.
@ -2415,7 +2472,7 @@ def _get_all_physical_device_attrs() -> dict[str, _PhysicalDeviceAttrs]:
), ),
) )
for device in PhysicalDevice.all() for device in PhysicalDevice.all()
] ],
) )
return _PHYSICAL_DEVICE_ATTRS return _PHYSICAL_DEVICE_ATTRS
@ -2598,7 +2655,9 @@ def _parse_cuda_visible_devices_to_uuids(
def _cuda_visible_devices_parser( def _cuda_visible_devices_parser(
cuda_visible_devices: str, queue: mp.SimpleQueue, verbose: bool = True cuda_visible_devices: str,
queue: mp.SimpleQueue,
verbose: bool = True,
) -> None: ) -> None:
try: try:
if cuda_visible_devices is not None: if cuda_visible_devices is not None:
@ -2622,7 +2681,7 @@ def _cuda_visible_devices_parser(
uuids = list(map(libcuda.cuDeviceGetUuid, map(libcuda.cuDeviceGet, range(count)))) uuids = list(map(libcuda.cuDeviceGetUuid, map(libcuda.cuDeviceGet, range(count))))
queue.put(uuids) queue.put(uuids)
return return
except Exception as ex: # pylint: disable=broad-except except Exception as ex: # noqa: BLE001 # pylint: disable=broad-except
queue.put(ex) queue.put(ex)
if verbose: if verbose:
raise ex raise ex

View file

@ -20,7 +20,10 @@
utilization (CPU, memory, disks, network, sensors) in Python. utilization (CPU, memory, disks, network, sensors) in Python.
""" """
from __future__ import annotations
import os as _os import os as _os
from typing import Callable as _Callable
import psutil as _psutil import psutil as _psutil
from cachetools.func import ttl_cache as _ttl_cache from cachetools.func import ttl_cache as _ttl_cache
@ -49,30 +52,32 @@ swap_memory = _ttl_cache(ttl=0.25)(_psutil.swap_memory)
try: try:
load_average = _ttl_cache(ttl=2.0)(_psutil.getloadavg) load_average: _Callable[[], tuple[float, float, float]] = _ttl_cache(ttl=2.0)(
_psutil.getloadavg,
)
load_average.__doc__ = """Get the system load average.""" load_average.__doc__ = """Get the system load average."""
except AttributeError: except AttributeError:
def load_average(): def load_average() -> None:
"""Get the system load average.""" """Get the system load average."""
return None return
def memory_percent(): def memory_percent() -> float:
"""The percentage usage of virtual memory, calculated as ``(total - available) / total * 100``.""" """The percentage usage of virtual memory, calculated as ``(total - available) / total * 100``."""
return virtual_memory().percent return virtual_memory().percent
def swap_percent(): def swap_percent() -> float:
"""The percentage usage of virtual memory, calculated as ``used / total * 100``.""" """The percentage usage of virtual memory, calculated as ``used / total * 100``."""
return swap_memory().percent return swap_memory().percent
ppid_map = _psutil._ppid_map # pylint: disable=protected-access ppid_map: _Callable[[], dict[int, int]] = _psutil._ppid_map # pylint: disable=protected-access
"""Obtain a ``{pid: ppid, ...}`` dict for all running processes in one shot.""" """Obtain a ``{pid: ppid, ...}`` dict for all running processes in one shot."""
def reverse_ppid_map(): # pylint: disable=function-redefined def reverse_ppid_map() -> dict[int, list[int]]: # pylint: disable=function-redefined
"""Obtain a ``{ppid: [pid, ...], ...}`` dict for all running processes in one shot.""" """Obtain a ``{ppid: [pid, ...], ...}`` dict for all running processes in one shot."""
from collections import defaultdict # pylint: disable=import-outside-toplevel from collections import defaultdict # pylint: disable=import-outside-toplevel
@ -91,6 +96,3 @@ else:
WSL = None WSL = None
WINDOWS_SUBSYSTEM_FOR_LINUX = WSL WINDOWS_SUBSYSTEM_FOR_LINUX = WSL
"""The Linux distribution name of the Windows Subsystem for Linux.""" """The Linux distribution name of the Windows Subsystem for Linux."""
del _os, _ttl_cache

View file

@ -244,7 +244,7 @@ class CUDAError(Exception):
try: try:
if self.value not in CUDAError._errcode_to_string: if self.value not in CUDAError._errcode_to_string:
CUDAError._errcode_to_string[self.value] = '{}.'.format( CUDAError._errcode_to_string[self.value] = '{}.'.format(
cuGetErrorString(self.value).rstrip('.').capitalize() cuGetErrorString(self.value).rstrip('.').capitalize(),
) )
if self.value not in CUDAError._errcode_to_name: if self.value not in CUDAError._errcode_to_name:
CUDAError._errcode_to_name[self.value] = cuGetErrorName(self.value) CUDAError._errcode_to_name[self.value] = cuGetErrorName(self.value)
@ -299,8 +299,7 @@ def _extract_cuda_errors_as_classes() -> None:
def gen_new(value): def gen_new(value):
def new(cls): def new(cls):
obj = CUDAError.__new__(cls, value) return CUDAError.__new__(cls, value)
return obj
return new return new
@ -314,9 +313,7 @@ def _extract_cuda_errors_as_classes() -> None:
err_val, err_val,
) )
else: else:
new_error_class.__doc__ = 'CUDA Error with code :data:`{}` ({})'.format( new_error_class.__doc__ = f'CUDA Error with code :data:`{err_name}` ({err_val})'
err_name, err_val
)
setattr(this_module, class_name, new_error_class) setattr(this_module, class_name, new_error_class)
CUDAError._value_class_mapping[err_val] = new_error_class CUDAError._value_class_mapping[err_val] = new_error_class
CUDAError._errcode_to_name[err_val] = err_name CUDAError._errcode_to_name[err_val] = err_name
@ -395,7 +392,7 @@ def __LoadCudaLibrary() -> None:
] ]
# Also add libraries with version suffix `.1` # Also add libraries with version suffix `.1`
lib_filenames = list( lib_filenames = list(
_itertools.chain.from_iterable((f'{lib}.1', lib) for lib in lib_filenames) _itertools.chain.from_iterable((f'{lib}.1', lib) for lib in lib_filenames),
) )
elif system == 'Windows': elif system == 'Windows':
bits = _platform.architecture()[0].replace('bit', '') # e.g., '64' or '32' bits = _platform.architecture()[0].replace('bit', '') # e.g., '64' or '32'

View file

@ -295,7 +295,7 @@ class cudaError(Exception):
try: try:
if self.value not in cudaError._errcode_to_string: if self.value not in cudaError._errcode_to_string:
cudaError._errcode_to_string[self.value] = '{}.'.format( cudaError._errcode_to_string[self.value] = '{}.'.format(
cuGetErrorString(self.value).rstrip('.').capitalize() cuGetErrorString(self.value).rstrip('.').capitalize(),
) )
if self.value not in cudaError._errcode_to_name: if self.value not in cudaError._errcode_to_name:
cudaError._errcode_to_name[self.value] = cudaGetErrorName(self.value) cudaError._errcode_to_name[self.value] = cudaGetErrorName(self.value)
@ -353,8 +353,7 @@ def _extract_cuda_errors_as_classes() -> None:
def gen_new(value): def gen_new(value):
def new(cls): def new(cls):
obj = cudaError.__new__(cls, value) return cudaError.__new__(cls, value)
return obj
return new return new
@ -368,9 +367,7 @@ def _extract_cuda_errors_as_classes() -> None:
err_val, err_val,
) )
else: else:
new_error_class.__doc__ = 'CUDA Error with code :data:`{}` ({})'.format( new_error_class.__doc__ = f'CUDA Error with code :data:`{err_name}` ({err_val})'
err_name, err_val
)
setattr(this_module, class_name, new_error_class) setattr(this_module, class_name, new_error_class)
cudaError._value_class_mapping[err_val] = new_error_class cudaError._value_class_mapping[err_val] = new_error_class
cudaError._errcode_to_name[err_val] = err_name cudaError._errcode_to_name[err_val] = err_name
@ -464,7 +461,7 @@ def __LoadCudaLibrary() -> None: # pylint: disable=too-many-branches
[ [
_os.path.join(cuda_path, f'lib{bits}', lib_filename), _os.path.join(cuda_path, f'lib{bits}', lib_filename),
_os.path.join(cuda_path, 'lib', lib_filename), _os.path.join(cuda_path, 'lib', lib_filename),
] ],
) )
else: else:
candidate_dirs = _os.getenv('PATH', '').split(_os.path.pathsep) candidate_dirs = _os.getenv('PATH', '').split(_os.path.pathsep)
@ -476,18 +473,18 @@ def __LoadCudaLibrary() -> None: # pylint: disable=too-many-branches
_os.path.join(cuda_path, 'bin'), _os.path.join(cuda_path, 'bin'),
_os.path.join(cuda_path, f'lib{bits}'), _os.path.join(cuda_path, f'lib{bits}'),
_os.path.join(cuda_path, 'lib'), _os.path.join(cuda_path, 'lib'),
] ],
) )
for candidate_dir in candidate_dirs: for candidate_dir in candidate_dirs:
candidate_paths.extend( candidate_paths.extend(
_glob.iglob(_os.path.join(candidate_dir, f'cudart{bits}*.dll')) _glob.iglob(_os.path.join(candidate_dir, f'cudart{bits}*.dll')),
) )
# Normalize paths and remove duplicates # Normalize paths and remove duplicates
candidate_paths = list( candidate_paths = list(
dict.fromkeys( dict.fromkeys(
_os.path.normpath(_os.path.normcase(p)) for p in candidate_paths _os.path.normpath(_os.path.normcase(p)) for p in candidate_paths
) ),
) )
for lib_filename in candidate_paths: for lib_filename in candidate_paths:
try: try:

View file

@ -56,7 +56,7 @@ __all__ = [ # will be updated in below
if not callable(getattr(_pynvml, 'nvmlInitWithFlags', None)): if not callable(getattr(_pynvml, 'nvmlInitWithFlags', None)):
raise ImportError( raise ImportError(
'Your installed package `nvidia-ml-py` is corrupted. Please reinstall package ' 'Your installed package `nvidia-ml-py` is corrupted. Please reinstall package '
'`nvidia-ml-py` via `pip3 install --force-reinstall nvidia-ml-py nvitop`.' '`nvidia-ml-py` via `pip3 install --force-reinstall nvidia-ml-py nvitop`.',
) )
@ -79,7 +79,7 @@ _errcode_to_string = NVMLError._errcode_to_string # pylint: disable=protected-a
for _name, _attr in _vars_pynvml.items(): for _name, _attr in _vars_pynvml.items():
if _name in ('nvmlInit', 'nvmlInitWithFlags', 'nvmlShutdown'): if _name in ('nvmlInit', 'nvmlInitWithFlags', 'nvmlShutdown'):
continue continue
if _name.startswith('NVML_ERROR_') or _name.startswith('NVMLError_'): if _name.startswith(('NVML_ERROR_', 'NVMLError_')):
__all__.append(_name) __all__.append(_name)
if _name.startswith('NVML_ERROR_'): if _name.startswith('NVML_ERROR_'):
_errcode_to_name[_attr] = _name _errcode_to_name[_attr] = _name
@ -101,7 +101,9 @@ _errcode = _reason = _subclass = None
for _errcode, _reason in _errcode_to_string.items(): for _errcode, _reason in _errcode_to_string.items():
_subclass = nvmlExceptionClass(_errcode) _subclass = nvmlExceptionClass(_errcode)
_subclass.__doc__ = '{}. Code: :data:`{}` ({})'.format( _subclass.__doc__ = '{}. Code: :data:`{}` ({})'.format(
_reason.rstrip('.'), _errcode_to_name[_errcode], _errcode _reason.rstrip('.'),
_errcode_to_name[_errcode],
_errcode,
) )
# 4. Add undocumented constants into module docstring # 4. Add undocumented constants into module docstring
@ -184,7 +186,7 @@ except (ValueError, TypeError):
pass pass
if not LOGGER.hasHandlers() and LOGGER.isEnabledFor(_logging.DEBUG): if not LOGGER.hasHandlers() and LOGGER.isEnabledFor(_logging.DEBUG):
_formatter = _logging.Formatter( _formatter = _logging.Formatter(
'[%(levelname)s] %(asctime)s %(name)s::%(funcName)s: %(message)s' '[%(levelname)s] %(asctime)s %(name)s::%(funcName)s: %(message)s',
) )
_stream_handler = _logging.StreamHandler() _stream_handler = _logging.StreamHandler()
_stream_handler.setFormatter(_formatter) _stream_handler.setFormatter(_formatter)
@ -332,11 +334,11 @@ def nvmlShutdown() -> None: # pylint: disable=function-redefined
def nvmlQuery( def nvmlQuery(
func: _Callable[..., _Any] | str, func: _Callable[..., _Any] | str,
*args, *args: _Any,
default: _Any = NA, default: _Any = NA,
ignore_errors: bool = True, ignore_errors: bool = True,
ignore_function_not_found: bool = False, ignore_function_not_found: bool = False,
**kwargs, **kwargs: _Any,
) -> _Any: ) -> _Any:
"""Call a function with the given arguments from NVML. """Call a function with the given arguments from NVML.
@ -416,7 +418,10 @@ def nvmlQuery(
return retval return retval
def nvmlCheckReturn(retval: _Any, types: type | tuple[type, ...] | None = None) -> bool: def nvmlCheckReturn(
retval: _Any,
types: type | tuple[type, ...] | None = None,
) -> bool:
"""Check whether the return value is not :const:`nvitop.NA` and is one of the given types.""" """Check whether the return value is not :const:`nvitop.NA` and is one of the given types."""
if types is None: if types is None:
return retval != NA return retval != NA
@ -433,7 +438,7 @@ def __patch_backward_compatibility_layers() -> None:
if __patched_backward_compatibility_layers: if __patched_backward_compatibility_layers:
return return
function_name_mapping_lock = _threading.Lock() # noqa: F405 function_name_mapping_lock = _threading.Lock()
function_name_mapping = {} function_name_mapping = {}
def function_mapping_update(mapping): def function_mapping_update(mapping):
@ -456,8 +461,8 @@ def __patch_backward_compatibility_layers() -> None:
_pynvml.__dict__.update( # need to use module.__dict__.__setitem__ because module.__setattr__ will not work _pynvml.__dict__.update( # need to use module.__dict__.__setitem__ because module.__setattr__ will not work
_nvmlGetFunctionPointer=wrapper( _nvmlGetFunctionPointer=wrapper(
_pynvml._nvmlGetFunctionPointer # pylint: disable=protected-access,no-member _pynvml._nvmlGetFunctionPointer, # pylint: disable=protected-access,no-member
) ),
) )
def patch_function_pointers_when_fail(names, callback): def patch_function_pointers_when_fail(names, callback):
@ -514,8 +519,12 @@ def __patch_backward_compatibility_layers() -> None:
} }
def patch_process_info_callback( def patch_process_info_callback(
name, names, exception, pynvml, modself name,
): # pylint: disable=unused-argument names, # pylint: disable=unused-argument
exception,
pynvml,
modself,
):
if name in nvmlDeviceGetRunningProcesses_v3_v2: if name in nvmlDeviceGetRunningProcesses_v3_v2:
mapping = nvmlDeviceGetRunningProcesses_v3_v2 mapping = nvmlDeviceGetRunningProcesses_v3_v2
struct_type = c_nvmlProcessInfo_v2_t struct_type = c_nvmlProcessInfo_v2_t
@ -533,7 +542,8 @@ def __patch_backward_compatibility_layers() -> None:
for old_name, mapped_name in mapping.items(): for old_name, mapped_name in mapping.items():
LOGGER.debug(' Map NVML function `%s` to `%s`', old_name, mapped_name) LOGGER.debug(' Map NVML function `%s` to `%s`', old_name, mapped_name)
LOGGER.debug( LOGGER.debug(
' Patch NVML struct `c_nvmlProcessInfo_t` to `%s`', struct_type.__name__ ' Patch NVML struct `c_nvmlProcessInfo_t` to `%s`',
struct_type.__name__,
) )
return mapping[name] return mapping[name]
@ -547,9 +557,9 @@ def __patch_backward_compatibility_layers() -> None:
names=set(nvmlDeviceGetRunningProcesses_v2_v1), names=set(nvmlDeviceGetRunningProcesses_v2_v1),
callback=patch_process_info_callback, callback=patch_process_info_callback,
)( )(
_pynvml._nvmlGetFunctionPointer # pylint: disable=protected-access,no-member _pynvml._nvmlGetFunctionPointer, # pylint: disable=protected-access,no-member
) ),
) ),
) )
with_mapped_function_name() # patch first and only for once with_mapped_function_name() # patch first and only for once
@ -570,7 +580,8 @@ _pynvml_get_memory_info_v2_available = _pynvml_memory_v2_available
_driver_get_memory_info_v2_available = None if not _pynvml_installation_corrupted else False _driver_get_memory_info_v2_available = None if not _pynvml_installation_corrupted else False
def nvmlDeviceGetMemoryInfo(handle): # pylint: disable=function-redefined,too-many-branches # pylint: disable-next=function-redefined,too-many-branches
def nvmlDeviceGetMemoryInfo(handle: c_nvmlDevice_t) -> _pynvml.c_nvmlMemory_t:
"""Retrieve the amount of used, free, reserved and total memory available on the device, in bytes. """Retrieve the amount of used, free, reserved and total memory available on the device, in bytes.
Note: Note:
@ -620,7 +631,8 @@ def nvmlDeviceGetMemoryInfo(handle): # pylint: disable=function-redefined,too-m
with __lock: with __lock:
_pynvml_get_memory_info_v2_available = False _pynvml_get_memory_info_v2_available = False
LOGGER.debug( LOGGER.debug(
'NVML memory info version 2 is not available due to incompatible `nvidia-ml-py` package.' 'NVML memory info version 2 is not available '
'due to incompatible `nvidia-ml-py` package.',
) )
else: else:
# driver ✔ pynvml ? user ✘ # driver ✔ pynvml ? user ✘
@ -634,7 +646,8 @@ def nvmlDeviceGetMemoryInfo(handle): # pylint: disable=function-redefined,too-m
with __lock: with __lock:
_pynvml_get_memory_info_v2_available = False _pynvml_get_memory_info_v2_available = False
LOGGER.debug( LOGGER.debug(
'NVML memory info version 2 is not available due to incompatible NVIDIA driver.' 'NVML memory info version 2 is not available '
'due to incompatible NVIDIA driver.',
) )
else: else:
# driver ✔ pynvml ✔ # driver ✔ pynvml ✔
@ -647,19 +660,19 @@ def nvmlDeviceGetMemoryInfo(handle): # pylint: disable=function-redefined,too-m
'your NVIDIA driver does support the NVML memory info version 2 APIs. NVML ' 'your NVIDIA driver does support the NVML memory info version 2 APIs. NVML '
'memory info version 2 is not available due to the legacy dependencies. ' 'memory info version 2 is not available due to the legacy dependencies. '
'Please consider upgrading your `nvidia-ml-py` package by running ' 'Please consider upgrading your `nvidia-ml-py` package by running '
'`pip3 install --upgrade nvitop nvidia-ml-py`.' '`pip3 install --upgrade nvitop nvidia-ml-py`.',
) )
elif _pynvml_memory_v2_available: elif _pynvml_memory_v2_available:
# driver ✘ pynvml ? # driver ✘ pynvml ?
LOGGER.debug( LOGGER.debug(
'NVML memory info version 2 is not available due to incompatible NVIDIA driver.' 'NVML memory info version 2 is not available due to incompatible NVIDIA driver.',
) )
else: else:
# driver ✘ pynvml ✘ # driver ✘ pynvml ✘
LOGGER.debug( LOGGER.debug(
'NVML constant `nvmlMemory_v2` not found in package `nvidia-ml-py`, and ' 'NVML constant `nvmlMemory_v2` not found in package `nvidia-ml-py`, and '
'your NVIDIA driver does not support the NVML memory info version 2 APIs. ' 'your NVIDIA driver does not support the NVML memory info version 2 APIs. '
'NVML memory info version 2 is not available.' 'NVML memory info version 2 is not available.',
) )
elif _pynvml_get_memory_info_v2_available: elif _pynvml_get_memory_info_v2_available:
@ -697,12 +710,8 @@ class _CustomModule(_ModuleType):
_lazy_init() _lazy_init()
return self return self
def __exit__(self, *args, **kwargs) -> None: def __exit__(self, *args: _Any, **kwargs: _Any) -> None:
"""Shutdown the NVML context in the context manager for ``with`` statement.""" """Shutdown the NVML context in the context manager for ``with`` statement."""
self.__del__()
def __del__(self) -> None:
"""Automatically shutdown the NVML context on destruction."""
try: try:
nvmlShutdown() nvmlShutdown()
except NVMLError: except NVMLError:

View file

@ -62,7 +62,7 @@ if host.POSIX:
if "'" not in s and '\n' not in s: if "'" not in s and '\n' not in s:
return f"'{s}'" return f"'{s}'"
return '"{}"'.format( return '"{}"'.format(
s.replace('\\', r'\\').replace('"', r'\"').replace('$', r'\$').replace('\n', r'\n') s.replace('\\', r'\\').replace('"', r'\"').replace('$', r'\$').replace('\n', r'\n'),
) )
elif host.WINDOWS: elif host.WINDOWS:
@ -77,7 +77,7 @@ elif host.WINDOWS:
if '"' not in s: if '"' not in s:
return f'"{s}"' return f'"{s}"'
return '"{}"'.format( return '"{}"'.format(
s.replace('^', '^^').replace('"', '^"').replace('%', '^%').replace('\n', r'\n') s.replace('^', '^^').replace('"', '^"').replace('%', '^%').replace('\n', r'\n'),
) )
else: else:
@ -113,7 +113,7 @@ def auto_garbage_clean(
def wrapper(func: Callable[..., Any]) -> Callable[..., Any]: def wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
@functools.wraps(func) @functools.wraps(func)
def wrapped(self: GpuProcess, *args, **kwargs): def wrapped(self: GpuProcess, *args: Any, **kwargs: Any) -> Any:
try: try:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
except host.PsutilError as ex: except host.PsutilError as ex:
@ -127,9 +127,8 @@ def auto_garbage_clean(
del HostProcess.INSTANCES[self.pid] del HostProcess.INSTANCES[self.pid]
except KeyError: except KeyError:
pass pass
if fallback is _RAISE or not getattr( # See also `GpuProcess.failsafe`
_USE_FALLBACK_WHEN_RAISE, 'value', False # see also `GpuProcess.failsafe` if fallback is _RAISE or not getattr(_USE_FALLBACK_WHEN_RAISE, 'value', False):
):
raise ex raise ex
if isinstance(fallback, tuple): if isinstance(fallback, tuple):
if isinstance(ex, host.AccessDenied) and fallback == ('No Such Process',): if isinstance(ex, host.AccessDenied) and fallback == ('No Such Process',):
@ -381,7 +380,7 @@ class HostProcess(host.Process, metaclass=ABCMeta):
return [HostProcess(child.pid) for child in super().children(recursive)] return [HostProcess(child.pid) for child in super().children(recursive)]
@contextlib.contextmanager @contextlib.contextmanager
def oneshot(self): def oneshot(self) -> contextlib.AbstractContextManager:
"""A utility context manager which considerably speeds up the retrieval of multiple process information at the same time. """A utility context manager which considerably speeds up the retrieval of multiple process information at the same time.
Internally different process info (e.g. name, ppid, uids, gids, ...) may be fetched by using Internally different process info (e.g. name, ppid, uids, gids, ...) may be fetched by using
@ -416,7 +415,9 @@ class HostProcess(host.Process, metaclass=ABCMeta):
self.running_time.cache_deactivate(self) self.running_time.cache_deactivate(self)
def as_snapshot( def as_snapshot(
self, attrs: Iterable[str] | None = None, ad_value: Any | None = None self,
attrs: Iterable[str] | None = None,
ad_value: Any | None = None,
) -> Snapshot: ) -> Snapshot:
"""Return a onetime snapshot of the process.""" """Return a onetime snapshot of the process."""
with self.oneshot(): with self.oneshot():
@ -912,7 +913,7 @@ class GpuProcess: # pylint: disable=too-many-instance-attributes,too-many-publi
def host_snapshot(self) -> Snapshot: def host_snapshot(self) -> Snapshot:
"""Return a onetime snapshot of the host process.""" """Return a onetime snapshot of the host process."""
with self.host.oneshot(): with self.host.oneshot():
host_snapshot = Snapshot( return Snapshot(
real=self.host, real=self.host,
is_running=self.is_running(), is_running=self.is_running(),
status=self.status(), status=self.status(),
@ -929,11 +930,11 @@ class GpuProcess: # pylint: disable=too-many-instance-attributes,too-many-publi
running_time_in_seconds=self.running_time_in_seconds(), running_time_in_seconds=self.running_time_in_seconds(),
) )
return host_snapshot
@auto_garbage_clean(fallback=_RAISE) @auto_garbage_clean(fallback=_RAISE)
def as_snapshot( def as_snapshot(
self, *, host_process_snapshot_cache: dict[int, Snapshot] | None = None self,
*,
host_process_snapshot_cache: dict[int, Snapshot] | None = None,
) -> Snapshot: ) -> Snapshot:
"""Return a onetime snapshot of the process on the GPU device. """Return a onetime snapshot of the process on the GPU device.
@ -983,7 +984,10 @@ class GpuProcess: # pylint: disable=too-many-instance-attributes,too-many-publi
@classmethod @classmethod
def take_snapshots( # batched version of `as_snapshot` def take_snapshots( # batched version of `as_snapshot`
cls, gpu_processes: Iterable[GpuProcess], *, failsafe: bool = False cls,
gpu_processes: Iterable[GpuProcess],
*,
failsafe: bool = False,
) -> list[Snapshot]: ) -> list[Snapshot]:
"""Take snapshots for a list of :class:`GpuProcess` instances. """Take snapshots for a list of :class:`GpuProcess` instances.
@ -993,15 +997,13 @@ class GpuProcess: # pylint: disable=too-many-instance-attributes,too-many-publi
cache = {} cache = {}
context = cls.failsafe if failsafe else contextlib.nullcontext context = cls.failsafe if failsafe else contextlib.nullcontext
with context(): with context():
snapshots = [ return [
process.as_snapshot(host_process_snapshot_cache=cache) for process in gpu_processes process.as_snapshot(host_process_snapshot_cache=cache) for process in gpu_processes
] ]
return snapshots
@classmethod @classmethod
@contextlib.contextmanager @contextlib.contextmanager
def failsafe(cls): def failsafe(cls) -> contextlib.AbstractContextManager:
"""A context manager that enables fallback values for methods that fail. """A context manager that enables fallback values for methods that fail.
Examples: Examples:

View file

@ -491,7 +491,8 @@ SIZE_UNITS = {
} }
"""Units of storage and memory measurements.""" """Units of storage and memory measurements."""
SIZE_PATTERN = re.compile( SIZE_PATTERN = re.compile(
r'^\s*\+?\s*(?P<size>\d+(?:\.\d+)?)\s*(?P<unit>[KMGTP]i?B?|B?)\s*$', flags=re.IGNORECASE r'^\s*\+?\s*(?P<size>\d+(?:\.\d+)?)\s*(?P<unit>[KMGTP]i?B?|B?)\s*$',
flags=re.IGNORECASE,
) )
"""The regex pattern for human readable size.""" """The regex pattern for human readable size."""
@ -607,7 +608,7 @@ class Snapshot:
Missing attributes will be automatically fetched from the original object. Missing attributes will be automatically fetched from the original object.
""" """
def __init__(self, real: Any, **items) -> None: def __init__(self, real: Any, **items: Any) -> None:
"""Initialize a new :class:`Snapshot` object with the given attributes.""" """Initialize a new :class:`Snapshot` object with the given attributes."""
self.real = real self.real = real
self.timestamp = time.time() self.timestamp = time.time()
@ -626,7 +627,9 @@ class Snapshot:
keyval = keyval.replace('\n', '\n ') # extra indentation for nested snapshots keyval = keyval.replace('\n', '\n ') # extra indentation for nested snapshots
keyvals.append(keyval) keyvals.append(keyval)
return '{}{}(\n {},\n)'.format( return '{}{}(\n {},\n)'.format(
self.real.__class__.__name__, self.__class__.__name__, ',\n '.join(keyvals) self.real.__class__.__name__,
self.__class__.__name__,
',\n '.join(keyvals),
) )
__repr__ = __str__ __repr__ = __str__
@ -686,7 +689,7 @@ def memoize_when_activated(method: Callable[[Any], Any]) -> Callable[[Any], Any]
""" """
@functools.wraps(method) @functools.wraps(method)
def wrapped(self): def wrapped(self): # noqa: ANN001,ANN202
try: try:
# case 1: we previously entered oneshot() ctx # case 1: we previously entered oneshot() ctx
ret = self._cache[method] # pylint: disable=protected-access ret = self._cache[method] # pylint: disable=protected-access
@ -705,7 +708,7 @@ def memoize_when_activated(method: Callable[[Any], Any]) -> Callable[[Any], Any]
pass pass
return ret return ret
def cache_activate(self): def cache_activate(self): # noqa: ANN001,ANN202
"""Activate cache. """Activate cache.
Expects an instance. Cache will be stored as a "_cache" instance attribute. Expects an instance. Cache will be stored as a "_cache" instance attribute.
@ -713,7 +716,7 @@ def memoize_when_activated(method: Callable[[Any], Any]) -> Callable[[Any], Any]
if not hasattr(self, '_cache'): if not hasattr(self, '_cache'):
self._cache = {} # pylint: disable=protected-access self._cache = {} # pylint: disable=protected-access
def cache_deactivate(self): def cache_deactivate(self): # noqa: ANN001,ANN202
"""Deactivate and clear cache.""" """Deactivate and clear cache."""
try: try:
del self._cache # pylint: disable=protected-access del self._cache # pylint: disable=protected-access

View file

@ -109,7 +109,7 @@ class GpuStatsLogger(Callback): # pylint: disable=too-many-instance-attributes
libnvml.nvmlInit() libnvml.nvmlInit()
except libnvml.NVMLError as ex: except libnvml.NVMLError as ex:
raise ValueError( raise ValueError(
'Cannot use the GpuStatsLogger callback because the NVIDIA driver is not installed.' 'Cannot use the GpuStatsLogger callback because the NVIDIA driver is not installed.',
) from ex ) from ex
if isinstance(gpus, (list, tuple)): if isinstance(gpus, (list, tuple)):
@ -126,7 +126,7 @@ class GpuStatsLogger(Callback): # pylint: disable=too-many-instance-attributes
except (libnvml.NVMLError, RuntimeError) as ex: except (libnvml.NVMLError, RuntimeError) as ex:
raise ValueError( raise ValueError(
f'Cannot use GpuStatsLogger callback because devices unavailable. ' f'Cannot use GpuStatsLogger callback because devices unavailable. '
f'Received: `gpus={gpu_ids}`' f'Received: `gpus={gpu_ids}`',
) from ex ) from ex
self._memory_utilization = memory_utilization self._memory_utilization = memory_utilization

View file

@ -98,7 +98,7 @@ class GpuStatsLogger(Callback): # pylint: disable=too-many-instance-attributes
libnvml.nvmlInit() libnvml.nvmlInit()
except libnvml.NVMLError as ex: except libnvml.NVMLError as ex:
raise MisconfigurationException( raise MisconfigurationException(
'Cannot use GpuStatsLogger callback because NVIDIA driver is not installed.' 'Cannot use GpuStatsLogger callback because NVIDIA driver is not installed.',
) from ex ) from ex
self._memory_utilization = memory_utilization self._memory_utilization = memory_utilization
@ -111,13 +111,13 @@ class GpuStatsLogger(Callback): # pylint: disable=too-many-instance-attributes
def on_train_start(self, trainer, pl_module) -> None: def on_train_start(self, trainer, pl_module) -> None:
if not trainer.logger: if not trainer.logger:
raise MisconfigurationException( raise MisconfigurationException(
'Cannot use GpuStatsLogger callback with Trainer that has no logger.' 'Cannot use GpuStatsLogger callback with Trainer that has no logger.',
) )
if trainer.strategy.root_device.type != 'cuda': if trainer.strategy.root_device.type != 'cuda':
raise MisconfigurationException( raise MisconfigurationException(
f'You are using GpuStatsLogger but are not running on GPU. ' f'You are using GpuStatsLogger but are not running on GPU. '
f'The root device type is {trainer.strategy.root_device.type}.' f'The root device type is {trainer.strategy.root_device.type}.',
) )
device_ids = trainer.data_parallel_device_ids device_ids = trainer.data_parallel_device_ids
@ -126,7 +126,7 @@ class GpuStatsLogger(Callback): # pylint: disable=too-many-instance-attributes
except (libnvml.NVMLError, RuntimeError) as ex: except (libnvml.NVMLError, RuntimeError) as ex:
raise ValueError( raise ValueError(
f'Cannot use GpuStatsLogger callback because devices unavailable. ' f'Cannot use GpuStatsLogger callback because devices unavailable. '
f'Received: `gpus={device_ids}`' f'Received: `gpus={device_ids}`',
) from ex ) from ex
def on_train_epoch_start(self, trainer, pl_module) -> None: def on_train_epoch_start(self, trainer, pl_module) -> None:

View file

@ -15,7 +15,10 @@ from nvitop.version import __version__
TTY = sys.stdin.isatty() and sys.stdout.isatty() TTY = sys.stdin.isatty() and sys.stdout.isatty()
NVITOP_MONITOR_MODE = set( NVITOP_MONITOR_MODE = set(
map(str.strip, os.environ.get('NVITOP_MONITOR_MODE', '').lower().split(',')) map(
str.strip,
os.environ.get('NVITOP_MONITOR_MODE', '').lower().split(','),
),
) )
@ -23,7 +26,9 @@ NVITOP_MONITOR_MODE = set(
def parse_arguments() -> argparse.Namespace: def parse_arguments() -> argparse.Namespace:
"""Parse command-line arguments for ``nvtiop``.""" """Parse command-line arguments for ``nvtiop``."""
coloring_rules = '{} < th1 %% <= {} < th2 %% <= {}'.format( coloring_rules = '{} < th1 %% <= {} < th2 %% <= {}'.format(
colored('light', 'green'), colored('moderate', 'yellow'), colored('heavy', 'red') colored('light', 'green'),
colored('moderate', 'yellow'),
colored('heavy', 'red'),
) )
def posint(argstring: str) -> int: def posint(argstring: str) -> int:
@ -59,7 +64,11 @@ def parse_arguments() -> argparse.Namespace:
mode = parser.add_mutually_exclusive_group() mode = parser.add_mutually_exclusive_group()
mode.add_argument( mode.add_argument(
'--once', '-1', dest='once', action='store_true', help='Report query data only once.' '--once',
'-1',
dest='once',
action='store_true',
help='Report query data only once.',
) )
mode.add_argument( mode.add_argument(
'--monitor', '--monitor',
@ -354,7 +363,8 @@ def main() -> None:
grandparent = parent.parent() if parent is not None else None grandparent = parent.parent() if parent is not None else None
if grandparent is not None and parent.name() == 'sh' and grandparent.name() == 'watch': if grandparent is not None and parent.name() == 'sh' and grandparent.name() == 'watch':
messages.append( messages.append(
'HINT: You are running `nvitop` under `watch` command. Please try `nvitop -m` directly.' 'HINT: You are running `nvitop` under `watch` command. '
'Please try `nvitop -m` directly.',
) )
ui.print() ui.print()
@ -364,7 +374,7 @@ def main() -> None:
unknown_function_messages = [ unknown_function_messages = [
'ERROR: Some FunctionNotFound errors occurred while calling:' 'ERROR: Some FunctionNotFound errors occurred while calling:'
if len(libnvml.UNKNOWN_FUNCTIONS) > 1 if len(libnvml.UNKNOWN_FUNCTIONS) > 1
else 'ERROR: A FunctionNotFound error occurred while calling:' else 'ERROR: A FunctionNotFound error occurred while calling:',
] ]
unknown_function_messages.extend( unknown_function_messages.extend(
f' nvmlQuery({func.__name__!r}, *args, **kwargs)' f' nvmlQuery({func.__name__!r}, *args, **kwargs)'
@ -376,8 +386,8 @@ def main() -> None:
'You can check the release history of `nvidia-ml-py` and install the compatible version manually.\n' 'You can check the release history of `nvidia-ml-py` and install the compatible version manually.\n'
'See {} for more information.' 'See {} for more information.'
).format( ).format(
colored('https://github.com/XuehaiPan/nvitop#installation', attrs=('underline',)) colored('https://github.com/XuehaiPan/nvitop#installation', attrs=('underline',)),
) ),
) )
message = '\n'.join(unknown_function_messages) message = '\n'.join(unknown_function_messages)
if ( if (
@ -393,7 +403,7 @@ def main() -> None:
'', '',
' pip3 install "nvitop[cuda10]"', ' pip3 install "nvitop[cuda10]"',
'', '',
) ),
).replace('@VERSION@', Device.driver_version()) ).replace('@VERSION@', Device.driver_version())
messages.append(message) messages.append(message)
@ -409,7 +419,7 @@ def main() -> None:
' pip3 install --upgrade pipx', ' pip3 install --upgrade pipx',
' pipx install nvitop', ' pipx install nvitop',
'', '',
) ),
) )
messages.append(message) messages.append(message)
@ -422,7 +432,7 @@ def main() -> None:
'', '',
' pip3 install --upgrade nvitop nvidia-ml-py', ' pip3 install --upgrade nvitop nvidia-ml-py',
'', '',
) ),
) )
messages.append(message) messages.append(message)
@ -430,15 +440,21 @@ def main() -> None:
for message in messages: for message in messages:
if message.startswith('ERROR:'): if message.startswith('ERROR:'):
message = message.replace( message = message.replace(
'ERROR:', colored('ERROR:', color='red', attrs=('bold',)), 1 'ERROR:',
colored('ERROR:', color='red', attrs=('bold',)),
1,
) )
elif message.startswith('WARNING:'): elif message.startswith('WARNING:'):
message = message.replace( message = message.replace(
'WARNING:', colored('WARNING:', color='yellow', attrs=('bold',)), 1 'WARNING:',
colored('WARNING:', color='yellow', attrs=('bold',)),
1,
) )
elif message.startswith('HINT:'): elif message.startswith('HINT:'):
message = message.replace( message = message.replace(
'HINT:', colored('HINT:', color='green', attrs=('bold',)), 1 'HINT:',
colored('HINT:', color='green', attrs=('bold',)),
1,
) )
print(message, file=sys.stderr) print(message, file=sys.stderr)
return 1 return 1

View file

@ -55,7 +55,7 @@ class Displayable(CursesShortcuts): # pylint: disable=too-many-instance-attribu
self.parent = None self.parent = None
def __contains__(self, item): def __contains__(self, item):
"""Checks if item is inside the boundaries. """Check if item is inside the boundaries.
item can be an iterable like [y, x] or an object with x and y methods. item can be an iterable like [y, x] or an object with x and y methods.
""" """
@ -77,8 +77,7 @@ class Displayable(CursesShortcuts): # pylint: disable=too-many-instance-attribu
return (self.x <= x < self.x + self.width) and (self.y <= y < self.y + self.height) return (self.x <= x < self.x + self.width) and (self.y <= y < self.y + self.height)
def poke(self): def poke(self):
"""Called before drawing, even if invisible""" """Called before drawing, even if invisible."""
if self._old_visible != self.visible: if self._old_visible != self.visible:
self._old_visible = self.visible self._old_visible = self.visible
self.need_redraw = True self.need_redraw = True
@ -92,7 +91,6 @@ class Displayable(CursesShortcuts): # pylint: disable=too-many-instance-attribu
Called on every main iteration if visible. Containers should call draw() Called on every main iteration if visible. Containers should call draw()
on their contained objects here. Override this! on their contained objects here. Override this!
""" """
self.need_redraw = False self.need_redraw = False
def finalize(self): def finalize(self):
@ -100,12 +98,10 @@ class Displayable(CursesShortcuts): # pylint: disable=too-many-instance-attribu
Override this! Override this!
""" """
self.need_redraw = False self.need_redraw = False
def destroy(self): def destroy(self):
"""Called when the object is destroyed.""" """Called when the object is destroyed."""
self.win = None self.win = None
self.root = None self.root = None
@ -184,15 +180,13 @@ class DisplayableContainer(Displayable):
# extended or overridden methods # extended or overridden methods
def poke(self): def poke(self):
"""Recursively called on objects in container""" """Recursively called on objects in container."""
super().poke() super().poke()
for displayable in self.container: for displayable in self.container:
displayable.poke() displayable.poke()
def draw(self): def draw(self):
"""Recursively called on visible objects in container""" """Recursively called on visible objects in container."""
for displayable in self.container: for displayable in self.container:
if self.need_redraw: if self.need_redraw:
displayable.need_redraw = True displayable.need_redraw = True
@ -202,22 +196,19 @@ class DisplayableContainer(Displayable):
self.need_redraw = False self.need_redraw = False
def finalize(self): def finalize(self):
"""Recursively called on visible objects in container""" """Recursively called on visible objects in container."""
for displayable in self.container: for displayable in self.container:
if displayable.visible: if displayable.visible:
displayable.finalize() displayable.finalize()
def destroy(self): def destroy(self):
"""Recursively called on objects in container""" """Recursively called on objects in container."""
for displayable in self.container: for displayable in self.container:
displayable.destroy() displayable.destroy()
super().destroy() super().destroy()
def press(self, key): def press(self, key):
"""Recursively called on objects in container""" """Recursively called on objects in container."""
focused_obj = self.get_focused_obj() focused_obj = self.get_focused_obj()
if focused_obj: if focused_obj:
@ -226,8 +217,7 @@ class DisplayableContainer(Displayable):
return False return False
def click(self, event): def click(self, event):
"""Recursively called on objects in container""" """Recursively called on objects in container."""
focused_obj = self.get_focused_obj() focused_obj = self.get_focused_obj()
if focused_obj and focused_obj.click(event): if focused_obj and focused_obj.click(event):
return True return True
@ -241,7 +231,6 @@ class DisplayableContainer(Displayable):
def add_child(self, obj): def add_child(self, obj):
"""Add the objects to the container.""" """Add the objects to the container."""
if obj.parent is not None: if obj.parent is not None:
obj.parent.remove_child(obj) obj.parent.remove_child(obj)
self.container.append(obj) self.container.append(obj)
@ -250,14 +239,12 @@ class DisplayableContainer(Displayable):
def replace_child(self, old_obj, new_obj): def replace_child(self, old_obj, new_obj):
"""Replace the old object with the new instance in the container.""" """Replace the old object with the new instance in the container."""
self.container[self.container.index(old_obj)] = new_obj self.container[self.container.index(old_obj)] = new_obj
new_obj.parent = self new_obj.parent = self
new_obj.root = self.root new_obj.root = self.root
def remove_child(self, obj): def remove_child(self, obj):
"""Remove the object from the container.""" """Remove the object from the container."""
try: try:
self.container.remove(obj) self.container.remove(obj)
except ValueError: except ValueError:

View file

@ -41,7 +41,7 @@ PAIR2SYMBOL_DOWN = {
for s1, s2 in itertools.product(SYMBOL2VALUE_DOWN, repeat=2) for s1, s2 in itertools.product(SYMBOL2VALUE_DOWN, repeat=2)
} }
GRAPH_SYMBOLS = ''.join( GRAPH_SYMBOLS = ''.join(
sorted(set(itertools.chain(VALUE2SYMBOL_UP.values(), VALUE2SYMBOL_DOWN.values()))) sorted(set(itertools.chain(VALUE2SYMBOL_UP.values(), VALUE2SYMBOL_DOWN.values()))),
).replace(' ', '') ).replace(' ', '')
@ -92,7 +92,8 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
self.maxlen = 2 * self.width + 1 self.maxlen = 2 * self.width + 1
self.history = deque( self.history = deque(
[self.baseline - 0.1] * (2 * self.MAX_WIDTH + 1), maxlen=(2 * self.MAX_WIDTH + 1) [self.baseline - 0.1] * (2 * self.MAX_WIDTH + 1),
maxlen=(2 * self.MAX_WIDTH + 1),
) )
self.reversed_history = deque([self.baseline - 0.1] * self.maxlen, maxlen=self.maxlen) self.reversed_history = deque([self.baseline - 0.1] * self.maxlen, maxlen=self.maxlen)
self._max_value_maintainer = deque([self.baseline - 0.1] * self.maxlen, maxlen=self.maxlen) self._max_value_maintainer = deque([self.baseline - 0.1] * self.maxlen, maxlen=self.maxlen)
@ -118,18 +119,23 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
@width.setter @width.setter
def width(self, value): def width(self, value):
if self._width != value: if self._width != value:
assert isinstance(value, int) and value >= 1 assert isinstance(value, int)
assert value >= 1
self._width = value self._width = value
with self.write_lock: with self.write_lock:
self.maxlen = 2 * self.width + 1 self.maxlen = 2 * self.width + 1
self.reversed_history = deque( self.reversed_history = deque(
(self.baseline - 0.1,) * self.maxlen, maxlen=self.maxlen (self.baseline - 0.1,) * self.maxlen,
maxlen=self.maxlen,
) )
self._max_value_maintainer = deque( self._max_value_maintainer = deque(
(self.baseline - 0.1,) * self.maxlen, maxlen=self.maxlen (self.baseline - 0.1,) * self.maxlen,
maxlen=self.maxlen,
) )
for history in itertools.islice( for history in itertools.islice(
self.history, max(0, self.history.maxlen - self.maxlen), self.history.maxlen self.history,
max(0, self.history.maxlen - self.maxlen),
self.history.maxlen,
): ):
if self.reversed_history[-1] == self._max_value_maintainer[0]: if self.reversed_history[-1] == self._max_value_maintainer[0]:
self._max_value_maintainer.popleft() self._max_value_maintainer.popleft()
@ -149,7 +155,8 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
@height.setter @height.setter
def height(self, value): def height(self, value):
if self._height != value: if self._height != value:
assert isinstance(value, int) and value >= 1 assert isinstance(value, int)
assert value >= 1
self._height = value self._height = value
self.remake_graph() self.remake_graph()
@ -160,8 +167,10 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
@graph_size.setter @graph_size.setter
def graph_size(self, value): def graph_size(self, value):
width, height = value width, height = value
assert isinstance(width, int) and width >= 1 assert isinstance(width, int)
assert isinstance(height, int) and height >= 1 assert width >= 1
assert isinstance(height, int)
assert height >= 1
self._height = height self._height = height
self._width = width - 1 # trigger force remake self._width = width - 1 # trigger force remake
self.width = width self.width = width

View file

@ -41,7 +41,7 @@ SPECIAL_KEYS = OrderedDict(
('S-Tab', curses.KEY_BTAB), ('S-Tab', curses.KEY_BTAB),
('lt', ord('<')), ('lt', ord('<')),
('gt', ord('>')), ('gt', ord('>')),
] ],
) )
NAMED_SPECIAL_KEYS = tuple(SPECIAL_KEYS.keys()) NAMED_SPECIAL_KEYS = tuple(SPECIAL_KEYS.keys())
@ -55,7 +55,7 @@ VERY_SPECIAL_KEYS = {
def _uncase_special_key(string): def _uncase_special_key(string):
"""Uncase a special key """Uncase a special key.
>>> _uncase_special_key('Esc') >>> _uncase_special_key('Esc')
'esc' 'esc'
@ -71,7 +71,7 @@ def _uncase_special_key(string):
'a-x' 'a-x'
""" """
uncased = string.lower() uncased = string.lower()
if len(uncased) == 3 and (uncased.startswith('a-') or uncased.startswith('m-')): if len(uncased) == 3 and (uncased.startswith(('a-', 'm-'))):
uncased = f'{uncased[0]}-{string[-1]}' uncased = f'{uncased[0]}-{string[-1]}'
return uncased return uncased
@ -176,7 +176,7 @@ def key_to_string(key):
def construct_keybinding(keys): def construct_keybinding(keys):
"""Does the reverse of parse_keybinding """Do the reverse of parse_keybinding.
>>> construct_keybinding(parse_keybinding('lol<CR>')) >>> construct_keybinding(parse_keybinding('lol<CR>'))
'lol<Enter>' 'lol<Enter>'
@ -211,7 +211,7 @@ def construct_keybinding(keys):
def normalize_keybinding(keybinding): def normalize_keybinding(keybinding):
"""Normalize a keybinding to a string """Normalize a keybinding to a string.
>>> normalize_keybinding('lol<CR>') >>> normalize_keybinding('lol<CR>')
'lol<Enter>' 'lol<Enter>'
@ -275,7 +275,7 @@ class KeyMaps(dict):
pointer = pointer[key] pointer = pointer[key]
except KeyError as ex: except KeyError as ex:
raise KeyError( raise KeyError(
f'Tried to copy the keybinding `{source}`, but it was not found.' f'Tried to copy the keybinding `{source}`, but it was not found.',
) from ex ) from ex
try: try:
self.bind(context, target, copy.deepcopy(pointer)) self.bind(context, target, copy.deepcopy(pointer))

View file

@ -36,7 +36,7 @@ TRUE_COLORS = dict(
('bright cyan', 14), ('bright cyan', 14),
('bright white', 15), ('bright white', 15),
] ]
+ [(f'preserved {i:02d}', i) for i in range(16, 64)] + [(f'preserved {i:02d}', i) for i in range(16, 64)],
) )
@ -44,8 +44,7 @@ BASE_ATTR = 0
def _init_color_theme(light_theme=False): def _init_color_theme(light_theme=False):
"""Sets the default fg/bg colors.""" """Set the default fg/bg colors."""
global LIGHT_THEME, DEFAULT_FOREGROUND, DEFAULT_BACKGROUND # pylint: disable=global-statement global LIGHT_THEME, DEFAULT_FOREGROUND, DEFAULT_BACKGROUND # pylint: disable=global-statement
LIGHT_THEME = light_theme LIGHT_THEME = light_theme
@ -76,8 +75,7 @@ def _get_true_color(rgb):
def _get_color(fg, bg): def _get_color(fg, bg):
"""Returns the curses color pair for the given fg/bg combination.""" """Return the curses color pair for the given fg/bg combination."""
global COLOR_PAIRS # pylint: disable=global-statement,global-variable-not-assigned global COLOR_PAIRS # pylint: disable=global-statement,global-variable-not-assigned
if isinstance(fg, str): if isinstance(fg, str):
@ -185,8 +183,8 @@ class CursesShortcuts:
""" """
ASCII_TRANSTABLE = str.maketrans( ASCII_TRANSTABLE = str.maketrans(
'' + '─╴' + '╒╤╕╪╘╧╛┌┬┐┼└┴┘' + '│╞╡├┤▏▎▍▌▋▊▉█░' + '▲▼' + '' + GRAPH_SYMBOLS, '─╴╒╤╕╪╘╧╛┌┬┐┼└┴┘│╞╡├┤▏▎▍▌▋▊▉█░▲▼' + GRAPH_SYMBOLS,
'=' + '--' + '++++++++++++++' + '||||||||||||||' + '^v' + '?' + '=' * len(GRAPH_SYMBOLS), '=--++++++++++++++||||||||||||||^v?' + '=' * len(GRAPH_SYMBOLS),
) )
TERM_256COLOR = False TERM_256COLOR = False
@ -232,17 +230,14 @@ class CursesShortcuts:
def color(self, fg=-1, bg=-1, attr=0): def color(self, fg=-1, bg=-1, attr=0):
"""Change the colors from now on.""" """Change the colors from now on."""
return self.set_fg_bg_attr(fg, bg, attr) return self.set_fg_bg_attr(fg, bg, attr)
def color_reset(self): def color_reset(self):
"""Change the colors to the default colors""" """Change the colors to the default colors."""
return self.color() return self.color()
def color_at(self, y, x, width, *args, **kwargs): def color_at(self, y, x, width, *args, **kwargs):
"""Change the colors at the specified position""" """Change the colors at the specified position."""
try: try:
self.win.chgat(y, x, width, self.get_fg_bg_attr(*args, **kwargs)) self.win.chgat(y, x, width, self.get_fg_bg_attr(*args, **kwargs))
except curses.error: except curses.error:
@ -250,8 +245,7 @@ class CursesShortcuts:
@staticmethod @staticmethod
def get_fg_bg_attr(fg=-1, bg=-1, attr=0): def get_fg_bg_attr(fg=-1, bg=-1, attr=0):
"""Returns the curses attribute for the given fg/bg/attr combination.""" """Return the curses attribute for the given fg/bg/attr combination."""
if fg == -1 and bg == -1 and attr == 0: if fg == -1 and bg == -1 and attr == 0:
return BASE_ATTR return BASE_ATTR
@ -283,10 +277,11 @@ class CursesShortcuts:
return attr return attr
def update_size(self, termsize=None): def update_size(self, termsize=None):
if termsize is None: if termsize is not None:
self.update_lines_cols() return termsize
termsize = self.win.getmaxyx()
return termsize self.update_lines_cols()
return self.win.getmaxyx()
@staticmethod @staticmethod
def update_lines_cols(): def update_lines_cols():

View file

@ -42,7 +42,8 @@ class MessageBox(Displayable): # pylint: disable=too-many-instance-attributes
self.options = options self.options = options
self.num_options = len(self.options) self.num_options = len(self.options)
assert cancel is not None and self.num_options >= 2 assert cancel is not None
assert self.num_options >= 2
assert 0 <= no < self.num_options assert 0 <= no < self.num_options
assert 0 <= cancel < self.num_options assert 0 <= cancel < self.num_options
assert 0 <= default < self.num_options assert 0 <= default < self.num_options

View file

@ -39,7 +39,7 @@ class MouseEvent:
CTRL_SCROLLWHEEL_MULTIPLIER = 5 CTRL_SCROLLWHEEL_MULTIPLIER = 5
def __init__(self, state): def __init__(self, state):
"""Creates a MouseEvent object from the result of win.getmouse()""" """Create a MouseEvent object from the result of win.getmouse()."""
_, self.x, self.y, _, self.bstate = state _, self.x, self.y, _, self.bstate = state
# x-values above ~220 suddenly became negative, apparently # x-values above ~220 suddenly became negative, apparently
@ -51,35 +51,35 @@ class MouseEvent:
self.y += 0xFF self.y += 0xFF
def pressed(self, n): def pressed(self, n):
"""Returns whether the mouse key n is pressed""" """Return whether the mouse key n is pressed."""
try: try:
return (self.bstate & MouseEvent.PRESSED[n]) != 0 return (self.bstate & MouseEvent.PRESSED[n]) != 0
except IndexError: except IndexError:
return False return False
def released(self, n): def released(self, n):
"""Returns whether the mouse key n is released""" """Return whether the mouse key n is released."""
try: try:
return (self.bstate & MouseEvent.RELEASED[n]) != 0 return (self.bstate & MouseEvent.RELEASED[n]) != 0
except IndexError: except IndexError:
return False return False
def clicked(self, n): def clicked(self, n):
"""Returns whether the mouse key n is clicked""" """Return whether the mouse key n is clicked."""
try: try:
return (self.bstate & MouseEvent.CLICKED[n]) != 0 return (self.bstate & MouseEvent.CLICKED[n]) != 0
except IndexError: except IndexError:
return False return False
def double_clicked(self, n): def double_clicked(self, n):
"""Returns whether the mouse key n is double clicked""" """Return whether the mouse key n is double clicked."""
try: try:
return (self.bstate & MouseEvent.DOUBLE_CLICKED[n]) != 0 return (self.bstate & MouseEvent.DOUBLE_CLICKED[n]) != 0
except IndexError: except IndexError:
return False return False
def wheel_direction(self): def wheel_direction(self):
"""Returns the direction of the scroll action, 0 if there was none""" """Return the direction of the scroll action, 0 if there was none."""
# If the bstate > ALL_MOUSE_EVENTS, it's an invalid mouse button. # If the bstate > ALL_MOUSE_EVENTS, it's an invalid mouse button.
# I interpret invalid buttons as "scroll down" because all tested # I interpret invalid buttons as "scroll down" because all tested
# systems have a broken curses implementation and this is a workaround. # systems have a broken curses implementation and this is a workaround.

View file

@ -124,7 +124,7 @@ class Selection: # pylint: disable=too-many-instance-attributes
self.send_signal( self.send_signal(
signal.SIGINT signal.SIGINT
if not host.WINDOWS if not host.WINDOWS
else signal.CTRL_C_EVENT # pylint: disable=no-member else signal.CTRL_C_EVENT, # pylint: disable=no-member
) )
except SystemError: except SystemError:
pass pass

View file

@ -14,16 +14,14 @@ WIDE_SYMBOLS = set('WF')
def utf_char_width(string): def utf_char_width(string):
"""Return the width of a single character""" """Return the width of a single character."""
if east_asian_width(string) in WIDE_SYMBOLS: if east_asian_width(string) in WIDE_SYMBOLS:
return WIDE return WIDE
return NARROW return NARROW
def string_to_charlist(string): def string_to_charlist(string):
"""Return a list of characters with extra empty strings after wide chars""" """Return a list of characters with extra empty strings after wide chars."""
if ASCIIONLY.issuperset(string): if ASCIIONLY.issuperset(string):
return list(string) return list(string)
result = [] result = []
@ -35,8 +33,7 @@ def string_to_charlist(string):
def wcslen(string): def wcslen(string):
"""Return the length of a string with wide chars""" """Return the length of a string with wide chars."""
return len(WideString(string)) return len(WideString(string))
@ -63,7 +60,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> (WideString('afd') + 'bc').chars >>> (WideString('afd') + 'bc').chars
['a', 'f', 'd', 'b', 'c'] ['a', 'f', 'd', 'b', 'c']
""" """
if isinstance(other, str): if isinstance(other, str):
return WideString(self.string + other) return WideString(self.string + other)
if isinstance(other, WideString): if isinstance(other, WideString):
@ -75,7 +71,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> ('bc' + WideString('afd')).chars >>> ('bc' + WideString('afd')).chars
['b', 'c', 'a', 'f', 'd'] ['b', 'c', 'a', 'f', 'd']
""" """
if isinstance(other, str): if isinstance(other, str):
return WideString(other + self.string) return WideString(other + self.string)
if isinstance(other, WideString): if isinstance(other, WideString):
@ -131,7 +126,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString('aモ')[0:1] >>> WideString('aモ')[0:1]
<WideString 'a'> <WideString 'a'>
""" """
if isinstance(item, slice): if isinstance(item, slice):
assert item.step is None or item.step == 1 assert item.step is None or item.step == 1
start, stop = item.start, item.stop start, stop = item.start, item.stop
@ -166,7 +160,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> len(WideString('モヒカン')) >>> len(WideString('モヒカン'))
8 8
""" """
return len(self.chars) return len(self.chars)
def ljust(self, width, fillchar=' '): def ljust(self, width, fillchar=' '):
@ -178,7 +171,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString('モヒカン').ljust(10) >>> WideString('モヒカン').ljust(10)
<WideString 'モヒカン '> <WideString 'モヒカン '>
""" """
if width > len(self): if width > len(self):
return WideString(self.string + fillchar * width)[:width] return WideString(self.string + fillchar * width)[:width]
return self return self
@ -192,7 +184,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString('モヒカン').rljust(10) >>> WideString('モヒカン').rljust(10)
<WideString ' モヒカン'> <WideString ' モヒカン'>
""" """
if width > len(self): if width > len(self):
return WideString(fillchar * width + self.string)[-width:] return WideString(fillchar * width + self.string)[-width:]
return self return self
@ -206,7 +197,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString('モヒカン').center(10) >>> WideString('モヒカン').center(10)
<WideString ' モヒカン '> <WideString ' モヒカン '>
""" """
if width > len(self): if width > len(self):
left_width = (width - len(self)) // 2 left_width = (width - len(self)) // 2
right_width = width - left_width right_width = width - left_width
@ -220,7 +210,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString(' モヒカン ').strip() >>> WideString(' モヒカン ').strip()
<WideString 'モヒカン'> <WideString 'モヒカン'>
""" """
return WideString(self.string.strip(chars)) return WideString(self.string.strip(chars))
def lstrip(self, chars=None): def lstrip(self, chars=None):
@ -230,7 +219,6 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString(' モヒカン ').lstrip() >>> WideString(' モヒカン ').lstrip()
<WideString 'モヒカン '> <WideString 'モヒカン '>
""" """
return WideString(self.string.lstrip(chars)) return WideString(self.string.lstrip(chars))
def rstrip(self, chars=None): def rstrip(self, chars=None):
@ -240,5 +228,4 @@ class WideString: # pylint: disable=too-few-public-methods,wrong-spelling-in-do
>>> WideString(' モヒカン ').rstrip() >>> WideString(' モヒカン ').rstrip()
<WideString ' モヒカン'> <WideString ' モヒカン'>
""" """
return WideString(self.string.rstrip(chars)) return WideString(self.string.rstrip(chars))

View file

@ -127,7 +127,8 @@ class EnvironScreen(Displayable): # pylint: disable=too-many-instance-attribute
n_items = len(self.environ) n_items = len(self.environ)
old_scroll_offset = self.scroll_offset old_scroll_offset = self.scroll_offset
self.scroll_offset = max( self.scroll_offset = max(
0, min(self.scroll_offset + direction, n_items - self.display_height) 0,
min(self.scroll_offset + direction, n_items - self.display_height),
) )
direction -= self.scroll_offset - old_scroll_offset direction -= self.scroll_offset - old_scroll_offset
self._y_offset += self.scroll_offset - old_scroll_offset self._y_offset += self.scroll_offset - old_scroll_offset
@ -146,12 +147,13 @@ class EnvironScreen(Displayable): # pylint: disable=too-many-instance-attribute
if isinstance(self.process, GpuProcess): if isinstance(self.process, GpuProcess):
process_type = 'GPU: ' + self.process.type.replace('C', 'Compute').replace( process_type = 'GPU: ' + self.process.type.replace('C', 'Compute').replace(
'G', 'Graphics' 'G',
'Graphics',
) )
else: else:
process_type = 'Host' process_type = 'Host'
header_prefix = WideString( header_prefix = WideString(
f'Environment of process {self.process.pid} ({self.username}@{process_type}): ' f'Environment of process {self.process.pid} ({self.username}@{process_type}): ',
) )
offset = max(0, min(self.x_offset, len(self.command) + len(header_prefix) - self.width)) offset = max(0, min(self.x_offset, len(self.command) + len(header_prefix) - self.width))
header = str((header_prefix + self.command[offset:]).ljust(self.width)[: self.width]) header = str((header_prefix + self.command[offset:]).ljust(self.width)[: self.width])

View file

@ -7,7 +7,7 @@ from nvitop.gui.library import Device, Displayable, MouseEvent
from nvitop.version import __version__ from nvitop.version import __version__
HELP_TEMPLATE = '''nvitop {} - (C) Xuehai Pan, 2021-2023. HELP_TEMPLATE = """nvitop {} - (C) Xuehai Pan, 2021-2023.
Released under the GNU GPLv3 License. Released under the GNU GPLv3 License.
GPU Process Type: C: Compute, G: Graphics, X: Mixed. GPU Process Type: C: Compute, G: Graphics, X: Mixed.
@ -38,7 +38,7 @@ Device coloring rules by loading intensity:
, .: select sort column /: invert sort order , .: select sort column /: invert sort order
Press any key to return. Press any key to return.
''' """
class HelpScreen(Displayable): # pylint: disable=too-many-instance-attributes class HelpScreen(Displayable): # pylint: disable=too-many-instance-attributes

View file

@ -12,7 +12,7 @@ from nvitop.gui.screens.main.host import HostPanel
from nvitop.gui.screens.main.process import ProcessPanel from nvitop.gui.screens.main.process import ProcessPanel
class BreakLoop(Exception): class BreakLoop(Exception): # noqa: N818
pass pass
@ -44,7 +44,11 @@ class MainScreen(DisplayableContainer): # pylint: disable=too-many-instance-att
self.add_child(self.host_panel) self.add_child(self.host_panel)
self.process_panel = ProcessPanel( self.process_panel = ProcessPanel(
self.device_panel.leaf_devices, compact, filters, win=win, root=root self.device_panel.leaf_devices,
compact,
filters,
win=win,
root=root,
) )
self.process_panel.focused = False self.process_panel.focused = False
self.add_child(self.process_panel) self.add_child(self.process_panel)
@ -265,8 +269,12 @@ class MainScreen(DisplayableContainer): # pylint: disable=too-many-instance-att
keymaps.bind('main', '/', order_reverse) keymaps.bind('main', '/', order_reverse)
for order in ProcessPanel.ORDERS: for order in ProcessPanel.ORDERS:
keymaps.bind( keymaps.bind(
'main', 'o' + order[:1].lower(), partial(sort_by, order=order, reverse=False) 'main',
'o' + order[:1].lower(),
partial(sort_by, order=order, reverse=False),
) )
keymaps.bind( keymaps.bind(
'main', 'o' + order[:1].upper(), partial(sort_by, order=order, reverse=True) 'main',
'o' + order[:1].upper(),
partial(sort_by, order=order, reverse=True),
) )

View file

@ -60,14 +60,16 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
self.snapshot_lock = threading.Lock() self.snapshot_lock = threading.Lock()
self.snapshots = self.take_snapshots() self.snapshots = self.take_snapshots()
self._snapshot_daemon = threading.Thread( self._snapshot_daemon = threading.Thread(
name='device-snapshot-daemon', target=self._snapshot_target, daemon=True name='device-snapshot-daemon',
target=self._snapshot_target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
self.formats_compact = [ self.formats_compact = [
'{physical_index:>3} {fan_speed_string:>3} {temperature_string:>4} ' '{physical_index:>3} {fan_speed_string:>3} {temperature_string:>4} '
'{performance_state:>3} {power_status:>12} ' '{performance_state:>3} {power_status:>12} '
'{memory_usage:>20}{gpu_utilization_string:>7} {compute_mode:>11}' '{memory_usage:>20}{gpu_utilization_string:>7} {compute_mode:>11}',
] ]
self.formats_full = [ self.formats_full = [
'{physical_index:>3} {name:<18} {persistence_mode:<4} ' '{physical_index:>3} {name:<18} {persistence_mode:<4} '
@ -78,12 +80,13 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
self.mig_formats = [ self.mig_formats = [
'{physical_index:>2}:{mig_index:<2}{name:>12} @ GI/CI:{gpu_instance_id:>2}/{compute_instance_id:<2}' '{physical_index:>2}:{mig_index:<2}{name:>12} @ GI/CI:{gpu_instance_id:>2}/{compute_instance_id:<2}'
'{memory_usage:>20} │ BAR1: {bar1_memory_used_human:>8} / {bar1_memory_percent_string:>3}' '{memory_usage:>20} │ BAR1: {bar1_memory_used_human:>8} / {bar1_memory_percent_string:>3}',
] ]
if host.WINDOWS: if host.WINDOWS:
self.formats_full[0] = self.formats_full[0].replace( self.formats_full[0] = self.formats_full[0].replace(
'persistence_mode', 'current_driver_model' 'persistence_mode',
'current_driver_model',
) )
self.support_mig = any('N/A' not in device.mig_mode for device in self.snapshots) self.support_mig = any('N/A' not in device.mig_mode for device in self.snapshots)
@ -143,10 +146,12 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
device.name = cut_string(device.name, maxlen=18, padstr='..', align='right') device.name = cut_string(device.name, maxlen=18, padstr='..', align='right')
device.current_driver_model = device.current_driver_model.replace('WDM', 'TCC') device.current_driver_model = device.current_driver_model.replace('WDM', 'TCC')
device.display_active = device.display_active.replace('Enabled', 'On').replace( device.display_active = device.display_active.replace('Enabled', 'On').replace(
'Disabled', 'Off' 'Disabled',
'Off',
) )
device.persistence_mode = device.persistence_mode.replace('Enabled', 'On').replace( device.persistence_mode = device.persistence_mode.replace('Enabled', 'On').replace(
'Disabled', 'Off' 'Disabled',
'Off',
) )
device.mig_mode = device.mig_mode.replace('N/A', 'N/A ') device.mig_mode = device.mig_mode.replace('N/A', 'N/A ')
device.compute_mode = device.compute_mode.replace('Exclusive', 'E.') device.compute_mode = device.compute_mode.replace('Exclusive', 'E.')
@ -183,25 +188,25 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
] ]
if self.device_count > 0: if self.device_count > 0:
header.append( header.append(
'├───────────────────────────────┬──────────────────────┬──────────────────────┤' '├───────────────────────────────┬──────────────────────┬──────────────────────┤',
) )
if compact: if compact:
header.append( header.append(
'│ GPU Fan Temp Perf Pwr:Usg/Cap │ Memory-Usage │ GPU-Util Compute M. │' '│ GPU Fan Temp Perf Pwr:Usg/Cap │ Memory-Usage │ GPU-Util Compute M. │',
) )
else: else:
header.extend( header.extend(
( (
'│ GPU Name Persistence-M│ Bus-Id Disp.A │ Volatile Uncorr. ECC │', '│ GPU Name Persistence-M│ Bus-Id Disp.A │ Volatile Uncorr. ECC │',
'│ Fan Temp Perf Pwr:Usage/Cap│ Memory-Usage │ GPU-Util Compute M. │', '│ Fan Temp Perf Pwr:Usage/Cap│ Memory-Usage │ GPU-Util Compute M. │',
) ),
) )
if host.WINDOWS: if host.WINDOWS:
header[-2] = header[-2].replace('Persistence-M', ' TCC/WDDM ') header[-2] = header[-2].replace('Persistence-M', ' TCC/WDDM ')
if self.support_mig: if self.support_mig:
header[-2] = header[-2].replace('Volatile Uncorr. ECC', 'MIG M. Uncorr. ECC') header[-2] = header[-2].replace('Volatile Uncorr. ECC', 'MIG M. Uncorr. ECC')
header.append( header.append(
'╞═══════════════════════════════╪══════════════════════╪══════════════════════╡' '╞═══════════════════════════════╪══════════════════════╪══════════════════════╡',
) )
else: else:
header.extend( header.extend(
@ -209,7 +214,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
'╞═════════════════════════════════════════════════════════════════════════════╡', '╞═════════════════════════════════════════════════════════════════════════════╡',
'│ No visible devices found │', '│ No visible devices found │',
'╘═════════════════════════════════════════════════════════════════════════════╛', '╘═════════════════════════════════════════════════════════════════════════════╛',
) ),
) )
return header return header
@ -241,7 +246,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
frame.pop() frame.pop()
frame.append( frame.append(
'╘═══════════════════════════════╧══════════════════════╧══════════════════════╛' '╘═══════════════════════════════╧══════════════════════╧══════════════════════╛',
) )
if self.width >= 100: if self.width >= 100:
frame[5 - int(compact)] = ( frame[5 - int(compact)] = (
@ -273,10 +278,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
self.addstr(self.y, self.x, cut_string(time.strftime('%a %b %d %H:%M:%S %Y'), maxlen=32)) self.addstr(self.y, self.x, cut_string(time.strftime('%a %b %d %H:%M:%S %Y'), maxlen=32))
if self.compact: formats = self.formats_compact if self.compact else self.formats_full
formats = self.formats_compact
else:
formats = self.formats_full
remaining_width = self.width - 79 remaining_width = self.width - 79
draw_bars = self.width >= 100 draw_bars = self.width >= 100
@ -375,7 +377,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
full_bar_len = width - prefix_len - 5 full_bar_len = width - prefix_len - 5
self.color_at(y, x_offset, width=width, fg=float(bar_len / full_bar_len)) self.color_at(y, x_offset, width=width, fg=float(bar_len / full_bar_len))
for i, x in enumerate( for i, x in enumerate(
range(x_offset + prefix_len + 1, x_offset + prefix_len + 1 + bar_len) range(x_offset + prefix_len + 1, x_offset + prefix_len + 1 + bar_len),
): ):
self.color_at(y, x, width=1, fg=float(i / full_bar_len)) self.color_at(y, x, width=1, fg=float(i / full_bar_len))
else: else:
@ -404,7 +406,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
or prev_device_index[0] != device.tuple_index[0] or prev_device_index[0] != device.tuple_index[0]
): ):
lines.append( lines.append(
'├───────────────────────────────┼──────────────────────┼──────────────────────┤' '├───────────────────────────────┼──────────────────────┼──────────────────────┤',
) )
def colorize(s): def colorize(s):
@ -421,7 +423,7 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
prev_device_index = device.tuple_index prev_device_index = device.tuple_index
lines.append( lines.append(
'╘═══════════════════════════════╧══════════════════════╧══════════════════════╛' '╘═══════════════════════════════╧══════════════════════╧══════════════════════╛',
) )
if self.width >= 100: if self.width >= 100:
@ -458,7 +460,9 @@ class DevicePanel(Displayable): # pylint: disable=too-many-instance-attributes
matrix.pop() matrix.pop()
for y, (prefix, utilization, color) in enumerate(matrix, start=y_start): for y, (prefix, utilization, color) in enumerate(matrix, start=y_start):
bar = make_bar( # pylint: disable=disallowed-name bar = make_bar( # pylint: disable=disallowed-name
prefix, utilization, width=remaining_width - 3 prefix,
utilization,
width=remaining_width - 3,
) )
lines[y] += f' {colored(bar, color)}' lines[y] += f' {colored(bar, color)}'

View file

@ -43,7 +43,9 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.memory_percent = None self.memory_percent = None
self.swap_percent = None self.swap_percent = None
self._snapshot_daemon = threading.Thread( self._snapshot_daemon = threading.Thread(
name='host-snapshot-daemon', target=self._snapshot_target, daemon=True name='host-snapshot-daemon',
target=self._snapshot_target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
@ -262,7 +264,11 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.color_at(self.y + 1, self.x, width=width_left, fg='magenta', attr='bold') self.color_at(self.y + 1, self.x, width=width_left, fg='magenta', attr='bold')
self.color_at(self.y, self.x + width_left + 2, width=width_right, attr='bold') self.color_at(self.y, self.x + width_left + 2, width=width_right, attr='bold')
self.color_at( self.color_at(
self.y + 1, self.x + width_left + 2, width=width_right, fg='blue', attr='bold' self.y + 1,
self.x + width_left + 2,
width=width_right,
fg='blue',
attr='bold',
) )
return return

View file

@ -142,7 +142,9 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
self._snapshots = [] self._snapshots = []
self.snapshot_lock = threading.Lock() self.snapshot_lock = threading.Lock()
self._snapshot_daemon = threading.Thread( self._snapshot_daemon = threading.Thread(
name='process-snapshot-daemon', target=self._snapshot_target, daemon=True name='process-snapshot-daemon',
target=self._snapshot_target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
@ -168,7 +170,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
self._compact = value self._compact = value
processes = self.snapshots processes = self.snapshots
n_processes, n_devices = len(processes), len( n_processes, n_devices = len(processes), len(
{p.device.physical_index for p in processes} {p.device.physical_index for p in processes},
) )
self.full_height = 1 + max(6, 5 + n_processes + n_devices - 1) self.full_height = 1 + max(6, 5 + n_processes + n_devices - 1)
self.compact_height = 1 + max(6, 5 + n_processes) self.compact_height = 1 + max(6, 5 + n_processes)
@ -242,7 +244,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
cls.SNAPSHOT_INTERVAL = min(interval / 3.0, 1.0) cls.SNAPSHOT_INTERVAL = min(interval / 3.0, 1.0)
cls.take_snapshots = ttl_cache(ttl=interval)( cls.take_snapshots = ttl_cache(ttl=interval)(
cls.take_snapshots.__wrapped__ # pylint: disable=no-member cls.take_snapshots.__wrapped__, # pylint: disable=no-member
) )
def ensure_snapshots(self): def ensure_snapshots(self):
@ -265,7 +267,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
' ' * (time_length - len(snapshot.running_time_human)) ' ' * (time_length - len(snapshot.running_time_human))
+ snapshot.running_time_human, + snapshot.running_time_human,
snapshot.command, snapshot.command,
) ),
) )
with self.snapshot_lock: with self.snapshot_lock:
@ -284,7 +286,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
'' + '' * (self.width - 2) + '', '' + '' * (self.width - 2) + '',
'{}'.format('Processes:'.ljust(self.width - 4)), '{}'.format('Processes:'.ljust(self.width - 4)),
'│ GPU PID USER GPU-MEM %SM {}'.format( '│ GPU PID USER GPU-MEM %SM {}'.format(
' '.join(self.host_headers).ljust(self.width - 40) ' '.join(self.host_headers).ljust(self.width - 40),
), ),
'' + '' * (self.width - 2) + '', '' + '' * (self.width - 2) + '',
] ]
@ -294,14 +296,14 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
else: else:
message = ' Gathering process status...' message = ' Gathering process status...'
header.extend( header.extend(
[f'{message.ljust(self.width - 4)}', '' + '' * (self.width - 2) + ''] [f'{message.ljust(self.width - 4)}', '' + '' * (self.width - 2) + ''],
) )
return header return header
@property @property
def processes(self): def processes(self):
return list( return list(
itertools.chain.from_iterable(device.processes().values() for device in self.devices) itertools.chain.from_iterable(device.processes().values() for device in self.devices),
) )
def poke(self): def poke(self):
@ -313,7 +315,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.selection.within_window = False self.selection.within_window = False
if len(self.snapshots) > 0 and self.selection.is_set(): if len(self.snapshots) > 0 and self.selection.is_set():
y = self.y + 5 # noqa: SIM113 y = self.y + 5
prev_device_index = None prev_device_index = None
for process in self.snapshots: for process in self.snapshots:
device_index = process.device.physical_index device_index = process.device.physical_index
@ -397,7 +399,10 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
if offset > 38 or host_offset == 0: if offset > 38 or host_offset == 0:
self.addstr(self.y + 3, self.x + offset - 1, column + indicator) self.addstr(self.y + 3, self.x + offset - 1, column + indicator)
self.color_at( self.color_at(
self.y + 3, self.x + offset - 1, width=column_width, attr='bold | underline' self.y + 3,
self.x + offset - 1,
width=column_width,
attr='bold | underline',
) )
elif offset <= 38 < offset + column_width: elif offset <= 38 < offset + column_width:
self.addstr(self.y + 3, self.x + 38, (column + indicator)[39 - offset :]) self.addstr(self.y + 3, self.x + 38, (column + indicator)[39 - offset :])
@ -415,7 +420,10 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
else: else:
self.addstr(self.y + 3, self.x + offset - 1, column + indicator) self.addstr(self.y + 3, self.x + offset - 1, column + indicator)
self.color_at( self.color_at(
self.y + 3, self.x + offset - 1, width=column_width, attr='bold | underline' self.y + 3,
self.x + offset - 1,
width=column_width,
attr='bold | underline',
) )
self.color_at(self.y + 3, self.x + offset + column_width - 1, width=1, attr='bold') self.color_at(self.y + 3, self.x + offset + column_width - 1, width=1, attr='bold')
@ -426,7 +434,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.selection.within_window = False self.selection.within_window = False
if len(self.snapshots) > 0: if len(self.snapshots) > 0:
y = self.y + 5 # noqa: SIM113 y = self.y + 5
prev_device_index = None prev_device_index = None
prev_device_display_index = None prev_device_display_index = None
color = -1 color = -1
@ -457,7 +465,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
cut_string(process.pid, maxlen=7, padstr='.'), cut_string(process.pid, maxlen=7, padstr='.'),
process.type, process.type,
str( str(
WideString(cut_string(process.username, maxlen=7, padstr='+')).rjust(7) WideString(cut_string(process.username, maxlen=7, padstr='+')).rjust(7),
), ),
process.gpu_memory_human, process.gpu_memory_human,
process.gpu_sm_utilization_string.replace('%', ''), process.gpu_sm_utilization_string.replace('%', ''),
@ -542,7 +550,8 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
def print_width(self): def print_width(self):
self.ensure_snapshots() self.ensure_snapshots()
return min( return min(
self.width, max((39 + len(process.host_info) for process in self.snapshots), default=79) self.width,
max((39 + len(process.host_info) for process in self.snapshots), default=79),
) )
def print(self): def print(self):
@ -556,7 +565,7 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
colored('@', attrs=('bold',)), colored('@', attrs=('bold',)),
colored(HOSTNAME, color='green', attrs=('bold',)), colored(HOSTNAME, color='green', attrs=('bold',)),
lines[2][-2:], lines[2][-2:],
) ),
) )
if len(self.snapshots) > 0: if len(self.snapshots) > 0:
@ -591,7 +600,8 @@ class ProcessPanel(Displayable): # pylint: disable=too-many-instance-attributes
if process.username != USERNAME and not SUPERUSER: if process.username != USERNAME and not SUPERUSER:
info = (colored(item, attrs=('dark',)) for item in info) info = (colored(item, attrs=('dark',)) for item in info)
info = colored( info = colored(
process.command, color=('red' if process.is_gone else 'yellow') process.command,
color=('red' if process.is_gone else 'yellow'),
).join(info) ).join(info)
elif process.username != USERNAME and not SUPERUSER: elif process.username != USERNAME and not SUPERUSER:
info = colored(info, attrs=('dark',)) info = colored(info, attrs=('dark',))

View file

@ -58,10 +58,7 @@ def get_yticks(history, y_offset): # pylint: disable=too-many-branches,too-many
if len(h2p) >= 2: if len(h2p) >= 2:
(hm1, pm1), (h2, p2) = h2p[-2:] (hm1, pm1), (h2, p2) = h2p[-2:]
if height < 12: if height < 12:
if h2e[hm1] < h2e[h2]: ticks = [(hm1, pm1)] if h2e[hm1] < h2e[h2] else [(h2, p2)]
ticks = [(hm1, pm1)]
else:
ticks = [(h2, p2)]
else: else:
ticks = [(h2, p2)] ticks = [(h2, p2)]
if p2 % 2 == 0: if p2 % 2 == 0:
@ -96,7 +93,9 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.enabled = False self.enabled = False
self.snapshot_lock = threading.Lock() self.snapshot_lock = threading.Lock()
self._snapshot_daemon = threading.Thread( self._snapshot_daemon = threading.Thread(
name='process-metrics-snapshot-daemon', target=self._snapshot_target, daemon=True name='process-metrics-snapshot-daemon',
target=self._snapshot_target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
@ -313,7 +312,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
def frame_lines(self): def frame_lines(self):
line = '' + ' ' * self.left_width + '' + ' ' * self.right_width + '' line = '' + ' ' * self.left_width + '' + ' ' * self.right_width + ''
frame = [ return [
'' + '' * (self.width - 2) + '', '' + '' * (self.width - 2) + '',
'{}'.format('Process:'.ljust(self.width - 4)), '{}'.format('Process:'.ljust(self.width - 4)),
'{}'.format('GPU'.ljust(self.width - 4)), '{}'.format('GPU'.ljust(self.width - 4)),
@ -325,7 +324,6 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
*([line] * self.lower_height), *([line] * self.lower_height),
'' + '' * self.left_width + '' + '' * self.right_width + '', '' + '' * self.left_width + '' + '' * self.right_width + '',
] ]
return frame
def poke(self): def poke(self):
if self.visible and not self._daemon_running.is_set(): if self.visible and not self._daemon_running.is_set():
@ -403,7 +401,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
WideString(process.username).rjust(4), WideString(process.username).rjust(4),
maxlen=32, maxlen=32,
padstr='+', padstr='+',
) ),
), ),
), ),
(' GPU-MEM', process.gpu_memory_human.rjust(8)), (' GPU-MEM', process.gpu_memory_human.rjust(8)),
@ -414,7 +412,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
(' %CPU', process.cpu_percent_string.rjust(6)), (' %CPU', process.cpu_percent_string.rjust(6)),
(' %MEM', process.memory_percent_string.rjust(5)), (' %MEM', process.memory_percent_string.rjust(5)),
(' TIME', (' ' + process.running_time_human).rjust(5)), (' TIME', (' ' + process.running_time_human).rjust(5)),
] ],
) )
x = self.x + 1 x = self.x + 1
@ -438,7 +436,10 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.addstr(self.y + 2, self.x + 1, header.ljust(self.width - 2)) self.addstr(self.y + 2, self.x + 1, header.ljust(self.width - 2))
self.addstr(self.y + 4, self.x + 1, str(fields.ljust(self.width - 2))) self.addstr(self.y + 4, self.x + 1, str(fields.ljust(self.width - 2)))
self.color_at( self.color_at(
self.y + 4, self.x + 1, width=4, fg=self.process.device.snapshot.display_color self.y + 4,
self.x + 1,
width=4,
fg=self.process.device.snapshot.display_color,
) )
if no_break: if no_break:
@ -448,7 +449,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.y + 2, self.y + 2,
x, x,
cut_string('COMMAND', self.width - x - 2, padstr='..').ljust( cut_string('COMMAND', self.width - x - 2, padstr='..').ljust(
self.width - x - 2 self.width - x - 2,
), ),
) )
if process.is_zombie or process.no_permissions: if process.is_zombie or process.no_permissions:
@ -471,7 +472,8 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.color(fg='magenta') self.color(fg='magenta')
for y, line in enumerate( for y, line in enumerate(
self.used_host_memory.graph, start=self.y + self.upper_height + 7 self.used_host_memory.graph,
start=self.y + self.upper_height + 7,
): ):
self.addstr(y, self.x + 1, line) self.addstr(y, self.x + 1, line)
@ -480,7 +482,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.upper_height - 1 self.upper_height - 1
) )
for i, (y, line) in enumerate( for i, (y, line) in enumerate(
enumerate(self.used_gpu_memory.graph, start=self.y + 6) enumerate(self.used_gpu_memory.graph, start=self.y + 6),
): ):
self.addstr( self.addstr(
y, y,
@ -493,7 +495,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.lower_height - 1 self.lower_height - 1
) )
for i, (y, line) in enumerate( for i, (y, line) in enumerate(
enumerate(self.gpu_sm_utilization.graph, start=self.y + self.upper_height + 7) enumerate(self.gpu_sm_utilization.graph, start=self.y + self.upper_height + 7),
): ):
self.addstr( self.addstr(
y, y,
@ -508,7 +510,8 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.color(fg=self.process.device.snapshot.gpu_display_color) self.color(fg=self.process.device.snapshot.gpu_display_color)
for y, line in enumerate( for y, line in enumerate(
self.gpu_sm_utilization.graph, start=self.y + self.upper_height + 7 self.gpu_sm_utilization.graph,
start=self.y + self.upper_height + 7,
): ):
self.addstr(y, self.x + self.left_width + 2, line) self.addstr(y, self.x + self.left_width + 2, line)
@ -528,7 +531,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.used_host_memory.max_value_string(), self.used_host_memory.max_value_string(),
maxlen=self.left_width - 2, maxlen=self.left_width - 2,
padstr='..', padstr='..',
) ),
), ),
) )
self.addstr( self.addstr(
@ -539,7 +542,7 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.used_gpu_memory.max_value_string(), self.used_gpu_memory.max_value_string(),
maxlen=self.right_width - 2, maxlen=self.right_width - 2,
padstr='..', padstr='..',
) ),
), ),
) )
self.addstr(self.y + 7, self.x + self.left_width + 6, f' {self.used_gpu_memory} ') self.addstr(self.y + 7, self.x + self.left_width + 6, f' {self.used_gpu_memory} ')
@ -558,7 +561,8 @@ class ProcessMetricsScreen(Displayable): # pylint: disable=too-many-instance-at
self.addstr(y, self.x, '') self.addstr(y, self.x, '')
self.addstr(y, self.x + self.left_width + 1, '') self.addstr(y, self.x + self.left_width + 1, '')
for y in range( for y in range(
self.y + self.upper_height + 7, self.y + self.upper_height + self.lower_height + 7 self.y + self.upper_height + 7,
self.y + self.upper_height + self.lower_height + 7,
): ):
self.addstr(y, self.x, '') self.addstr(y, self.x, '')
self.addstr(y, self.x + self.left_width + 1, '') self.addstr(y, self.x + self.left_width + 1, '')

View file

@ -129,7 +129,7 @@ class TreeNode: # pylint: disable=too-many-instance-attributes
node._gone, # pylint: disable=protected-access node._gone, # pylint: disable=protected-access
node.username, node.username,
node.pid, node.pid,
) ),
) )
for child in self.children: for child in self.children:
child.is_last = False child.is_last = False
@ -193,9 +193,7 @@ class TreeNode: # pylint: disable=too-many-instance-attributes
nodes[cpid] = child = cls(HostProcess(cpid)) nodes[cpid] = child = cls(HostProcess(cpid))
node.add(child) node.add(child)
roots = sorted(filter(lambda node: node.is_root, nodes.values()), key=lambda node: node.pid) return sorted(filter(lambda node: node.is_root, nodes.values()), key=lambda node: node.pid)
return roots
@staticmethod @staticmethod
def freeze(roots): def freeze(roots):
@ -231,7 +229,9 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
self._snapshots = [] self._snapshots = []
self.snapshot_lock = threading.Lock() self.snapshot_lock = threading.Lock()
self._snapshot_daemon = threading.Thread( self._snapshot_daemon = threading.Thread(
name='treeview-snapshot-daemon', target=self._snapshot_target, daemon=True name='treeview-snapshot-daemon',
target=self._snapshot_target,
daemon=True,
) )
self._daemon_running = threading.Event() self._daemon_running = threading.Event()
@ -289,7 +289,7 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
cls.SNAPSHOT_INTERVAL = min(interval / 3.0, 1.0) cls.SNAPSHOT_INTERVAL = min(interval / 3.0, 1.0)
cls.take_snapshots = ttl_cache(ttl=interval)( cls.take_snapshots = ttl_cache(ttl=interval)(
cls.take_snapshots.__wrapped__ # pylint: disable=no-member cls.take_snapshots.__wrapped__, # pylint: disable=no-member
) )
@ttl_cache(ttl=2.0) @ttl_cache(ttl=2.0)
@ -354,7 +354,8 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
elif y >= self.y + self.height: elif y >= self.y + self.height:
self.scroll_offset += y - self.y - self.height + 1 self.scroll_offset += y - self.y - self.height + 1
self.scroll_offset = max( self.scroll_offset = max(
min(len(self.snapshots) - self.display_height, self.scroll_offset), 0 min(len(self.snapshots) - self.display_height, self.scroll_offset),
0,
) )
break break
else: else:
@ -367,14 +368,17 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
pid_width = max(3, max((len(str(process.pid)) for process in self.snapshots), default=3)) pid_width = max(3, max((len(str(process.pid)) for process in self.snapshots), default=3))
username_width = max( username_width = max(
4, max((len(process.username) for process in self.snapshots), default=4) 4,
max((len(process.username) for process in self.snapshots), default=4),
) )
device_width = max(6, max((len(process.devices) for process in self.snapshots), default=6)) device_width = max(6, max((len(process.devices) for process in self.snapshots), default=6))
num_threads_width = max( num_threads_width = max(
4, max((len(str(process.num_threads)) for process in self.snapshots), default=4) 4,
max((len(str(process.num_threads)) for process in self.snapshots), default=4),
) )
time_width = max( time_width = max(
4, max((len(process.running_time_human) for process in self.snapshots), default=4) 4,
max((len(process.running_time_human) for process in self.snapshots), default=4),
) )
header = ' '.join( header = ' '.join(
@ -387,12 +391,14 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
'%MEM', '%MEM',
'TIME'.rjust(time_width), 'TIME'.rjust(time_width),
'COMMAND', 'COMMAND',
] ],
) )
command_offset = len(header) - 7 command_offset = len(header) - 7
if self.x_offset < command_offset: if self.x_offset < command_offset:
self.addstr( self.addstr(
self.y, self.x, header[self.x_offset : self.x_offset + self.width].ljust(self.width) self.y,
self.x,
header[self.x_offset : self.x_offset + self.width].ljust(self.width),
) )
else: else:
self.addstr(self.y, self.x, 'COMMAND'.ljust(self.width)) self.addstr(self.y, self.x, 'COMMAND'.ljust(self.width))
@ -413,7 +419,9 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
self.selection.within_window = False self.selection.within_window = False
processes = islice( processes = islice(
self.snapshots, self.scroll_offset, self.scroll_offset + self.display_height self.snapshots,
self.scroll_offset,
self.scroll_offset + self.display_height,
) )
for y, process in enumerate(processes, start=self.y + 1): for y, process in enumerate(processes, start=self.y + 1):
prefix_length = len(process.prefix) prefix_length = len(process.prefix)
@ -485,7 +493,12 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
attr='bold | italic | reverse', attr='bold | italic | reverse',
) )
self.color_at( self.color_at(
self.y, text_offset + 10, width=3, fg='cyan', bg='red', attr='bold | reverse' self.y,
text_offset + 10,
width=3,
fg='cyan',
bg='red',
attr='bold | reverse',
) )
self.color_at( self.color_at(
self.y, self.y,
@ -496,7 +509,12 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
attr='bold | italic | reverse', attr='bold | italic | reverse',
) )
self.color_at( self.color_at(
self.y, text_offset + 17, width=4, fg='cyan', bg='red', attr='bold | reverse' self.y,
text_offset + 17,
width=4,
fg='cyan',
bg='red',
attr='bold | reverse',
) )
self.color_at( self.color_at(
self.y, self.y,
@ -507,7 +525,12 @@ class TreeViewScreen(Displayable): # pylint: disable=too-many-instance-attribut
attr='bold | italic | reverse', attr='bold | italic | reverse',
) )
self.color_at( self.color_at(
self.y, text_offset + 25, width=4, fg='cyan', bg='red', attr='bold | reverse' self.y,
text_offset + 25,
width=4,
fg='cyan',
bg='red',
attr='bold | reverse',
) )
def finalize(self): def finalize(self):

View file

@ -41,7 +41,12 @@ class UI(DisplayableContainer): # pylint: disable=too-many-instance-attributes
self.device_count = len(self.devices) self.device_count = len(self.devices)
self.main_screen = MainScreen( self.main_screen = MainScreen(
self.devices, filters, ascii=ascii, mode=mode, win=win, root=self self.devices,
filters,
ascii=ascii,
mode=mode,
win=win,
root=self,
) )
self.main_screen.visible = True self.main_screen.visible = True
self.main_screen.focused = False self.main_screen.focused = False
@ -193,8 +198,7 @@ class UI(DisplayableContainer): # pylint: disable=too-many-instance-attributes
self.main_screen.print() self.main_screen.print()
def handle_mouse(self): def handle_mouse(self):
"""Handles mouse input""" """Handle mouse input."""
try: try:
event = MouseEvent(curses.getmouse()) event = MouseEvent(curses.getmouse())
except curses.error: except curses.error:
@ -202,8 +206,7 @@ class UI(DisplayableContainer): # pylint: disable=too-many-instance-attributes
super().click(event) super().click(event)
def handle_key(self, key): def handle_key(self, key):
"""Handles key input""" """Handle key input."""
if key < 0: if key < 0:
self.keybuffer.clear() self.keybuffer.clear()
elif not super().press(key): elif not super().press(key):

View file

@ -245,7 +245,7 @@ def select_devices( # pylint: disable=too-many-branches,too-many-statements,too
(not math.isnan(device.gpu_utilization), device.gpu_utilization), # ascending (not math.isnan(device.gpu_utilization), device.gpu_utilization), # ascending
(not math.isnan(device.memory_utilization), device.memory_utilization), # ascending (not math.isnan(device.memory_utilization), device.memory_utilization), # ascending
-device.physical_index, # descending to keep <GPU 0> free -device.physical_index, # descending to keep <GPU 0> free
) ),
) )
if any(device.is_mig_device for device in available_devices): # found MIG devices! if any(device.is_mig_device for device in available_devices): # found MIG devices!

View file

@ -85,3 +85,79 @@ multi_line_output = 3
[tool.pydocstyle] [tool.pydocstyle]
convention = "google" convention = "google"
match-dir = '^(?!(gui|callbacks|docs))[^\.].*' match-dir = '^(?!(gui|callbacks|docs))[^\.].*'
[tool.ruff]
target-version = "py37"
line-length = 100
show-source = true
src = ["nvitop"]
select = [
"E", "W", # pycodestyle
"F", # pyflakes
"N", # pep8-naming
"UP", # pyupgrade
"ANN", # flake8-annotations
"S", # flake8-bandit
"BLE", # flake8-blind-except
"B", # flake8-bugbear
"COM", # flake8-commas
"C4", # flake8-comprehensions
"EXE", # flake8-executable
"ISC", # flake8-implicit-str-concat
"PIE", # flake8-pie
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RSE", # flake8-raise
"RET", # flake8-return
"SIM", # flake8-simplify
"TID", # flake8-tidy-imports
"RUF", # ruff
]
ignore = [
# E501: line too long
# W505: doc line too long
# too long docstring due to long example blocks
"E501",
"W505",
# ANN101: missing type annotation for `self` in method
# ANN102: missing type annotation for `cls` in classmethod
"ANN101",
"ANN102",
# ANN401: dynamically typed expressions (typing.Any) are disallowed
"ANN401",
# S101: use of `assert` detected
# internal use and may never raise at runtime
"S101",
# SIM105: use `contextlib.suppress(...)` instead of try-except-pass
# reduce unnessary function call
"SIM105",
]
[tool.ruff.per-file-ignores]
"__init__.py" = [
"F401", # unused-import
]
"setup.py" = [
"ANN", # flake8-annotations
]
"nvitop/api/lib*.py" = [
"N", # pep8-naming
"ANN", # flake8-annotations
]
"nvitop/callbacks/*.py" = [
"ANN", # flake8-annotations
]
"nvitop/gui/**/*.py" = [
"ANN", # flake8-annotations
]
[tool.ruff.flake8-annotations]
allow-star-arg-any = true
[tool.ruff.flake8-quotes]
docstring-quotes = "double"
multiline-quotes = "double"
inline-quotes = "single"
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "all"