feat(gui/host): show more metrics

This commit is contained in:
Xuehai Pan 2023-03-16 11:25:41 +00:00
parent 20313d08bd
commit e00582a8f2
8 changed files with 113 additions and 44 deletions

View file

@ -137,3 +137,4 @@ submodule
submodules
namespace
noqa
uptime

View file

@ -23,6 +23,7 @@ utilization (CPU, memory, disks, network, sensors) in Python.
from __future__ import annotations
import os as _os
import time as _time
from typing import Callable as _Callable
import psutil as _psutil
@ -32,6 +33,7 @@ from psutil import * # noqa: F403 # pylint: disable=wildcard-import,unused-wild
__all__ = [name for name in _psutil.__all__ if not name.startswith('_')] + [
'load_average',
'uptime',
'memory_percent',
'swap_percent',
'ppid_map',
@ -63,6 +65,11 @@ except AttributeError:
return
def uptime() -> float:
"""Get the system uptime."""
return _time.time() - _psutil.boot_time()
def memory_percent() -> float:
"""The percentage usage of virtual memory, calculated as ``(total - available) / total * 100``."""
return virtual_memory().percent

View file

@ -497,7 +497,12 @@ SIZE_PATTERN = re.compile(
"""The regex pattern for human readable size."""
def bytes2human(b: int | float | NaType) -> str: # pylint: disable=too-many-return-statements
# pylint: disable-next=too-many-return-statements
def bytes2human(
b: int | float | NaType,
*,
min_unit: int = 1,
) -> str:
"""Convert bytes to a human readable string."""
if b == NA:
return NA
@ -508,19 +513,19 @@ def bytes2human(b: int | float | NaType) -> str: # pylint: disable=too-many-ret
except ValueError:
return NA
if b < KiB:
if b < KiB and min_unit < KiB:
return f'{b}B'
if b < MiB:
if b < MiB and min_unit <= KiB:
return f'{round(b / KiB)}KiB'
if b <= 20 * GiB:
if b <= 20 * GiB and min_unit <= MiB:
return f'{round(b / MiB)}MiB'
if b < 100 * GiB:
if b < 100 * GiB and min_unit <= GiB:
return f'{round(b / GiB, 2):.2f}GiB'
if b < 1000 * GiB:
if b < 1000 * GiB and min_unit <= GiB:
return f'{round(b / GiB, 1):.1f}GiB'
if b < 100 * TiB:
if b < 100 * TiB and min_unit <= TiB:
return f'{round(b / TiB, 2):.2f}TiB'
if b < 1000 * TiB:
if b < 1000 * TiB and min_unit <= TiB:
return f'{round(b / TiB, 1):.1f}TiB'
if b < 100 * PiB:
return f'{round(b / PiB, 2):.2f}PiB'
@ -561,7 +566,11 @@ def human2bytes(s: int | str) -> int:
return int(float(size) * SIZE_UNITS[unit])
def timedelta2human(dt: int | float | datetime.timedelta | NaType) -> str:
def timedelta2human(
dt: int | float | datetime.timedelta | NaType,
*,
round: bool = False, # pylint: disable=redefined-builtin
) -> str:
"""Convert a number in seconds or a :class:`datetime.timedelta` instance to a human readable string."""
if isinstance(dt, (int, float)):
dt = datetime.timedelta(seconds=dt)
@ -569,7 +578,7 @@ def timedelta2human(dt: int | float | datetime.timedelta | NaType) -> str:
if not isinstance(dt, datetime.timedelta):
return NA
if dt.days >= 4:
if dt.days >= 4 or (round and dt.days >= 1):
return f'{dt.days + dt.seconds / 86400:.1f} days'
hours, seconds = divmod(86400 * dt.days + dt.seconds, 3600)

View file

@ -19,7 +19,15 @@ from nvitop.gui.library.keybinding import (
from nvitop.gui.library.libcurses import libcurses, setlocale_utf8
from nvitop.gui.library.messagebox import MessageBox, send_signal
from nvitop.gui.library.mouse import MouseEvent
from nvitop.gui.library.process import GpuProcess, HostProcess, Snapshot, bytes2human, host
from nvitop.gui.library.process import (
GiB,
GpuProcess,
HostProcess,
Snapshot,
bytes2human,
host,
timedelta2human,
)
from nvitop.gui.library.selection import Selection
from nvitop.gui.library.utils import (
HOSTNAME,

View file

@ -97,6 +97,7 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
)
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.last_retval = None
self.graph = []
self.last_graph = []
@ -281,7 +282,7 @@ class HistoryGraph: # pylint: disable=too-many-instance-attributes
def hook(self, func, get_value=None):
@functools.wraps(func)
def wrapped(*args, **kwargs):
retval = value = func(*args, **kwargs)
self.last_retval = retval = value = func(*args, **kwargs)
if get_value is not None:
value = get_value(retval)
self.add(value)

View file

@ -4,12 +4,21 @@
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
from nvitop.api import NA
from nvitop.api import NA, GiB
from nvitop.api import GpuProcess as GpuProcessBase
from nvitop.api import HostProcess, Snapshot, bytes2human, host, utilization2string
from nvitop.api import HostProcess, Snapshot, bytes2human, host, timedelta2human, utilization2string
__all__ = ['host', 'HostProcess', 'GpuProcess', 'NA', 'Snapshot', 'bytes2human']
__all__ = [
'host',
'HostProcess',
'GpuProcess',
'NA',
'Snapshot',
'bytes2human',
'GiB',
'timedelta2human',
]
class GpuProcess(GpuProcessBase):

View file

@ -33,7 +33,7 @@ def cut_string(s, maxlen, padstr='...', align='left'):
# pylint: disable=disallowed-name
def make_bar(prefix, percent, width):
def make_bar(prefix, percent, width, *, extra_text=''):
bar = f'{prefix}: '
if percent != NA and not (isinstance(percent, float) and not math.isfinite(percent)):
if isinstance(percent, str) and percent.endswith('%'):
@ -45,12 +45,15 @@ def make_bar(prefix, percent, width):
if remainder > 0:
bar += ' ▏▎▍▌▋▊▉'[remainder]
if isinstance(percent, float) and len(f'{bar} {percent:.1f}%') <= width:
bar += f' {percent:.1f}%'
text = f'{percent:.1f}%'
else:
bar += f' {min(round(percent), 100):d}%'.replace('100%', 'MAX')
text = f'{min(round(percent), 100):d}%'.replace('100%', 'MAX')
else:
bar += '' * (width - len(bar) - 4) + ' N/A'
return bar.ljust(width)
bar += '' * (width - len(bar) - 4)
text = 'N/A'
if extra_text and len(f'{bar} {text} {extra_text}') <= width:
return f'{bar} {text}'.ljust(width - len(extra_text) - 1) + f' {extra_text}'
return f'{bar} {text}'.ljust(width)
try:

View file

@ -11,9 +11,12 @@ from nvitop.gui.library import (
BufferedHistoryGraph,
Device,
Displayable,
GiB,
bytes2human,
colored,
host,
make_bar,
timedelta2human,
)
@ -40,8 +43,8 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.cpu_percent = None
self.load_average = None
self.memory_percent = None
self.swap_percent = None
self.virtual_memory = None
self.swap_memory = None
self._snapshot_daemon = threading.Thread(
name='host-snapshot-daemon',
target=self._snapshot_target,
@ -91,7 +94,7 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
dynamic_bound=False,
format='CPU: {:.1f}%'.format,
)(host.cpu_percent)
host.memory_percent = BufferedHistoryGraph(
host.virtual_memory = BufferedHistoryGraph(
interval=1.0,
width=77,
height=4,
@ -99,9 +102,9 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
baseline=0.0,
upperbound=100.0,
dynamic_bound=False,
format='MEM: {:.1f}%'.format,
)(host.memory_percent)
host.swap_percent = BufferedHistoryGraph(
format='{:.1f}%'.format,
)(host.virtual_memory, get_value=lambda vm: vm.percent)
host.swap_memory = BufferedHistoryGraph(
interval=1.0,
width=77,
height=1,
@ -109,8 +112,8 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
baseline=0.0,
upperbound=100.0,
dynamic_bound=False,
format='SWP: {:.1f}%'.format,
)(host.swap_percent)
format='{:.1f}%'.format,
)(host.swap_memory, get_value=lambda sm: sm.percent)
def percentage(x):
return f'{x:.1f}%' if x is not NA else NA
@ -164,13 +167,13 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
def take_snapshots(self):
host.cpu_percent()
host.memory_percent()
host.swap_percent()
host.virtual_memory()
host.swap_memory()
self.load_average = host.load_average()
self.cpu_percent = host.cpu_percent.history.last_value
self.memory_percent = host.memory_percent.history.last_value
self.swap_percent = host.swap_percent.history.last_value
self.virtual_memory = host.virtual_memory.history.last_retval
self.swap_memory = host.swap_memory.history.last_retval
total_memory_used = 0
total_memory_total = 0
@ -255,9 +258,23 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
if self.compact:
width_right = len(load_average) + 4
width_left = self.width - 2 - width_right
cpu_bar = '[ {} ]'.format(make_bar('CPU', self.cpu_percent, width_left - 4))
memory_bar = '[ {} ]'.format(make_bar('MEM', self.memory_percent, width_left - 4))
swap_bar = '[ {} ]'.format(make_bar('SWP', self.swap_percent, width_right - 4))
cpu_bar = '[ {} ]'.format(
make_bar(
'CPU',
self.cpu_percent,
width_left - 4,
extra_text=f' UPTIME: {timedelta2human(host.uptime(), round=True)}',
),
)
memory_bar = '[ {} ]'.format(
make_bar(
'MEM',
self.virtual_memory.percent,
width_left - 4,
extra_text=f' USED: {bytes2human(self.virtual_memory.used, min_unit=GiB)}',
),
)
swap_bar = '[ {} ]'.format(make_bar('SWP', self.swap_memory.percent, width_right - 4))
self.addstr(self.y, self.x, f'{cpu_bar} ( {load_average} )')
self.addstr(self.y + 1, self.x, f'{memory_bar} {swap_bar}')
self.color_at(self.y, self.x, width=len(cpu_bar), fg='cyan', attr='bold')
@ -305,11 +322,11 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.addstr(y, self.x + 1, line)
self.color(fg='magenta')
for y, line in enumerate(host.memory_percent.history.graph, start=self.y + 6):
for y, line in enumerate(host.virtual_memory.history.graph, start=self.y + 6):
self.addstr(y, self.x + 1, line)
self.color(fg='blue')
for y, line in enumerate(host.swap_percent.history.graph, start=self.y + 10):
for y, line in enumerate(host.swap_memory.history.graph, start=self.y + 10):
self.addstr(y, self.x + 1, line)
if self.width >= 100:
@ -342,12 +359,12 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
self.addstr(
self.y + 9,
self.x + 1,
f' {host.memory_percent.history} ',
f' MEM: {bytes2human(self.virtual_memory.used, min_unit=GiB)} ({host.virtual_memory.history}) ',
)
self.addstr(
self.y + 10,
self.x + 1,
f' {host.swap_percent.history} ',
f' SWP: {bytes2human(self.swap_memory.used, min_unit=GiB)} ({host.swap_memory.history}) ',
)
if self.width >= 100:
self.addstr(self.y, self.x + 79, f' {gpu_memory_percent} ')
@ -364,8 +381,8 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
def print(self):
self.cpu_percent = host.cpu_percent()
self.memory_percent = host.memory_percent()
self.swap_percent = host.swap_percent()
self.virtual_memory = host.virtual_memory()
self.swap_memory = host.swap_memory()
self.load_average = host.load_average()
if self.load_average is not None:
@ -378,9 +395,23 @@ class HostPanel(Displayable): # pylint: disable=too-many-instance-attributes
width_right = len(load_average) + 4
width_left = self.width - 2 - width_right
cpu_bar = '[ {} ]'.format(make_bar('CPU', self.cpu_percent, width_left - 4))
memory_bar = '[ {} ]'.format(make_bar('MEM', self.memory_percent, width_left - 4))
swap_bar = '[ {} ]'.format(make_bar('SWP', self.swap_percent, width_right - 4))
cpu_bar = '[ {} ]'.format(
make_bar(
'CPU',
self.cpu_percent,
width_left - 4,
extra_text=f' UPTIME: {timedelta2human(host.uptime(), round=True)}',
),
)
memory_bar = '[ {} ]'.format(
make_bar(
'MEM',
self.virtual_memory.percent,
width_left - 4,
extra_text=f' USED: {bytes2human(self.virtual_memory.used, min_unit=GiB)}',
),
)
swap_bar = '[ {} ]'.format(make_bar('SWP', self.swap_memory.percent, width_right - 4))
lines = [
'{} {}'.format(