Initial work on adding NVidia GPU monitor

Adds support for nvidia cars via py3nvml.
Design is completly broken TODO.

Signed-off-by: Lucas Zampieri <lzampier@redhat.com>
gpu
Lucas Zampieri 2021-08-27 17:29:22 -03:00
parent 776927e79e
commit 2b852042f2
1 changed files with 196 additions and 120 deletions

316
bpytop.py
View File

@ -19,6 +19,7 @@
import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse
import urllib.request import urllib.request
import py3nvml.py3nvml as nvml
from time import time, sleep, strftime, tzset from time import time, sleep, strftime, tzset
from datetime import timedelta from datetime import timedelta
from _thread import interrupt_main from _thread import interrupt_main
@ -2458,7 +2459,7 @@ class ProcBox(Box):
else: else:
width_p = cls.width_p width_p = cls.width_p
if not "cpu" in cls.boxes: if not "cpu" in cls.boxes or "gpu" in cls.boxes:
height_p = 100 height_p = 100
else: else:
height_p = cls.height_p height_p = cls.height_p
@ -2878,6 +2879,7 @@ class ProcBox(Box):
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
cls.redraw = cls.resized = cls.moved = False cls.redraw = cls.resized = cls.moved = False
class GpuBox(Box): class GpuBox(Box):
name = "gpu" name = "gpu"
height_p = 30 height_p = 30
@ -2894,15 +2896,20 @@ class GpuBox(Box):
@classmethod @classmethod
def _calc_size(cls): def _calc_size(cls):
width_p: int # if not "gpu" in cls.boxes:
if cls.stat_mode: # width_p = 0
width_p = 100 # Box._b_cpu_h = 0
else: # cls.width = Term.width
width_p = cls.width_p # return
# width_p: int
# if cls.stat_mode:
# width_p = 30
# else:
width_p = cls.width_p
cls.width = round(Term.width * width_p / 100) cls.width = round(Term.width * width_p / 100)
cls.height = Term.height - Box._b_proc_h - Box._b_cpu_h cls.height = Term.height - Box._b_cpu_h - Box._b_cpu_h - 4
cls.y = Box._b_cpu_h + 1 cls.y = Box._b_cpu_h + 24
cls.x = Term.width - cls.width + 1 cls.x = Term.width - cls.width + 1
cls.box_width = 27 if cls.width > 45 else 19 cls.box_width = 27 if cls.width > 45 else 19
cls.box_height = 9 if cls.height > 10 else cls.height - 2 cls.box_height = 9 if cls.height > 10 else cls.height - 2
@ -2912,33 +2919,34 @@ class GpuBox(Box):
@classmethod @classmethod
def _draw_bg(cls) -> str: def _draw_bg(cls) -> str:
if cls.proc_mode: return ""
return f'{create_box(box=cls, line_color=THEME.gpu_box)}' return f'{create_box(box=cls, line_color=THEME.gpu_box)}'
@classmethod @classmethod
def _draw_fg(cls): def _draw_fg(cls):
if cls.proc_mode: return
gpu = GpuCollector gpu = GpuCollector
if gpu.redraw: cls.redraw = True
if not gpu.gpu: return if gpu.redraw:
card, name = gpu.gpu[0], gpu.gpu[1][:20] cls.redraw = True
stat = gpu.stats[card] if not gpu.gpu:
stat_nums = gpu.stat_nums[card] return
card, name = gpu.gpu[0], gpu.name
stat = gpu.stats[gpu.uuid]
stat_nums = gpu.stat_nums[gpu.uuid]
out: str = "" out: str = ""
out_misc: str = "" out_misc: str = ""
x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2 x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2
reset: bool = bool(True) reset: bool = bool(True)
if cls.resized or cls.redraw: if cls.resized or cls.redraw:
Meters.gpu = {k:{} for k in cls.gpu_keys} Meters.gpu = {k: {} for k in cls.gpu_keys}
out_misc += cls._draw_bg() out_misc += cls._draw_bg()
out_misc += (f'{Mv.to(y-1, x+w - 23)}{THEME.gpu_box}{Symbol.h_line * (18 - len(name))}' out_misc += (f'{Mv.to(y-1, x+w - 30)}{THEME.gpu_box}{Symbol.h_line * (18 - len(name))}'
f'{Symbol.title_left}{Fx.b}{THEME.title(name)}{Fx.ub}{THEME.gpu_box(Symbol.title_right)}{Term.fg}') f'{Symbol.title_left}{Fx.b}{THEME.title(name)}{Fx.ub}{THEME.gpu_box(Symbol.title_right)}{Term.fg}')
Meters.gpu["load"][card+"gpu"] = Meter(int(stat["load"]["gpu"]), (w // 2) - 4, "cpu") Meters.gpu["load"]["gpu"] = Meter(int(stat["load"]["gpu"]), (w // 2) - 4, "cpu")
Meters.gpu["load"][card+"mem"] = Meter(int(stat["load"]["mem"]), (w // 2) - 4, "cpu") Meters.gpu["load"]["mem"] = Meter(int(stat["load"]["mem"]), (w // 2) - 4, "cpu")
Meters.gpu["vitals"][card+"vram"] = Meter(int(stat["vitals"]["vram"]), (w // 2) - 4, "cpu") # Meters.gpu["vitals"]["vram"] = Meter(int(stat["vitals"]["vram"]), (w // 2) - 4, "cpu")
Draw.buffer("gpu_misc", out_misc, only_save=True) Draw.buffer("gpu_misc", out_misc, only_save=True)
@ -2947,42 +2955,43 @@ class GpuBox(Box):
fixedEnding = lambda s, n : f'{s}{(n-(int(log10(s))+1)) * " "}{Mv.r(n-(int(log10(s))+1))}' fixedEnding = lambda s, n : f'{s}{(n-(int(log10(s))+1)) * " "}{Mv.r(n-(int(log10(s))+1))}'
clock = lambda s: fixedEnding(s, 4) + 'Mhz' clock = lambda s: fixedEnding(s, 4) + 'Mhz'
voltage = lambda s: fixedEnding(s, 4) + 'mV' # voltage = lambda s: fixedEnding(s, 4) + 'mV'
watts = lambda s: f'{s}{(6-(len(str(s))+1)) * " "}{Mv.r(6-(len(str(s))+1))}W' watts = lambda s: f'{s}{(6-(len(str(s))+1)) * " "}{Mv.r(6-(len(str(s))+1))}W'
# load percent # load percent
out += f'{Mv.to(y, Term.width - round(w / 2) - 1)}{THEME.graph_text("GPU ")}{Meters.gpu["load"][card+"gpu"](None if cls.resized else stat["load"]["gpu"])}' out += f'{Mv.to(y, Term.width - round(w / 2) - 1)}{THEME.graph_text("GPU ")}{Meters.gpu["load"]["gpu"](None if cls.resized else stat["load"]["gpu"])}'
out += f'{Mv.to(y+1, Term.width - round(w / 2) - 1)}{THEME.graph_text("Mem ")}{Meters.gpu["load"][card+"mem"](None if cls.resized else stat["load"]["mem"])}' out += f'{Mv.to(y+1, Term.width - round(w / 2) - 1)}{THEME.graph_text("Mem ")}{Meters.gpu["load"]["mem"](None if cls.resized else stat["load"]["mem"])}'
# vram # vram
out += f'{Mv.to(y+2, Term.width - round(w / 2) - 2)}{THEME.graph_text("VRAM ")}{Meters.gpu["vitals"][card+"vram"](None if cls.resized else stat["vitals"]["vram"])}' if "AMD" == True:
out += f'{Mv.to(y+2, Term.width - round(w / 2) - 2)}{THEME.graph_text("VRAM ")}{Meters.gpu["vitals"]["vram"](None if cls.resized else stat["vitals"]["vram"])}'
# clocks # clocks
for f_i in range(len(stat_nums["freqs"])): for f_i in range(len(stat_nums["freqs"])):
(f, name) = stat_nums["freqs"][f_i] (f, name) = stat_nums["freqs"][f_i]
out += f'{Mv.to(y+f_i, x)}{THEME.graph_text(name+": ")}{clock(stat["freqs"][f"freq{f}"][0])}' out += f'{Mv.to(y+f_i, x)}{THEME.graph_text(name+": ")}{clock(stat["freqs"][f"freq{f_i}"][0])}'
# voltage # voltage
out += f'{Mv.to(y+len(stat_nums["freqs"])-1, x)}' if "AMD" == True:
for f_i in range(len(stat_nums["volts"])): out += f'{Mv.to(y+len(stat_nums["freqs"])-1, x)}'
(n, name) = stat_nums["volts"][f_i] for f_i in range(len(stat_nums["volts"])):
out += f'{Mv.d(1)}{THEME.graph_text(name+": ")}{voltage(stat["volts"][f"volt{n}"][0])}' (n, name) = stat_nums["volts"][f_i]
out += f'{Mv.d(1)}{THEME.graph_text(name+": ")}{voltage(stat["volts"][f"volt{n}"][0])}'
# power # power
out += f'{Mv.to(y+len(stat_nums["freqs"])+len(stat_nums["volts"])-1, x)}' out += f'{Mv.to(y+len(stat_nums["freqs"])+len(stat_nums["volts"])-2, x)}'
for f_i in range(len(stat_nums["power"])): for f_i in range(len(stat_nums["power"])):
n = stat_nums["power"][f_i] n = stat_nums["power"][f_i]
out += f'{Mv.d(1)}{THEME.graph_text("Draw: ")}{watts(stat["power"][f"power{n}"])}' out += f'{Mv.d(1)}{THEME.graph_text("Draw: ")}{stat["power"][f"power{n}"]} W'
# temps # temps
out += f'{Mv.to(y+3, Term.width - 5)}{stat["vitals"]["temp1"][0]}°C' out += f'{Mv.to(y+2, Term.width - 5)}{stat["vitals"]["temp1"]}°C'
# fans # fans
out += f'{Mv.l(4)}'#len of temps out += f'{Mv.l(4)}'#len of temps
for f_i in range(len(stat_nums["fans"])): for f_i in range(len(stat_nums["fans"])):
(n, rpm) = stat_nums["freqs"][f_i] rpm = stat_nums["fans"][f_i]
out += f'{Mv.l(8 * (f_i + 1))}{stat["fans"][f"fan{n}"][0]}RPM' out += f'{Mv.l(12 * (f_i + 1))}Fans: {rpm}% '
Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active)
cls.redraw = cls.resized = False cls.redraw = cls.resized = False
@ -4084,25 +4093,33 @@ class ProcCollector(Collector):
def _draw(cls): def _draw(cls):
ProcBox._draw_fg() ProcBox._draw_fg()
class GpuCollector(Collector): class GpuCollector(Collector):
'''Collects GPU stats''' '''Collects GPU stats'''
buffer: str = GpuBox.buffer buffer: str = GpuBox.buffer
dir: str = "/sys/class/drm/"
hwmon: str = "/device/hwmon/hwmon0/"
pci_id_locations = ["/usr/share/hwdata/", "/usr/share/misc/"]
gpus: List[Tuple[str, str]] = [] gpus: List[Tuple[str, str]] = []
gpu_i: int = 0 gpu_i: int = 0
gpu: str = "" gpu: str = ""
name: str = ""
gpu_error: bool = False gpu_error: bool = False
reset: bool = False reset: bool = False
#* stats structure = stats[cardN] if "AMD" == True:
#[fans, freqs, vitals, power, load] dir: str = "/sys/class/drm/"
#[fanN_rpm, fanN_max, freqN, tempN, vram_used, vram_total, vddgfx, avg_draw, gpu_load_p, mem_load_p] hwmon: str = "/device/hwmon/hwmon0/"
pci_id_locations = ["/usr/share/hwdata/", "/usr/share/misc/"]
else:
nvml.nvmlInit()
handle = nvml.nvmlDeviceGetHandleByIndex(0)
uuid = nvml.nvmlDeviceGetUUID(handle)
# * stats structure = stats[cardN]
# [fans, freqs, vitals, power, load]
# [fanN_rpm, fanN_max, freqN, tempN, vram_used, vram_total, vddgfx, avg_draw, gpu_load_p, mem_load_p]
stats: Dict[str, Dict[str, Dict[str, Any]]] = {} stats: Dict[str, Dict[str, Dict[str, Any]]] = {}
#* stat_nums structure = stats[cardN][fans, freqs, temps, power, volts][n, ..., m] # * stat_nums structure = stats[cardN][fans, freqs, temps, power, volts][n, ..., m]
stat_nums: Dict[str, Dict[str, List[Tuple[str, str]]]] = {} stat_nums: Dict[str, Dict[str, List[Tuple[str, str]]]] = {}
populated: bool = False populated: bool = False
@ -4160,115 +4177,177 @@ class GpuCollector(Collector):
def _get_stat_nums(cls, card): def _get_stat_nums(cls, card):
stat_keys = ["fans", "freqs", "temps", "power", "volts"] stat_keys = ["fans", "freqs", "temps", "power", "volts"]
sys_keys = ["fan", "freq", "temp", "power", "in"] sys_keys = ["fan", "freq", "temp", "power", "in"]
cls.stat_nums[card] = { cls.stat_nums[cls.uuid] = {
"fans": [], "fans": [],
"freqs": [], "freqs": [],
"temps": [], "temps": [],
"power": [], "power": [],
"volts": [], "volts": [],
} }
get_num = lambda s, f: re.match(f"{s}\d", f)
with os.scandir(cls._get_hwmon(card)) as files: if "AMD" == True:
for file in files: get_num = lambda s, f: re.match(f"{s}\d", f)
matches = [get_num(k, file.name) for k in sys_keys] with os.scandir(cls._get_hwmon(card)) as files:
for i in range (0,5): for file in files:
if matches[i]: matches = [get_num(k, file.name) for k in sys_keys]
num = file.name[matches[i].end() - 1] for i in range (0,5):
arr = cls.stat_nums[card][stat_keys[i]] if matches[i]:
if len(arr) == 0 or num not in arr: num = file.name[matches[i].end() - 1]
arr.append(num) arr = cls.stat_nums[card][stat_keys[i]]
break if len(arr) == 0 or num not in arr:
# label the properties which have labels arr.append(num)
for key_i in range(len(cls.stat_nums[card])): break
key = cls.stat_nums[card][stat_keys[key_i]] # label the properties which have labels
for j in range(len(key)): for key_i in range(len(cls.stat_nums[card])):
num = key[j] key = cls.stat_nums[card][stat_keys[key_i]]
try: for j in range(len(key)):
label = cls._read(cls._get_hwmon(card) + sys_keys[key_i] + num + "_label") num = key[j]
key[j] = (key[j], label) try:
except: pass label = cls._read(cls._get_hwmon(card) + sys_keys[key_i] + num + "_label")
# prepopulate fans' max RPM key[j] = (key[j], label)
for f in range(len(cls.stat_nums[card]["fans"])): except: pass
fan = cls.stat_nums[card]["fans"][f] # prepopulate fans' max RPM
f_max = cls._read(cls._get_hwmon(card) + f"fan{fan}_max") for f in range(len(cls.stat_nums[card]["fans"])):
cls.stat_nums[card]["fans"][f] = (fan, f_max) fan = cls.stat_nums[card]["fans"][f]
f_max = cls._read(cls._get_hwmon(card) + f"fan{fan}_max")
cls.stat_nums[card]["fans"][f] = (fan, f_max)
else:
cls.stat_nums[cls.uuid]["fans"] = [nvml.nvmlDeviceGetFanSpeed(card)]
cls.stat_nums[cls.uuid]["freqs"] = [(str(nvml.nvmlDeviceGetClockInfo(card, nvml.NVML_CLOCK_GRAPHICS)), "GPU"),
(str(nvml.nvmlDeviceGetClockInfo(card, nvml.NVML_CLOCK_MEM)), "MEM")]
cls.stat_nums[cls.uuid]["temps"] = [nvml.nvmlDeviceGetTemperature(card, nvml.NVML_TEMPERATURE_GPU)]
cls.stat_nums[cls.uuid]["volts"] = [nvml.nvmlDeviceGetPowerUsage(card)]
cls.stat_nums[cls.uuid]["power"] = [nvml.nvmlDeviceGetPowerUsage(card)]
@classmethod @classmethod
def _get_gpus(cls): def _get_gpus(cls):
'''Get a list of all GPUs with names''' '''Get a list of all GPUs with names'''
cls.gpu_i = 0 if "AMD" == True:
cls.gpu = "" cls.gpu_i = 0
id, keys = ["", "", "", ""], ["vendor", "device", "subsystem_vendor", "subsystem_device"] cls.gpu = ""
with os.scandir(cls.dir) as cards: id, keys = ["", "", "", ""], ["vendor", "device", "subsystem_vendor", "subsystem_device"]
for card in cards: with os.scandir(cls.dir) as cards:
if re.match('card\d$', card.name): for card in cards:
for i in range(len(keys)): if re.match('card\d$', card.name):
id[i] = cls._read(cls._get_device(card.name) + keys[i]).replace("0x", "") for i in range(len(keys)):
id[2] = f"{id[2]} {id[3]}" id[i] = cls._read(cls._get_device(card.name) + keys[i]).replace("0x", "")
cls.gpus.append((card.name, tuple(id[:3]))) id[2] = f"{id[2]} {id[3]}"
cls.gpus.append((card.name, tuple(id[:3])))
cls.gpus = cls._get_gpu_names(cls.gpus); cls.gpus = cls._get_gpu_names(cls.gpus)
for gpu in cls.gpus: for gpu in cls.gpus:
cls._get_stat_nums(gpu[0]) cls._get_stat_nums(gpu[0])
if not cls.gpus: cls.gpus = [""] if not cls.gpus: cls.gpus = [""]
cls.gpu = cls.gpus[cls.gpu_i] cls.gpu = cls.gpus[cls.gpu_i]
cls.populated = True cls.populated = True
else:
if nvml.nvmlDeviceGetCount() == 1:
cls.gpu = [cls.handle]
cls.name = nvml.nvmlDeviceGetName(cls.handle)
cls._get_stat_nums(cls.gpu[0])
else:
print("multi gpu not yet supported")
@classmethod @classmethod
def _get_vitals(cls, card): def _get_vitals(cls, card):
temp = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"temp{n}_input")) / 1000)) #°C stat = {}
stat = { if "AMD" == True:
"vram_used": int(cls._read(cls._get_device(card) + "mem_info_vram_used")), #kB temp = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"temp{n}_input")) / 1000)) #°C
"vram_total": int(cls._read(cls._get_device(card) + "mem_info_vram_total")), #kB stat = {
} "vram_used": int(cls._read(cls._get_device(card) + "mem_info_vram_used")), #kB
stat["vram"] = (stat["vram_used"] / stat["vram_total"]) * 100 #percent "vram_total": int(cls._read(cls._get_device(card) + "mem_info_vram_total")), #kB
for (i, name) in cls.stat_nums[card]["temps"]: }
stat[f"temp{i}"] = (temp(i), name) stat["vram"] = (stat["vram_used"] / stat["vram_total"]) * 100 #percent
for (i, name) in cls.stat_nums[card]["temps"]:
stat[f"temp{i}"] = (temp(i), name)
else:
temp = nvml.nvmlDeviceGetTemperature(card, nvml.NVML_TEMPERATURE_GPU)
stat[f"temp1"] = temp
return stat return stat
@classmethod @classmethod
def _get_power(cls, card): def _get_power(cls, card):
power = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"power{n}_average")) / 1000000, 2)) # Watts
volt = lambda n: cls._read(cls._get_hwmon(card) + f"in{n}_input") # mV
stat = {} stat = {}
for i in cls.stat_nums[card]["power"]: if "amd" == True:
stat[f"power{i}"] = power(i) power = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"power{n}_average")) / 1000000, 2)) # Watts
volt = lambda n: cls._read(cls._get_hwmon(card) + f"in{n}_input") # mV
else:
power = str(round(int(nvml.nvmlDeviceGetPowerUsage(cls.handle) / 1000)))
for i in cls.stat_nums[cls.uuid]["power"]:
stat[f"power{i}"] = power
return stat return stat
@classmethod @classmethod
def _get_volts(cls, card): def _get_volts(cls, card):
volt = lambda n: int(cls._read(cls._get_hwmon(card) + f"in{n}_input")) # mV if "amd" == True:
volt = lambda n: int(cls._read(cls._get_hwmon(card) + f"in{n}_input")) # mV
else:
# nvidia only allow voltage data to be returned for their S-class products, see nvmlReturn_t nvmlUnitGetPsuInfo()
return ""
stat = {} stat = {}
for (i, name) in cls.stat_nums[card]["volts"]: for (i, name) in cls.stat_nums[card]["volts"]:
stat[f"volt{i}"] = (volt(i), name) stat[f"volt{i}"] = (volt(i), name)
return stat return stat
@classmethod @classmethod
def _get_freqs(cls, card): def _get_freqs(cls, card):
freq = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"freq{n}_input")) / 1000000)) # MHz
freq = lambda n: round(int(cls._read(cls._get_hwmon(card) + f"freq{n}_input")) / 1000000) # MHz
stat = {} stat = {}
for (i, name) in cls.stat_nums[card]["freqs"]:
stat[f"freq{i}"] = (freq(i), name) if "AMD" == True:
freq = lambda n: str(round(int(cls._read(cls._get_hwmon(card) + f"freq{n}_input")) / 1000000)) # MHz
for (i, name) in cls.stat_nums[card]["freqs"]:
stat[f"freq{i}"] = (freq(i), name)
else:
stat["freq0"] = (nvml.nvmlDeviceGetClockInfo(card, nvml.NVML_CLOCK_GRAPHICS), "GPU")
stat["freq1"] = (nvml.nvmlDeviceGetClockInfo(card, nvml.NVML_CLOCK_MEM), "MEM")
return stat return stat
@classmethod @classmethod
def _get_fans(cls, card): def _get_fans(cls, card):
fan = lambda n: int(cls._read(cls._get_hwmon(card) + f"fan{n}_input")) # RPM
stat = {} stat = {}
for (i, f_max) in cls.stat_nums[card]["fans"]:
stat[f"fan{i}"] = (fan(i), f_max) if "amd" == True:
fan = lambda n: int(cls._read(cls._get_hwmon(card) + f"fan{n}_input")) # RPM
for (i, f_max) in cls.stat_nums[card]["fans"]:
stat[f"fan{i}"] = (fan(i), f_max)
else:
fan = nvml.nvmlDeviceGetFanSpeed(card)
stat[f"fans"] = fan
return stat
@classmethod
def _get_load(cls, card):
stat = {}
if "AMD" == True:
stat = {
"mem": int(cls._read(cls._get_device(card) + "mem_busy_percent")),
"gpu": int(cls._read(cls._get_device(card) + "gpu_busy_percent")),
}
else:
gpu_stats = nvml.nvmlDeviceGetUtilizationRates(card)
stat["mem"] = int(gpu_stats.gpu)
stat["gpu"] = int(gpu_stats.memory)
return stat return stat
@classmethod @classmethod
def _collect(cls): def _collect(cls):
if not cls.populated: cls._get_gpus() if not cls.populated:
if not cls.gpu: return cls._get_gpus()
if not cls.gpu:
return
stat: Dict[str, Dict[str, Any]] = {} stat: Dict[str, Dict[str, Any]] = {}
card = cls.gpu[0] card = cls.gpu[0]
@ -4277,19 +4356,16 @@ class GpuCollector(Collector):
stat["power"] = cls._get_power(card) stat["power"] = cls._get_power(card)
stat["volts"] = cls._get_volts(card) stat["volts"] = cls._get_volts(card)
stat["vitals"] = cls._get_vitals(card) stat["vitals"] = cls._get_vitals(card)
stat["load"] = { stat["load"] = cls._get_load(card)
"mem": int(cls._read(cls._get_device(card) + "mem_busy_percent")),
"gpu": int(cls._read(cls._get_device(card) + "gpu_busy_percent")),
}
cls.stats[card] = stat cls.stats[cls.uuid] = stat
cls.timestamp = time() cls.timestamp = time()
@classmethod @classmethod
def _draw(cls): def _draw(cls):
GpuBox._draw_fg() GpuBox._draw_fg()
class Menu: class Menu:
'''Holds all menus''' '''Holds all menus'''
active: bool = False active: bool = False
@ -5676,7 +5752,7 @@ def temperature(value: int, scale: str = "celsius") -> Tuple[int, str]:
def process_keys(): def process_keys():
mouse_pos: Tuple[int, int] = (0, 0) mouse_pos: Tuple[int, int] = (0, 0)
filtered: bool = False filtered: bool = False
box_keys = {"1" : "cpu", "2" : "mem", "3" : "net", "4" : "proc"} box_keys = {"1": "cpu", "2": "mem", "3": "net", "4": "proc", "5": "gpu"}
while Key.has_key(): while Key.has_key():
key = Key.get() key = Key.get()
found: bool = True found: bool = True