mirror of
https://github.com/aristocratos/btop.git
synced 2024-05-15 18:03:06 +12:00
Merge pull request #529 from romner-set/main
Add GPU monitoring support
This commit is contained in:
commit
5b01235315
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -51,6 +51,9 @@ bin
|
|||
btop
|
||||
.*/
|
||||
|
||||
# Optional libraries
|
||||
lib/rocm_smi_lib
|
||||
|
||||
# Don't ignore .github directory
|
||||
!.github/
|
||||
|
||||
|
|
84
Makefile
84
Makefile
|
@ -12,10 +12,7 @@ else
|
|||
endif
|
||||
|
||||
ifneq ($(QUIET),true)
|
||||
override PRE := info info-quiet
|
||||
override QUIET := false
|
||||
else
|
||||
override PRE := info-quiet
|
||||
endif
|
||||
|
||||
OLDCXX := $(CXXFLAGS)
|
||||
|
@ -39,6 +36,20 @@ endif
|
|||
|
||||
override PLATFORM_LC := $(shell echo $(PLATFORM) | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
#? GPU Support
|
||||
ifeq ($(PLATFORM_LC)$(ARCH),linuxx86_64)
|
||||
ifneq ($(STATIC),true)
|
||||
GPU_SUPPORT := true
|
||||
endif
|
||||
endif
|
||||
ifneq ($(GPU_SUPPORT),true)
|
||||
GPU_SUPPORT := false
|
||||
endif
|
||||
|
||||
ifeq ($(GPU_SUPPORT),true)
|
||||
override ADDFLAGS += -DGPU_SUPPORT
|
||||
endif
|
||||
|
||||
#? Compiler and Linker
|
||||
ifeq ($(shell $(CXX) --version | grep clang >/dev/null 2>&1; echo $$?),0)
|
||||
override CXX_IS_CLANG := true
|
||||
|
@ -206,24 +217,37 @@ endif
|
|||
|
||||
P := %%
|
||||
|
||||
#? Default Make
|
||||
all: $(PRE) directories btop
|
||||
ifeq ($(VERBOSE),true)
|
||||
override SUPPRESS := 1>/dev/null
|
||||
else
|
||||
override SUPPRESS :=
|
||||
endif
|
||||
|
||||
#? Default Make
|
||||
.ONESHELL:
|
||||
all: | info rocm_smi info-quiet directories btop
|
||||
|
||||
ifneq ($(QUIET),true)
|
||||
info:
|
||||
@printf " $(BANNER)\n"
|
||||
@printf "\033[1;92mPLATFORM \033[1;93m?| \033[0m$(PLATFORM)\n"
|
||||
@printf "\033[1;96mARCH \033[1;93m?| \033[0m$(ARCH)\n"
|
||||
@printf "\033[1;93mCXX \033[1;93m?| \033[0m$(CXX) \033[1;93m(\033[97m$(CXX_VERSION)\033[93m)\n"
|
||||
@printf "\033[1;94mTHREADS \033[1;94m:| \033[0m$(THREADS)\n"
|
||||
@printf "\033[1;92mREQFLAGS \033[1;91m!| \033[0m$(REQFLAGS)\n"
|
||||
@printf "\033[1;91mWARNFLAGS \033[1;94m:| \033[0m$(WARNFLAGS)\n"
|
||||
@printf "\033[1;94mOPTFLAGS \033[1;94m:| \033[0m$(OPTFLAGS)\n"
|
||||
@printf "\033[1;93mLDCXXFLAGS \033[1;94m:| \033[0m$(LDCXXFLAGS)\n"
|
||||
@printf "\033[1;95mCXXFLAGS \033[1;92m+| \033[0;37m\$$(\033[92mREQFLAGS\033[37m) \$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m) $(OLDCXX)\n"
|
||||
@printf "\033[1;95mLDFLAGS \033[1;92m+| \033[0;37m\$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m) $(OLDLD)\n"
|
||||
@printf "\033[1;92mPLATFORM \033[1;93m?| \033[0m$(PLATFORM)\n"
|
||||
@printf "\033[1;96mARCH \033[1;93m?| \033[0m$(ARCH)\n"
|
||||
@printf "\033[1;95mGPU_SUPPORT \033[1;94m:| \033[0m$(GPU_SUPPORT)\n"
|
||||
@printf "\033[1;93mCXX \033[1;93m?| \033[0m$(CXX) \033[1;93m(\033[97m$(CXX_VERSION)\033[93m)\n"
|
||||
@printf "\033[1;94mTHREADS \033[1;94m:| \033[0m$(THREADS)\n"
|
||||
@printf "\033[1;92mREQFLAGS \033[1;91m!| \033[0m$(REQFLAGS)\n"
|
||||
@printf "\033[1;91mWARNFLAGS \033[1;94m:| \033[0m$(WARNFLAGS)\n"
|
||||
@printf "\033[1;94mOPTFLAGS \033[1;94m:| \033[0m$(OPTFLAGS)\n"
|
||||
@printf "\033[1;93mLDCXXFLAGS \033[1;94m:| \033[0m$(LDCXXFLAGS)\n"
|
||||
@printf "\033[1;95mCXXFLAGS \033[1;92m+| \033[0;37m\$$(\033[92mREQFLAGS\033[37m) \$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m) $(OLDCXX)\n"
|
||||
@printf "\033[1;95mLDFLAGS \033[1;92m+| \033[0;37m\$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m) $(OLDLD)\n"
|
||||
else
|
||||
info:
|
||||
@true
|
||||
endif
|
||||
|
||||
info-quiet:
|
||||
@sleep 0.1 2>/dev/null || true
|
||||
|
||||
info-quiet: | info rocm_smi
|
||||
@printf "\n\033[1;92mBuilding btop++ \033[91m(\033[97mv$(BTOP_VERSION)\033[91m) \033[93m$(PLATFORM) \033[96m$(ARCH)\033[0m\n"
|
||||
|
||||
help:
|
||||
|
@ -300,9 +324,31 @@ uninstall:
|
|||
#? Pull in dependency info for *existing* .o files
|
||||
-include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT))
|
||||
|
||||
#? Compile rocm_smi
|
||||
ifeq ($(GPU_SUPPORT)$(RSMI_STATIC),truetrue)
|
||||
.ONESHELL:
|
||||
rocm_smi:
|
||||
@printf "\n\033[1;92mBuilding ROCm SMI static library\033[37m...\033[0m\n"
|
||||
@TSTAMP=$$(date +%s 2>/dev/null || echo "0")
|
||||
@mkdir -p lib/rocm_smi_lib/build
|
||||
@cd lib/rocm_smi_lib/build
|
||||
@$(QUIET) || printf "\033[1;97mRunning CMake...\033[0m\n"
|
||||
@cmake .. $(SUPPRESS) || { printf "\033[1;91mCMake failed, continuing build without statically linking ROCm SMI\033[37m...\033[0m\n"; exit 0; }
|
||||
@$(QUIET) || printf "\n\033[1;97mBuilding and linking...\033[0m\n"
|
||||
@$(MAKE) $(SUPPRESS) || { printf "\033[1;91mMake failed, continuing build without statically linking ROCm SMI\033[37m...\033[0m\n"; exit 0; }
|
||||
@ar -crs rocm_smi/librocm_smi64.a $$(find rocm_smi -name '*.o') $(SURPRESS) || { printf "\033[1;91mFailed to pack ROCm SMI into static library, continuing build without statically linking ROCm SMI\033[37m...\033[0m\n"; exit 0; }
|
||||
@printf "\033[1;92m100$(P)\033[10D\033[5C-> \033[1;37mrocm_smi/librocm_smi64.a \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah rocm_smi/librocm_smi64.a | cut -f1)iB\033[1;93m)\033[0m\n"
|
||||
@printf "\033[1;92mROCm SMI build complete in \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n"
|
||||
@$(eval override LDFLAGS += lib/rocm_smi_lib/build/rocm_smi/librocm_smi64.a -DRSMI_STATIC) # TODO: this seems to execute every time, no matter if the compilation failed or succeeded
|
||||
@$(eval override CXXFLAGS += -DRSMI_STATIC)
|
||||
else
|
||||
rocm_smi:
|
||||
@true
|
||||
endif
|
||||
|
||||
#? Link
|
||||
.ONESHELL:
|
||||
btop: $(OBJECTS) | directories
|
||||
btop: $(OBJECTS) | rocm_smi directories
|
||||
@sleep 0.2 2>/dev/null || true
|
||||
@TSTAMP=$$(date +%s 2>/dev/null || echo "0")
|
||||
@$(QUIET) || printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n"
|
||||
|
@ -313,7 +359,7 @@ btop: $(OBJECTS) | directories
|
|||
|
||||
#? Compile
|
||||
.ONESHELL:
|
||||
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) | directories
|
||||
$(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) | rocm_smi directories
|
||||
@sleep 0.3 2>/dev/null || true
|
||||
@TSTAMP=$$(date +%s 2>/dev/null || echo "0")
|
||||
@$(QUIET) || printf "\033[1;97mCompiling $<\033[0m\n"
|
||||
|
|
54
README.md
54
README.md
|
@ -33,12 +33,33 @@
|
|||
* [Compilation Linux](#compilation-linux)
|
||||
* [Compilation macOS](#compilation-macos-osx)
|
||||
* [Compilation FreeBSD](#compilation-freebsd)
|
||||
* [GPU compatibility](#gpu-compatibility)
|
||||
* [Installing the snap](#installing-the-snap)
|
||||
* [Configurability](#configurability)
|
||||
* [License](#license)
|
||||
|
||||
## News
|
||||
|
||||
##### 25 November 2023
|
||||
|
||||
GPU monitoring added for Linux!
|
||||
|
||||
Compile from git main to try it out.
|
||||
|
||||
Use keys `5`, `6`, `7` and `0` to show/hide the gpu monitoring boxes. `5` = Gpu 1, `6` = Gpu 2, etc.
|
||||
|
||||
Gpu stats/graphs can also be displayed in the "Cpu box" (not as verbose), see the cpu options menu for info and configuration.
|
||||
|
||||
Note that the binaries provided on the release page (when released) and the continuous builds will not have gpu support enabled.
|
||||
|
||||
Because the GPU support relies on loading of dynamic gpu libraries, gpu support will not work when also static linking.
|
||||
|
||||
See [Compilation Linux](#compilation-linux) for more info on how to compile with gpu monitoring support.
|
||||
|
||||
Many thanks to [@romner-set](https://github.com/romner-set) who wrote the vast majority of the implementation for GPU support.
|
||||
|
||||
Big update with version bump to 1.3 coming soon.
|
||||
|
||||
##### 28 August 2022
|
||||
|
||||
[![btop4win](https://github.com/aristocratos/btop4win/raw/master/Img/logo.png)](https://github.com/aristocratos/btop4win)
|
||||
|
@ -309,6 +330,34 @@ Also needs a UTF8 locale and a font that covers:
|
|||
|
||||
The makefile also needs GNU coreutils and `sed` (should already be installed on any modern distribution).
|
||||
|
||||
### GPU compatibility
|
||||
|
||||
Btop++ supports NVIDIA and AMD GPUs out of the box on Linux x86_64, provided you have the correct drivers and libraries.
|
||||
|
||||
Compatibility with Intel GPUs using generic DRM calls is planned, as is compatibility for FreeBSD and macOS.
|
||||
|
||||
Gpu support will not work when static linking glibc (or musl, etc.)!
|
||||
|
||||
For x86_64 Linux the flag `GPU_SUPPORT` is automatically set to `true`, to manually disable gpu support set the flag to false, like:
|
||||
|
||||
`make GPU_SUPPORT=false`
|
||||
|
||||
* **NVIDIA**
|
||||
|
||||
You must use an official NVIDIA driver, both the closed-source and [open-source](https://github.com/NVIDIA/open-gpu-kernel-modules) ones have been verified to work.
|
||||
|
||||
In addition to that you must also have the `nvidia-ml` dynamic library installed, which should be included with the driver package of your distribution.
|
||||
|
||||
* **AMD**
|
||||
|
||||
AMDGPU data is queried using the [ROCm SMI](https://github.com/RadeonOpenCompute/rocm_smi_lib) library, which may or may not be packaged for your distribution. If your distribution doesn't provide a package, btop++ is statically linked to ROCm SMI with the `RSMI_STATIC=true` make flag.
|
||||
|
||||
This flag expects the ROCm SMI source code in `lib/rocm_smi_lib`, and compilation will fail if it's not there. The latest tested version is 5.6.x, which can be obtained with the following command:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RadeonOpenCompute/rocm_smi_lib.git --depth 1 -b rocm-5.6.x lib/rocm_smi_lib
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
<summary>
|
||||
|
@ -347,6 +396,9 @@ Also needs a UTF8 locale and a font that covers:
|
|||
Append `ARCH=<architecture>` to manually set the target architecture.
|
||||
If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system.
|
||||
|
||||
Append `RSMI_STATIC=true` to statically link the ROCm SMI library used for querying AMDGPU data.
|
||||
See [GPU compatibility](#gpu-compatibility) for details.
|
||||
|
||||
Use `ADDFLAGS` variable for appending flags to both compiler and linker.
|
||||
|
||||
For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system.
|
||||
|
@ -834,7 +886,7 @@ graph_symbol_net = "default"
|
|||
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
|
||||
graph_symbol_proc = "default"
|
||||
|
||||
#* Manually set which boxes to show. Available values are "cpu mem net proc", separate values with whitespace.
|
||||
#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
|
||||
shown_boxes = "proc cpu mem net"
|
||||
|
||||
#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
|
||||
|
|
104
src/btop.cpp
104
src/btop.cpp
|
@ -183,8 +183,11 @@ void term_resize(bool force) {
|
|||
if (force and refreshed) force = false;
|
||||
}
|
||||
else return;
|
||||
|
||||
static const array<string, 4> all_boxes = {"cpu", "mem", "net", "proc"};
|
||||
#ifdef GPU_SUPPORT
|
||||
static const array<string, 10> all_boxes = {"gpu5", "cpu", "mem", "net", "proc", "gpu0", "gpu1", "gpu2", "gpu3", "gpu4"};
|
||||
#else
|
||||
static const array<string, 5> all_boxes = {"", "cpu", "mem", "net", "proc"};
|
||||
#endif
|
||||
Global::resized = true;
|
||||
if (Runner::active) Runner::stop();
|
||||
Term::refresh();
|
||||
|
@ -222,10 +225,18 @@ void term_resize(bool force) {
|
|||
auto key = Input::get();
|
||||
if (key == "q")
|
||||
clean_quit(0);
|
||||
else if (is_in(key, "1", "2", "3", "4")) {
|
||||
Config::current_preset = -1;
|
||||
Config::toggle_box(all_boxes.at(std::stoi(key) - 1));
|
||||
boxes = Config::getS("shown_boxes");
|
||||
else if (key.size() == 1 and isint(key)) {
|
||||
auto intKey = stoi(key);
|
||||
#ifdef GPU_SUPPORT
|
||||
if ((intKey == 0 and Gpu::gpu_names.size() >= 5) or (intKey >= 5 and std::cmp_greater_equal(Gpu::gpu_names.size(), intKey - 4))) {
|
||||
#else
|
||||
if (intKey > 0 and intKey < 5) {
|
||||
#endif
|
||||
auto box = all_boxes.at(intKey);
|
||||
Config::current_preset = -1;
|
||||
Config::toggle_box(box);
|
||||
boxes = Config::getS("shown_boxes");
|
||||
}
|
||||
}
|
||||
}
|
||||
min_size = Term::get_min_size(boxes);
|
||||
|
@ -258,6 +269,11 @@ void clean_quit(int sig) {
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
Gpu::Nvml::shutdown();
|
||||
Gpu::Rsmi::shutdown();
|
||||
#endif
|
||||
|
||||
Config::write();
|
||||
|
||||
if (Term::initialized) {
|
||||
|
@ -388,7 +404,9 @@ namespace Runner {
|
|||
|
||||
enum debug_actions {
|
||||
collect_begin,
|
||||
collect_done,
|
||||
draw_begin,
|
||||
draw_begin_only,
|
||||
draw_done
|
||||
};
|
||||
|
||||
|
@ -424,6 +442,13 @@ namespace Runner {
|
|||
case collect_begin:
|
||||
debug_times[name].at(collect) = time_micros();
|
||||
return;
|
||||
case collect_done:
|
||||
debug_times[name].at(collect) = time_micros() - debug_times[name].at(collect);
|
||||
debug_times["total"].at(collect) += debug_times[name].at(collect);
|
||||
return;
|
||||
case draw_begin_only:
|
||||
debug_times[name].at(draw) = time_micros();
|
||||
return;
|
||||
case draw_begin:
|
||||
debug_times[name].at(draw) = time_micros();
|
||||
debug_times[name].at(collect) = debug_times[name].at(draw) - debug_times[name].at(collect);
|
||||
|
@ -480,10 +505,14 @@ namespace Runner {
|
|||
|
||||
//! DEBUG stats
|
||||
if (Global::debug) {
|
||||
if (debug_bg.empty() or redraw)
|
||||
Runner::debug_bg = Draw::createBox(2, 2, 33, 8, "", true, "μs");
|
||||
|
||||
|
||||
if (debug_bg.empty() or redraw)
|
||||
Runner::debug_bg = Draw::createBox(2, 2, 33,
|
||||
#ifdef GPU_SUPPORT
|
||||
9,
|
||||
#else
|
||||
8,
|
||||
#endif
|
||||
"", true, "μs");
|
||||
|
||||
debug_times.clear();
|
||||
debug_times["total"] = {0, 0};
|
||||
|
@ -493,6 +522,29 @@ namespace Runner {
|
|||
|
||||
//* Run collection and draw functions for all boxes
|
||||
try {
|
||||
#ifdef GPU_SUPPORT
|
||||
//? GPU data collection
|
||||
const bool gpu_in_cpu_panel = Gpu::gpu_names.size() > 0 and (
|
||||
Config::getS("cpu_graph_lower").starts_with("gpu-") or Config::getS("cpu_graph_upper").starts_with("gpu-")
|
||||
or (Gpu::shown == 0 and Config::getS("show_gpu_info") != "Off")
|
||||
);
|
||||
|
||||
vector<unsigned int> gpu_panels = {};
|
||||
for (auto& box : conf.boxes)
|
||||
if (box.starts_with("gpu"))
|
||||
gpu_panels.push_back(box.back()-'0');
|
||||
|
||||
vector<Gpu::gpu_info> gpus;
|
||||
if (gpu_in_cpu_panel or not gpu_panels.empty()) {
|
||||
if (Global::debug) debug_timer("gpu", collect_begin);
|
||||
gpus = Gpu::collect(conf.no_update);
|
||||
if (Global::debug) debug_timer("gpu", collect_done);
|
||||
}
|
||||
auto& gpus_ref = gpus;
|
||||
#else
|
||||
vector<Gpu::gpu_info> gpus_ref{};
|
||||
#endif
|
||||
|
||||
//? CPU
|
||||
if (v_contains(conf.boxes, "cpu")) {
|
||||
try {
|
||||
|
@ -512,7 +564,7 @@ namespace Runner {
|
|||
if (Global::debug) debug_timer("cpu", draw_begin);
|
||||
|
||||
//? Draw box
|
||||
if (not pause_output) output += Cpu::draw(cpu, conf.force_redraw, conf.no_update);
|
||||
if (not pause_output) output += Cpu::draw(cpu, gpus_ref, conf.force_redraw, conf.no_update);
|
||||
|
||||
if (Global::debug) debug_timer("cpu", draw_done);
|
||||
}
|
||||
|
@ -520,7 +572,24 @@ namespace Runner {
|
|||
throw std::runtime_error("Cpu:: -> " + string{e.what()});
|
||||
}
|
||||
}
|
||||
#ifdef GPU_SUPPORT
|
||||
//? GPU
|
||||
if (not gpu_panels.empty() and not gpus_ref.empty()) {
|
||||
try {
|
||||
if (Global::debug) debug_timer("gpu", draw_begin_only);
|
||||
|
||||
//? Draw box
|
||||
if (not pause_output)
|
||||
for (unsigned long i = 0; i < gpu_panels.size(); ++i)
|
||||
output += Gpu::draw(gpus_ref[gpu_panels[i]], i, conf.force_redraw, conf.no_update);
|
||||
|
||||
if (Global::debug) debug_timer("gpu", draw_done);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
throw std::runtime_error("Gpu:: -> " + string{e.what()});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
//? MEM
|
||||
if (v_contains(conf.boxes, "mem")) {
|
||||
try {
|
||||
|
@ -580,6 +649,7 @@ namespace Runner {
|
|||
throw std::runtime_error("Proc:: -> " + string{e.what()});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
Global::exit_error_msg = "Exception in runner thread -> " + string{e.what()};
|
||||
|
@ -610,8 +680,9 @@ namespace Runner {
|
|||
"{mv3}{hiFg}2 {mainFg}| Show MEM box"
|
||||
"{mv4}{hiFg}3 {mainFg}| Show NET box"
|
||||
"{mv5}{hiFg}4 {mainFg}| Show PROC box"
|
||||
"{mv6}{hiFg}esc {mainFg}| Show menu"
|
||||
"{mv7}{hiFg}q {mainFg}| Quit",
|
||||
"{mv6}{hiFg}5-0 {mainFg}| Show GPU boxes"
|
||||
"{mv7}{hiFg}esc {mainFg}| Show menu"
|
||||
"{mv8}{hiFg}q {mainFg}| Quit",
|
||||
"banner"_a = Draw::banner_gen(y, 0, true),
|
||||
"titleFg"_a = Theme::c("title"), "b"_a = Fx::b, "hiFg"_a = Theme::c("hi_fg"), "mainFg"_a = Theme::c("main_fg"),
|
||||
"mv1"_a = Mv::to(y+6, x),
|
||||
|
@ -620,7 +691,8 @@ namespace Runner {
|
|||
"mv4"_a = Mv::to(y+10, x),
|
||||
"mv5"_a = Mv::to(y+11, x),
|
||||
"mv6"_a = Mv::to(y+12, x-2),
|
||||
"mv7"_a = Mv::to(y+13, x)
|
||||
"mv7"_a = Mv::to(y+13, x-2),
|
||||
"mv8"_a = Mv::to(y+14, x)
|
||||
);
|
||||
}
|
||||
output += empty_bg;
|
||||
|
@ -634,7 +706,11 @@ namespace Runner {
|
|||
"post"_a = Theme::c("main_fg") + Fx::ub
|
||||
);
|
||||
static auto loc = std::locale(std::locale::classic(), new MyNumPunct);
|
||||
#ifdef GPU_SUPPORT
|
||||
for (const string name : {"cpu", "mem", "net", "proc", "gpu", "total"}) {
|
||||
#else
|
||||
for (const string name : {"cpu", "mem", "net", "proc", "total"}) {
|
||||
#endif
|
||||
if (not debug_times.contains(name)) debug_times[name] = {0,0};
|
||||
const auto& [time_collect, time_draw] = debug_times.at(name);
|
||||
if (name == "total") output += Fx::b;
|
||||
|
|
|
@ -73,14 +73,16 @@ namespace Config {
|
|||
"#* Note that \"tty\" only has half the horizontal resolution of the other two, so will show a shorter historical view."},
|
||||
|
||||
{"graph_symbol_cpu", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
{"graph_symbol_gpu", "# Graph symbol to use for graphs in gpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
|
||||
#endif
|
||||
{"graph_symbol_mem", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
|
||||
|
||||
{"graph_symbol_net", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
|
||||
|
||||
{"graph_symbol_proc", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."},
|
||||
|
||||
{"shown_boxes", "#* Manually set which boxes to show. Available values are \"cpu mem net proc\", separate values with whitespace."},
|
||||
{"shown_boxes", "#* Manually set which boxes to show. Available values are \"cpu mem net proc\" and \"gpu0\" through \"gpu5\", separate values with whitespace."},
|
||||
|
||||
{"update_ms", "#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs."},
|
||||
|
||||
|
@ -114,7 +116,9 @@ namespace Config {
|
|||
|
||||
{"cpu_graph_lower", "#* Sets the CPU stat shown in lower half of the CPU graph, \"total\" is always available.\n"
|
||||
"#* Select from a list of detected attributes from the options menu."},
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
{"show_gpu_info", "#* If gpu info should be shown in the cpu box. Available values = \"Auto\", \"On\" and \"Off\"."},
|
||||
#endif
|
||||
{"cpu_invert_lower", "#* Toggles if the lower CPU graph should be inverted."},
|
||||
|
||||
{"cpu_single_graph", "#* Set to True to completely disable the lower CPU graph."},
|
||||
|
@ -194,7 +198,21 @@ namespace Config {
|
|||
{"selected_battery", "#* Which battery to use if multiple are present. \"Auto\" for auto detection."},
|
||||
|
||||
{"log_level", "#* Set loglevel for \"~/.config/btop/btop.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n"
|
||||
"#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."}
|
||||
"#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."},
|
||||
#ifdef GPU_SUPPORT
|
||||
|
||||
{"nvml_measure_pcie_speeds",
|
||||
"#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards."},
|
||||
|
||||
{"gpu_mirror_graph", "#* Horizontally mirror the GPU graph."},
|
||||
|
||||
{"custom_gpu_name0", "#* Custom gpu0 model name, empty string to disable."},
|
||||
{"custom_gpu_name1", "#* Custom gpu1 model name, empty string to disable."},
|
||||
{"custom_gpu_name2", "#* Custom gpu2 model name, empty string to disable."},
|
||||
{"custom_gpu_name3", "#* Custom gpu3 model name, empty string to disable."},
|
||||
{"custom_gpu_name4", "#* Custom gpu4 model name, empty string to disable."},
|
||||
{"custom_gpu_name5", "#* Custom gpu5 model name, empty string to disable."},
|
||||
#endif
|
||||
};
|
||||
|
||||
unordered_flat_map<std::string_view, string> strings = {
|
||||
|
@ -203,12 +221,13 @@ namespace Config {
|
|||
{"graph_symbol", "braille"},
|
||||
{"presets", "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"},
|
||||
{"graph_symbol_cpu", "default"},
|
||||
{"graph_symbol_gpu", "default"},
|
||||
{"graph_symbol_mem", "default"},
|
||||
{"graph_symbol_net", "default"},
|
||||
{"graph_symbol_proc", "default"},
|
||||
{"proc_sorting", "cpu lazy"},
|
||||
{"cpu_graph_upper", "total"},
|
||||
{"cpu_graph_lower", "total"},
|
||||
{"cpu_graph_upper", "Auto"},
|
||||
{"cpu_graph_lower", "Auto"},
|
||||
{"cpu_sensor", "Auto"},
|
||||
{"selected_battery", "Auto"},
|
||||
{"cpu_core_map", ""},
|
||||
|
@ -222,6 +241,15 @@ namespace Config {
|
|||
{"proc_filter", ""},
|
||||
{"proc_command", ""},
|
||||
{"selected_name", ""},
|
||||
#ifdef GPU_SUPPORT
|
||||
{"custom_gpu_name0", ""},
|
||||
{"custom_gpu_name1", ""},
|
||||
{"custom_gpu_name2", ""},
|
||||
{"custom_gpu_name3", ""},
|
||||
{"custom_gpu_name4", ""},
|
||||
{"custom_gpu_name5", ""},
|
||||
{"show_gpu_info", "Auto"}
|
||||
#endif
|
||||
};
|
||||
unordered_flat_map<std::string_view, string> stringsTmp;
|
||||
|
||||
|
@ -271,6 +299,10 @@ namespace Config {
|
|||
{"show_detailed", false},
|
||||
{"proc_filtering", false},
|
||||
{"proc_aggregate", false},
|
||||
#ifdef GPU_SUPPORT
|
||||
{"nvml_measure_pcie_speeds", true},
|
||||
{"gpu_mirror_graph", true},
|
||||
#endif
|
||||
};
|
||||
unordered_flat_map<std::string_view, bool> boolsTmp;
|
||||
|
||||
|
@ -321,7 +353,7 @@ namespace Config {
|
|||
validError = "Malformatted preset in config value presets!";
|
||||
return false;
|
||||
}
|
||||
if (not is_in(vals.at(0), "cpu", "mem", "net", "proc")) {
|
||||
if (not is_in(vals.at(0), "cpu", "mem", "net", "proc", "gpu0", "gpu1", "gpu2", "gpu3", "gpu4", "gpu5")) {
|
||||
validError = "Invalid box name in config value presets!";
|
||||
return false;
|
||||
}
|
||||
|
@ -417,6 +449,11 @@ namespace Config {
|
|||
else if (name == "shown_boxes" and not value.empty() and not check_boxes(value))
|
||||
validError = "Invalid box name(s) in shown_boxes!";
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
else if (name == "show_gpu_info" and not v_contains(show_gpu_values, value))
|
||||
validError = "Invalid value for show_gpu_info: " + value;
|
||||
#endif
|
||||
|
||||
else if (name == "presets" and not presetsValid(value))
|
||||
return false;
|
||||
|
||||
|
@ -519,6 +556,13 @@ namespace Config {
|
|||
auto new_boxes = ssplit(boxes);
|
||||
for (auto& box : new_boxes) {
|
||||
if (not v_contains(valid_boxes, box)) return false;
|
||||
#ifdef GPU_SUPPORT
|
||||
if (box.starts_with("gpu")) {
|
||||
size_t gpu_num = stoi(box.substr(3));
|
||||
if (gpu_num == 0) gpu_num = 5;
|
||||
if (std::cmp_greater(gpu_num, Gpu::gpu_names.size())) return false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
current_boxes = std::move(new_boxes);
|
||||
return true;
|
||||
|
|
|
@ -43,9 +43,16 @@ namespace Config {
|
|||
|
||||
const vector<string> valid_graph_symbols = { "braille", "block", "tty" };
|
||||
const vector<string> valid_graph_symbols_def = { "default", "braille", "block", "tty" };
|
||||
const vector<string> valid_boxes = { "cpu", "mem", "net", "proc" };
|
||||
const vector<string> valid_boxes = {
|
||||
"cpu", "mem", "net", "proc"
|
||||
#ifdef GPU_SUPPORT
|
||||
,"gpu0", "gpu1", "gpu2", "gpu3", "gpu4", "gpu5"
|
||||
#endif
|
||||
};
|
||||
const vector<string> temp_scales = { "celsius", "fahrenheit", "kelvin", "rankine" };
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
const vector<string> show_gpu_values = { "Auto", "On", "Off" };
|
||||
#endif
|
||||
extern vector<string> current_boxes;
|
||||
extern vector<string> preset_list;
|
||||
extern vector<string> available_batteries;
|
||||
|
|
|
@ -20,6 +20,7 @@ tab-size = 4
|
|||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ranges>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "btop_draw.hpp"
|
||||
|
@ -30,6 +31,7 @@ tab-size = 4
|
|||
#include "btop_input.hpp"
|
||||
#include "btop_menu.hpp"
|
||||
|
||||
|
||||
using std::array;
|
||||
using std::clamp;
|
||||
using std::cmp_equal;
|
||||
|
@ -509,33 +511,58 @@ namespace Cpu {
|
|||
int x = 1, y = 1, width = 20, height;
|
||||
int b_columns, b_column_size;
|
||||
int b_x, b_y, b_width, b_height;
|
||||
int graph_up_height;
|
||||
long unsigned int lavg_str_len = 0;
|
||||
int graph_up_height, graph_low_height;
|
||||
int graph_up_width, graph_low_width;
|
||||
int gpu_meter_width;
|
||||
bool shown = true, redraw = true, mid_line = false;
|
||||
string box;
|
||||
Draw::Graph graph_upper;
|
||||
Draw::Graph graph_lower;
|
||||
vector<Draw::Graph> graphs_upper;
|
||||
vector<Draw::Graph> graphs_lower;
|
||||
Draw::Meter cpu_meter;
|
||||
vector<Draw::Meter> gpu_meters;
|
||||
vector<Draw::Graph> core_graphs;
|
||||
vector<Draw::Graph> temp_graphs;
|
||||
vector<Draw::Graph> gpu_temp_graphs;
|
||||
vector<Draw::Graph> gpu_mem_graphs;
|
||||
|
||||
string draw(const cpu_info& cpu, bool force_redraw, bool data_same) {
|
||||
string draw(const cpu_info& cpu, const vector<Gpu::gpu_info>& gpus, bool force_redraw, bool data_same) {
|
||||
if (Runner::stopping) return "";
|
||||
if (force_redraw) redraw = true;
|
||||
bool show_temps = (Config::getB("check_temp") and got_sensors);
|
||||
auto single_graph = Config::getB("cpu_single_graph");
|
||||
bool hide_cores = show_temps and (cpu_temp_only or not Config::getB("show_coretemp"));
|
||||
const int extra_width = (hide_cores ? max(6, 6 * b_column_size) : 0);
|
||||
auto& graph_up_field = Config::getS("cpu_graph_upper");
|
||||
auto& graph_lo_field = Config::getS("cpu_graph_lower");
|
||||
#ifdef GPU_SUPPORT
|
||||
const auto& show_gpu_info = Config::getS("show_gpu_info");
|
||||
const bool gpu_always = show_gpu_info == "On";
|
||||
bool show_gpu = (gpus.size() > 0 and (gpu_always or (show_gpu_info == "Auto" and Gpu::shown == 0)));
|
||||
#else
|
||||
(void)gpus;
|
||||
#endif
|
||||
auto graph_up_field = Config::getS("cpu_graph_upper");
|
||||
if (graph_up_field == "Auto" or not v_contains(Cpu::available_fields, graph_up_field))
|
||||
graph_up_field = "total";
|
||||
auto graph_lo_field = Config::getS("cpu_graph_lower");
|
||||
if (graph_lo_field == "Auto" or not v_contains(Cpu::available_fields, graph_lo_field)) {
|
||||
#ifdef GPU_SUPPORT
|
||||
graph_lo_field = show_gpu ? "gpu-totals" : graph_up_field;
|
||||
#else
|
||||
graph_lo_field = graph_up_field;
|
||||
#endif
|
||||
}
|
||||
auto tty_mode = Config::getB("tty_mode");
|
||||
auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_cpu"));
|
||||
auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
|
||||
auto& temp_scale = Config::getS("temp_scale");
|
||||
auto cpu_bottom = Config::getB("cpu_bottom");
|
||||
|
||||
const string& title_left = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_left_down : Symbols::title_left);
|
||||
const string& title_right = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_right_down : Symbols::title_right);
|
||||
static int bat_pos = 0, bat_len = 0;
|
||||
if (cpu.cpu_percent.at("total").empty()
|
||||
or cpu.core_percent.at(0).empty()
|
||||
or (show_temps and cpu.temp.at(0).empty())) return "";
|
||||
if (cpu.cpu_percent.at("total").empty()
|
||||
or cpu.core_percent.at(0).empty()
|
||||
or (show_temps and cpu.temp.at(0).empty())) return "";
|
||||
|
@ -545,8 +572,8 @@ namespace Cpu {
|
|||
//* Redraw elements not needed to be updated every cycle
|
||||
if (redraw) {
|
||||
mid_line = (not single_graph and graph_up_field != graph_lo_field);
|
||||
graph_up_height = (single_graph ? height - 2 : ceil((double)(height - 2) / 2) - (mid_line and height % 2 != 0 ? 1 : 0));
|
||||
const int graph_low_height = height - 2 - graph_up_height - (mid_line ? 1 : 0);
|
||||
graph_up_height = (single_graph ? height - 2 : ceil((double)(height - 2) / 2) - (mid_line and height % 2 != 0));
|
||||
graph_low_height = height - 2 - graph_up_height - mid_line;
|
||||
const int button_y = cpu_bottom ? y + height - 1 : y;
|
||||
out += box;
|
||||
|
||||
|
@ -563,17 +590,91 @@ namespace Cpu {
|
|||
Input::mouse_mappings["+"] = {button_y, x + width - 5, 1, 2};
|
||||
|
||||
//? Graphs & meters
|
||||
graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", cpu.cpu_percent.at(graph_up_field), graph_symbol, false, true};
|
||||
const int graph_default_width = x + width - b_width - 3;
|
||||
|
||||
auto init_graphs = [&](vector<Draw::Graph>& graphs, const int graph_height, int& graph_width, const string& graph_field, bool invert) {
|
||||
#ifdef GPU_SUPPORT
|
||||
if (graph_field.starts_with("gpu")) {
|
||||
if (graph_field.find("totals") != string::npos) {
|
||||
graphs.resize(gpus.size());
|
||||
gpu_temp_graphs.resize(gpus.size());
|
||||
gpu_mem_graphs.resize(gpus.size());
|
||||
gpu_meters.resize(gpus.size());
|
||||
graph_width = graph_default_width/(int)gpus.size() - (int)gpus.size() + 1 + graph_default_width%gpus.size();
|
||||
for (unsigned long i = 0;;) {
|
||||
auto& gpu = gpus[i]; auto& graph = graphs[i];
|
||||
|
||||
//? GPU graphs/meters
|
||||
if (gpu.supported_functions.temp_info)
|
||||
gpu_temp_graphs[i] = Draw::Graph{ 5, 1, "temp", gpu.temp, graph_symbol, false, false, gpu.temp_max, -23 };
|
||||
if (gpu.supported_functions.mem_used and gpu.supported_functions.mem_total)
|
||||
gpu_mem_graphs[i] = Draw::Graph{ 5, 1, "used", gpu.gpu_percent.at("gpu-vram-totals"), graph_symbol };
|
||||
if (gpu.supported_functions.gpu_utilization) {
|
||||
gpu_meter_width = b_width - 12 - (int)floating_humanizer(gpu.mem_total, true).size() - (show_temps ? 24 : 12) - (int)to_string(i).size() + (gpus.size() == 1)*2 - (gpus.size() > 9 and i <= 9);
|
||||
gpu_meters[i] = Draw::Meter{gpu_meter_width, "cpu" };
|
||||
}
|
||||
|
||||
bool utilization_support = gpu.supported_functions.gpu_utilization;
|
||||
if (++i < gpus.size()) {
|
||||
if (utilization_support)
|
||||
graph = Draw::Graph{graph_width, graph_height, "cpu", gpu.gpu_percent.at(graph_field), graph_symbol, invert, true};
|
||||
} else {
|
||||
if (utilization_support)
|
||||
graph = Draw::Graph{
|
||||
graph_width + graph_default_width%graph_width - (int)gpus.size() + 1,
|
||||
graph_height, "cpu", gpu.gpu_percent.at(graph_field), graph_symbol, invert, true
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
graphs.resize(1);
|
||||
graph_width = graph_default_width;
|
||||
graphs[0] = Draw::Graph{ graph_width, graph_height, "cpu", Gpu::shared_gpu_percent.at(graph_field), graph_symbol, invert, true };
|
||||
gpu_temp_graphs.resize(gpus.size());
|
||||
gpu_mem_graphs.resize(gpus.size());
|
||||
gpu_meters.resize(gpus.size());
|
||||
for (unsigned long i = 0; i < gpus.size(); ++i) {
|
||||
if (gpus[i].supported_functions.temp_info)
|
||||
gpu_temp_graphs[i] = Draw::Graph{ 5, 1, "temp", gpus[i].temp, graph_symbol, false, false, gpus[i].temp_max, -23 };
|
||||
if (gpus[i].supported_functions.mem_used and gpus[i].supported_functions.mem_total)
|
||||
gpu_mem_graphs[i] = Draw::Graph{ 5, 1, "used", gpus[i].gpu_percent.at("gpu-vram-totals"), graph_symbol };
|
||||
if (gpus[i].supported_functions.gpu_utilization) {
|
||||
gpu_meter_width = b_width - 12 - (int)floating_humanizer(gpus[i].mem_total, true).size() - (show_temps ? 24 : 12) - (int)to_string(i).size() + (gpus.size() == 1)*2 - (gpus.size() > 9 and i <= 9);
|
||||
gpu_meters[i] = Draw::Meter{gpu_meter_width, "cpu" };
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#endif
|
||||
graphs.resize(1);
|
||||
graph_width = graph_default_width;
|
||||
graphs[0] = Draw::Graph{ graph_width, graph_height, "cpu", cpu.cpu_percent.at(graph_field), graph_symbol, invert, true };
|
||||
#ifdef GPU_SUPPORT
|
||||
if (std::cmp_less(Gpu::shown, gpus.size())) {
|
||||
gpu_temp_graphs.resize(gpus.size());
|
||||
gpu_mem_graphs.resize(gpus.size());
|
||||
gpu_meters.resize(gpus.size());
|
||||
for (unsigned long i = 0; i < gpus.size(); ++i) {
|
||||
if (gpus[i].supported_functions.temp_info)
|
||||
gpu_temp_graphs[i] = Draw::Graph{ 5, 1, "temp", gpus[i].temp, graph_symbol, false, false, gpus[i].temp_max, -23 };
|
||||
if (gpus[i].supported_functions.mem_used and gpus[i].supported_functions.mem_total)
|
||||
gpu_mem_graphs[i] = Draw::Graph{ 5, 1, "used", gpus[i].gpu_percent.at("gpu-vram-totals"), graph_symbol };
|
||||
if (gpus[i].supported_functions.gpu_utilization) {
|
||||
gpu_meter_width = b_width - 12 - (int)floating_humanizer(gpus[i].mem_total, true).size() - (show_temps ? 24 : 12) - (int)to_string(i).size() + (gpus.size() == 1)*2 - (gpus.size() > 9 and i <= 9);
|
||||
gpu_meters[i] = Draw::Meter{gpu_meter_width, "cpu" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
init_graphs(graphs_upper, graph_up_height, graph_up_width, graph_up_field, false);
|
||||
if (not single_graph)
|
||||
init_graphs(graphs_lower, graph_low_height, graph_low_width, graph_lo_field, Config::getB("cpu_invert_lower"));
|
||||
|
||||
cpu_meter = Draw::Meter{b_width - (show_temps ? 23 - (b_column_size <= 1 and b_columns == 1 ? 6 : 0) : 11), "cpu"};
|
||||
if (not single_graph) {
|
||||
graph_lower = Draw::Graph{
|
||||
x + width - b_width - 3,
|
||||
graph_low_height, "cpu",
|
||||
cpu.cpu_percent.at(graph_lo_field),
|
||||
graph_symbol,
|
||||
Config::getB("cpu_invert_lower"), true
|
||||
};
|
||||
}
|
||||
|
||||
if (mid_line) {
|
||||
out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line")
|
||||
|
@ -641,10 +742,39 @@ namespace Cpu {
|
|||
}
|
||||
|
||||
try {
|
||||
//? Cpu graphs
|
||||
out += Fx::ub + Mv::to(y + 1, x + 1) + graph_upper(cpu.cpu_percent.at(graph_up_field), (data_same or redraw));
|
||||
if (not single_graph)
|
||||
out += Mv::to( y + graph_up_height + 1 + (mid_line ? 1 : 0), x + 1) + graph_lower(cpu.cpu_percent.at(graph_lo_field), (data_same or redraw));
|
||||
//? Cpu/Gpu graphs
|
||||
out += Fx::ub + Mv::to(y + 1, x + 1);
|
||||
auto draw_graphs = [&](vector<Draw::Graph>& graphs, const int graph_height, const int graph_width, const string& graph_field) {
|
||||
#ifdef GPU_SUPPORT
|
||||
if (graph_field.starts_with("gpu"))
|
||||
if (graph_field.find("totals") != string::npos)
|
||||
for (unsigned long i = 0;;) {
|
||||
out += graphs[i](gpus[i].gpu_percent.at(graph_field), (data_same or redraw));
|
||||
if (gpus.size() > 1) {
|
||||
auto i_str = to_string(i);
|
||||
out += Mv::l(graph_width-1) + Mv::u(graph_height/2) + (graph_width > 5 ? "GPU " : "") + i_str
|
||||
+ Mv::d(graph_height/2) + Mv::r(graph_width - 1 - (graph_width > 5)*4 - i_str.size());
|
||||
}
|
||||
|
||||
if (++i < graphs.size())
|
||||
out += Theme::c("div_line") + (Symbols::v_line + Mv::l(1) + Mv::u(1))*graph_height + Mv::r(1) + Mv::d(1);
|
||||
else break;
|
||||
}
|
||||
else
|
||||
out += graphs[0](Gpu::shared_gpu_percent.at(graph_field), (data_same or redraw));
|
||||
else
|
||||
#else
|
||||
(void)graph_height;
|
||||
(void)graph_width;
|
||||
#endif
|
||||
out += graphs[0](cpu.cpu_percent.at(graph_field), (data_same or redraw));
|
||||
};
|
||||
|
||||
draw_graphs(graphs_upper, graph_up_height, graph_up_width, graph_up_field);
|
||||
if (not single_graph) {
|
||||
out += Mv::to(y + graph_up_height + 1 + mid_line, x + 1);
|
||||
draw_graphs(graphs_lower, graph_low_height, graph_low_width, graph_lo_field);
|
||||
}
|
||||
|
||||
//? Uptime
|
||||
if (Config::getB("show_uptime")) {
|
||||
|
@ -732,15 +862,231 @@ namespace Cpu {
|
|||
} else {
|
||||
lavg_str_len = lavg_str.length();
|
||||
}
|
||||
out += Mv::to(b_y + b_height - 2, b_x + cx + 1) + Theme::c("main_fg") + lavg_str;
|
||||
#ifdef GPU_SUPPORT
|
||||
cy = b_height - 2 - (show_gpu ? (gpus.size() - (gpu_always ? 0 : Gpu::shown)) : 0);
|
||||
#else
|
||||
cy = b_height - 2;
|
||||
#endif
|
||||
out += Mv::to(b_y + cy, b_x + cx + 1) + Theme::c("main_fg") + lavg_str;
|
||||
}
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
//? Gpu brief info
|
||||
if (show_gpu) {
|
||||
for (unsigned long i = 0; i < gpus.size(); ++i) {
|
||||
if (gpu_always or not v_contains(Gpu::shown_panels, i)) {
|
||||
out += Mv::to(b_y + ++cy, b_x + 1) + Theme::c("main_fg") + Fx::b + "GPU";
|
||||
if (show_temps and gpus[i].supported_functions.temp_info and b_width < 34) {
|
||||
const auto [temp, unit] = celsius_to(gpus[i].temp.back(), temp_scale);
|
||||
if (temp < 100) out += " ";
|
||||
}
|
||||
if (gpus.size() > 1) out += rjust(to_string(i), 1 + (gpus.size() > 9));
|
||||
if (gpus[i].supported_functions.gpu_utilization) {
|
||||
string meter = gpu_meters[i](gpus[i].gpu_percent.at("gpu-totals").back());
|
||||
out += (meter.size() > 1 ? " " : "") + meter
|
||||
+ Theme::g("cpu").at(gpus[i].gpu_percent.at("gpu-totals").back()) + rjust(to_string(gpus[i].gpu_percent.at("gpu-totals").back()), 4) + Theme::c("main_fg") + '%';
|
||||
} else out += Mv::r(gpu_meter_width);
|
||||
|
||||
if (gpus[i].supported_functions.mem_used) {
|
||||
out += ' ' + Theme::c("inactive_fg") + graph_bg * 6 + Mv::l(6) + Theme::g("used").at(gpus[i].gpu_percent.at("gpu-vram-totals").back())
|
||||
+ gpu_mem_graphs[i](gpus[i].gpu_percent.at("gpu-vram-totals"), data_same or redraw) + Theme::c("main_fg")
|
||||
+ rjust(floating_humanizer(gpus[i].mem_used, true), 5);
|
||||
if (gpus[i].supported_functions.mem_total)
|
||||
out += Theme::c("inactive_fg") + '/' + Theme::c("main_fg") + floating_humanizer(gpus[i].mem_total, true);
|
||||
else out += Mv::r(5);
|
||||
} else out += Mv::r(17);
|
||||
if (show_temps and gpus[i].supported_functions.temp_info) {
|
||||
const auto [temp, unit] = celsius_to(gpus[i].temp.back(), temp_scale);
|
||||
if (b_width > 38)
|
||||
out += ' ' + Theme::c("inactive_fg") + graph_bg * 6 + Mv::l(6) + Theme::g("temp").at(clamp(gpus[i].temp.back() * 100 / gpus[i].temp_max, 0ll, 100ll))
|
||||
+ gpu_temp_graphs[i](gpus[i].temp, data_same or redraw);
|
||||
else out += Theme::g("temp").at(clamp(gpus[i].temp.back() * 100 / gpus[i].temp_max, 0ll, 100ll));
|
||||
out += rjust(to_string(temp), 3 + (b_width >= 34 or temp > 99)) + Theme::c("main_fg") + unit;
|
||||
}
|
||||
}
|
||||
if (cy < b_height - 1) break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
redraw = false;
|
||||
return out + Fx::reset;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
namespace Gpu {
|
||||
int width_p = 100, height_p = 32;
|
||||
int min_width = 41, min_height = 11;
|
||||
int width = 41, height;
|
||||
vector<int> x_vec = {}, y_vec = {}, b_height_vec = {};
|
||||
int b_width;
|
||||
vector<int> b_x_vec = {}, b_y_vec = {};
|
||||
vector<bool> redraw = {};
|
||||
int shown = 0;
|
||||
vector<char> shown_panels = {};
|
||||
int graph_up_height;
|
||||
vector<Draw::Graph> graph_upper_vec = {}, graph_lower_vec = {};
|
||||
vector<Draw::Graph> temp_graph_vec = {};
|
||||
vector<Draw::Graph> mem_used_graph_vec = {}, mem_util_graph_vec = {};
|
||||
vector<Draw::Meter> gpu_meter_vec = {};
|
||||
vector<Draw::Meter> pwr_meter_vec = {};
|
||||
vector<string> box = {};
|
||||
|
||||
string draw(const gpu_info& gpu, unsigned long index, bool force_redraw, bool data_same) {
|
||||
if (Runner::stopping) return "";
|
||||
|
||||
auto& b_x = b_x_vec[index];
|
||||
auto& b_y = b_y_vec[index];
|
||||
auto& x = x_vec[index];
|
||||
auto& y = y_vec[index];
|
||||
|
||||
auto& graph_upper = graph_upper_vec[index];
|
||||
auto& graph_lower = graph_lower_vec[index];
|
||||
auto& temp_graph = temp_graph_vec[index];
|
||||
auto& mem_used_graph = mem_used_graph_vec[index];
|
||||
auto& mem_util_graph = mem_util_graph_vec[index];
|
||||
auto& gpu_meter = gpu_meter_vec[index];
|
||||
auto& pwr_meter = pwr_meter_vec[index];
|
||||
|
||||
if (force_redraw) redraw[index] = true;
|
||||
bool show_temps = gpu.supported_functions.temp_info and (Config::getB("check_temp"));
|
||||
auto tty_mode = Config::getB("tty_mode");
|
||||
auto& temp_scale = Config::getS("temp_scale");
|
||||
auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_gpu"));
|
||||
auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
|
||||
auto single_graph = !Config::getB("gpu_mirror_graph");
|
||||
string out;
|
||||
out.reserve(width * height);
|
||||
|
||||
//* Redraw elements not needed to be updated every cycle
|
||||
if (redraw[index]) {
|
||||
graph_up_height = single_graph ? height - 2 : ceil((double)(height - 2) / 2);
|
||||
const int graph_low_height = height - 2 - graph_up_height;
|
||||
out += box[index];
|
||||
|
||||
if (gpu.supported_functions.gpu_utilization) {
|
||||
graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", gpu.gpu_percent.at("gpu-totals"), graph_symbol, false, true}; // TODO cpu -> gpu
|
||||
if (not single_graph) {
|
||||
graph_lower = Draw::Graph{
|
||||
x + width - b_width - 3,
|
||||
graph_low_height, "cpu",
|
||||
gpu.gpu_percent.at("gpu-totals"),
|
||||
graph_symbol,
|
||||
Config::getB("cpu_invert_lower"), true
|
||||
};
|
||||
}
|
||||
gpu_meter = Draw::Meter{b_width - (show_temps ? 24 : 11), "cpu"};
|
||||
}
|
||||
if (gpu.supported_functions.temp_info)
|
||||
temp_graph = Draw::Graph{6, 1, "temp", gpu.temp, graph_symbol, false, false, gpu.temp_max, -23};
|
||||
if (gpu.supported_functions.pwr_usage)
|
||||
pwr_meter = Draw::Meter{b_width - 24, "cached"};
|
||||
if (gpu.supported_functions.mem_utilization)
|
||||
mem_util_graph = Draw::Graph{b_width/2 - 1, 2, "free", gpu.mem_utilization_percent, graph_symbol, 0, 0, 100, 4}; // offset so the graph isn't empty at 0-5% utilization
|
||||
if (gpu.supported_functions.mem_used and gpu.supported_functions.mem_total)
|
||||
mem_used_graph = Draw::Graph{b_width/2 - 2, 2 + 2*(gpu.supported_functions.mem_utilization), "used", gpu.gpu_percent.at("gpu-vram-totals"), graph_symbol};
|
||||
}
|
||||
|
||||
|
||||
//* General GPU info
|
||||
|
||||
//? Gpu graph, meter & clock speed
|
||||
if (gpu.supported_functions.gpu_utilization) {
|
||||
out += Fx::ub + Mv::to(y + 1, x + 1) + graph_upper(gpu.gpu_percent.at("gpu-totals"), (data_same or redraw[index]));
|
||||
if (not single_graph)
|
||||
out += Mv::to(y + graph_up_height + 1, x + 1) + graph_lower(gpu.gpu_percent.at("gpu-totals"), (data_same or redraw[index]));
|
||||
|
||||
out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "GPU " + gpu_meter(gpu.gpu_percent.at("gpu-totals").back())
|
||||
+ Theme::g("cpu").at(gpu.gpu_percent.at("gpu-totals").back()) + rjust(to_string(gpu.gpu_percent.at("gpu-totals").back()), 4) + Theme::c("main_fg") + '%';
|
||||
|
||||
//? Temperature graph, I assume the device supports utilization if it supports temperature
|
||||
if (show_temps) {
|
||||
const auto [temp, unit] = celsius_to(gpu.temp.back(), temp_scale);
|
||||
out += ' ' + Theme::c("inactive_fg") + graph_bg * 6 + Mv::l(6) + Theme::g("temp").at(clamp(gpu.temp.back() * 100 / gpu.temp_max, 0ll, 100ll))
|
||||
+ temp_graph(gpu.temp, data_same or redraw[index]);
|
||||
out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit;
|
||||
}
|
||||
out += Theme::c("div_line") + Symbols::v_line;
|
||||
}
|
||||
|
||||
if (gpu.supported_functions.gpu_clock) {
|
||||
string clock_speed_string = to_string(gpu.gpu_clock_speed);
|
||||
out += Mv::to(b_y, b_x + b_width - 12) + Theme::c("div_line") + Symbols::h_line*(5-clock_speed_string.size())
|
||||
+ Symbols::title_left + Fx::b + Theme::c("title") + clock_speed_string + " Mhz" + Fx::ub + Theme::c("div_line") + Symbols::title_right;
|
||||
}
|
||||
|
||||
//? Power usage meter, power state
|
||||
if (gpu.supported_functions.pwr_usage) {
|
||||
out += Mv::to(b_y + 2, b_x + 1) + Theme::c("main_fg") + Fx::b + "PWR " + pwr_meter(gpu.gpu_percent.at("gpu-pwr-totals").back())
|
||||
+ Theme::g("cached").at(gpu.gpu_percent.at("gpu-pwr-totals").back()) + rjust(to_string(gpu.pwr_usage/1000), 4) + Theme::c("main_fg") + 'W';
|
||||
if (gpu.supported_functions.pwr_state and gpu.pwr_state != 32) // NVML_PSTATE_UNKNOWN; unsupported or non-nvidia card
|
||||
out += std::string(" P-state: ") + (gpu.pwr_state > 9 ? "" : " ") + 'P' + Theme::g("cached").at(gpu.pwr_state) + to_string(gpu.pwr_state);
|
||||
}
|
||||
|
||||
if (gpu.supported_functions.mem_total or gpu.supported_functions.mem_used) {
|
||||
out += Mv::to(b_y + 3, b_x);
|
||||
if (gpu.supported_functions.mem_total and gpu.supported_functions.mem_used) {
|
||||
string used_memory_string = floating_humanizer(gpu.mem_used);
|
||||
|
||||
auto offset = (gpu.supported_functions.mem_total or gpu.supported_functions.mem_used)
|
||||
* (1 + 2*(gpu.supported_functions.mem_total and gpu.supported_functions.mem_used) + 2*gpu.supported_functions.mem_utilization);
|
||||
|
||||
//? Used graph, memory section header, total vram
|
||||
out += Theme::c("div_line") + Symbols::div_left + Symbols::h_line + Symbols::title_left + Fx::b + Theme::c("title") + "vram" + Theme::c("div_line") + Fx::ub + Symbols::title_right
|
||||
+ Symbols::h_line*(b_width/2-8) + Symbols::div_up + Mv::d(offset)+Mv::l(1) + Symbols::div_down + Mv::l(1)+Mv::u(1) + (Symbols::v_line + Mv::l(1)+Mv::u(1))*(offset-1) + Symbols::div_up
|
||||
+ Symbols::h_line + Theme::c("title") + "Used:" + Theme::c("div_line")
|
||||
+ Symbols::h_line*(b_width/2+b_width%2-9-used_memory_string.size()) + Theme::c("title") + used_memory_string + Theme::c("div_line") + Symbols::h_line + Symbols::div_right
|
||||
+ Mv::d(1) + Mv::l(b_width/2-1) + mem_used_graph(gpu.gpu_percent.at("gpu-vram-totals"), (data_same or redraw[index]))
|
||||
+ Mv::l(b_width-3) + Mv::u(1+2*gpu.supported_functions.mem_utilization) + Theme::c("main_fg") + Fx::b + "Total:" + rjust(floating_humanizer(gpu.mem_total), b_width/2-9) + Fx::ub
|
||||
+ Mv::r(3) + rjust(to_string(gpu.gpu_percent.at("gpu-vram-totals").back()), 3) + '%';
|
||||
|
||||
//? Memory utilization
|
||||
if (gpu.supported_functions.mem_utilization)
|
||||
out += Mv::l(b_width/2+6) + Mv::d(1) + Theme::c("div_line") + Symbols::div_left+Symbols::h_line + Theme::c("title") + "Utilization:" + Theme::c("div_line") + Symbols::h_line*(b_width/2-14) + Symbols::div_right
|
||||
+ Mv::l(b_width/2) + Mv::d(1) + mem_util_graph(gpu.mem_utilization_percent, (data_same or redraw[index]))
|
||||
+ Mv::l(b_width/2-1) + Mv::u(1) + rjust(to_string(gpu.mem_utilization_percent.back()), 3) + '%';
|
||||
|
||||
//? Memory clock speed
|
||||
if (gpu.supported_functions.mem_clock) {
|
||||
string clock_speed_string = to_string(gpu.mem_clock_speed);
|
||||
out += Mv::to(b_y + 3, b_x + b_width/2 - 11) + Theme::c("div_line") + Symbols::h_line*(5-clock_speed_string.size())
|
||||
+ Symbols::title_left + Fx::b + Theme::c("title") + clock_speed_string + " Mhz" + Fx::ub + Theme::c("div_line") + Symbols::title_right;
|
||||
}
|
||||
} else {
|
||||
out += Theme::c("main_fg") + Mv::r(1);
|
||||
if (gpu.supported_functions.mem_total)
|
||||
out += "VRAM total:" + rjust(floating_humanizer(gpu.mem_total), b_width/(1 + gpu.supported_functions.mem_clock)-14);
|
||||
else out += "VRAM usage:" + rjust(floating_humanizer(gpu.mem_used), b_width/(1 + gpu.supported_functions.mem_clock)-14);
|
||||
|
||||
if (gpu.supported_functions.mem_clock)
|
||||
out += " VRAM clock:" + rjust(to_string(gpu.mem_clock_speed) + " Mhz", b_width/2-13);
|
||||
}
|
||||
}
|
||||
|
||||
//? Processes section header
|
||||
//out += Mv::to(b_y+8, b_x) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line + Symbols::title_left + Theme::c("main_fg") + Fx::b + "gpu-proc" + Fx::ub + Theme::c("div_line")
|
||||
// + Symbols::title_right + Symbols::h_line*(b_width/2-12) + Symbols::div_down + Symbols::h_line*(b_width/2-2) + Symbols::div_right;
|
||||
|
||||
//? PCIe link throughput
|
||||
if (gpu.supported_functions.pcie_txrx and Config::getB("nvml_measure_pcie_speeds")) {
|
||||
string tx_string = floating_humanizer(gpu.pcie_tx, 0, 1, 0, 1);
|
||||
string rx_string = floating_humanizer(gpu.pcie_rx, 0, 1, 0, 1);
|
||||
out += Mv::to(b_y + b_height_vec[index] - 1, b_x+2) + Theme::c("div_line")
|
||||
+ Symbols::title_left_down + Theme::c("title") + Fx::b + "TX:" + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::h_line*(b_width/2-9-tx_string.size())
|
||||
+ Symbols::title_left_down + Theme::c("title") + Fx::b + tx_string + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + (gpu.supported_functions.mem_total and gpu.supported_functions.mem_used ? Symbols::div_down : Symbols::h_line)
|
||||
+ Symbols::title_left_down + Theme::c("title") + Fx::b + "RX:" + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::h_line*(b_width/2+b_width%2-9-rx_string.size())
|
||||
+ Symbols::title_left_down + Theme::c("title") + Fx::b + rx_string + Fx::ub + Theme::c("div_line") + Symbols::title_right_down + Symbols::round_right_down;
|
||||
}
|
||||
|
||||
redraw[index] = false;
|
||||
return out + Fx::reset;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Mem {
|
||||
int width_p = 45, height_p = 36;
|
||||
int min_width = 36, min_height = 10;
|
||||
|
@ -1613,6 +1959,7 @@ namespace Draw {
|
|||
auto proc_left = Config::getB("proc_left");
|
||||
|
||||
Cpu::box.clear();
|
||||
|
||||
Mem::box.clear();
|
||||
Net::box.clear();
|
||||
Proc::box.clear();
|
||||
|
@ -1633,6 +1980,24 @@ namespace Draw {
|
|||
Cpu::redraw = Mem::redraw = Net::redraw = Proc::redraw = true;
|
||||
|
||||
Cpu::shown = s_contains(boxes, "cpu");
|
||||
#ifdef GPU_SUPPORT
|
||||
Gpu::box.clear();
|
||||
Gpu::width = 0;
|
||||
Gpu::shown_panels.clear();
|
||||
if (not Gpu::gpu_names.empty()) {
|
||||
std::istringstream iss(boxes, std::istringstream::in);
|
||||
string current;
|
||||
while (iss >> current)
|
||||
if ( current == "gpu0"
|
||||
or current == "gpu1"
|
||||
or current == "gpu2"
|
||||
or current == "gpu3"
|
||||
or current == "gpu4"
|
||||
or current == "gpu5"
|
||||
) Gpu::shown_panels.push_back(current.back()-'0');
|
||||
}
|
||||
Gpu::shown = Gpu::shown_panels.size();
|
||||
#endif
|
||||
Mem::shown = s_contains(boxes, "mem");
|
||||
Net::shown = s_contains(boxes, "net");
|
||||
Proc::shown = s_contains(boxes, "proc");
|
||||
|
@ -1640,13 +2005,35 @@ namespace Draw {
|
|||
//* Calculate and draw cpu box outlines
|
||||
if (Cpu::shown) {
|
||||
using namespace Cpu;
|
||||
bool show_temp = (Config::getB("check_temp") and got_sensors);
|
||||
#ifdef GPU_SUPPORT
|
||||
const bool show_gpu_on = Config::getS("show_gpu_info") == "On";
|
||||
const bool gpus_shown_in_cpu_panel = Gpu::gpu_names.size() > 0 and (
|
||||
show_gpu_on or (Config::getS("cpu_graph_lower") == "Auto" and Gpu::shown == 0)
|
||||
);
|
||||
const int gpus_height_offset = (Gpu::gpu_names.size() - Gpu::shown)*gpus_shown_in_cpu_panel;
|
||||
int gpus_extra_height = gpus_shown_in_cpu_panel ? Gpu::gpu_names.size() - (show_gpu_on ? 0 : Gpu::shown) : 0;
|
||||
#endif
|
||||
const bool show_temp = (Config::getB("check_temp") and got_sensors);
|
||||
width = round((double)Term::width * width_p / 100);
|
||||
#ifdef GPU_SUPPORT
|
||||
if (Gpu::shown != 0 and not (Mem::shown or Net::shown or Proc::shown)) {
|
||||
height = Term::height - Gpu::min_height*Gpu::shown - gpus_height_offset;
|
||||
} else {
|
||||
height = max(8, (int)ceil((double)Term::height * (trim(boxes) == "cpu" ? 100 : height_p/(Gpu::shown+1) + (Gpu::shown != 0)*5) / 100));
|
||||
}
|
||||
if (height <= Term::height-gpus_height_offset) height += gpus_height_offset;
|
||||
if (height - gpus_extra_height < 7) gpus_extra_height = height - 7;
|
||||
#else
|
||||
height = max(8, (int)ceil((double)Term::height * (trim(boxes) == "cpu" ? 100 : height_p) / 100));
|
||||
#endif
|
||||
x = 1;
|
||||
y = cpu_bottom ? Term::height - height + 1 : 1;
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - gpus_extra_height - 5)));
|
||||
#else
|
||||
b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - 5)));
|
||||
#endif
|
||||
if (b_columns * (21 + 12 * show_temp) < width - (width / 3)) {
|
||||
b_column_size = 2;
|
||||
b_width = (21 + 12 * show_temp) * b_columns - (b_columns - 1);
|
||||
|
@ -1664,7 +2051,11 @@ namespace Draw {
|
|||
}
|
||||
|
||||
if (b_column_size == 0) b_width = (8 + 6 * show_temp) * b_columns + 1;
|
||||
#ifdef GPU_SUPPORT
|
||||
b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4 + gpus_extra_height);
|
||||
#else
|
||||
b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4);
|
||||
#endif
|
||||
|
||||
b_x = x + width - b_width - 1;
|
||||
b_y = y + ceil((double)(height - 2) / 2) - ceil((double)b_height / 2) + 1;
|
||||
|
@ -1676,6 +2067,53 @@ namespace Draw {
|
|||
box += createBox(b_x, b_y, b_width, b_height, "", false, cpu_title);
|
||||
}
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
//* Calculate and draw gpu box outlines
|
||||
if (Gpu::shown != 0) {
|
||||
using namespace Gpu;
|
||||
x_vec.resize(shown); y_vec.resize(shown);
|
||||
b_x_vec.resize(shown); b_y_vec.resize(shown);
|
||||
b_height_vec.resize(shown);
|
||||
box.resize(shown);
|
||||
graph_upper_vec.resize(shown); graph_lower_vec.resize(shown);
|
||||
temp_graph_vec.resize(shown);
|
||||
mem_used_graph_vec.resize(shown); mem_util_graph_vec.resize(shown);
|
||||
gpu_meter_vec.resize(shown);
|
||||
pwr_meter_vec.resize(shown);
|
||||
redraw.resize(shown);
|
||||
for (auto i = 0; i < shown; ++i) {
|
||||
redraw[i] = true;
|
||||
|
||||
width = Term::width;
|
||||
if (Cpu::shown)
|
||||
if (not (Mem::shown or Net::shown or Proc::shown))
|
||||
height = min_height;
|
||||
else height = Cpu::height;
|
||||
else
|
||||
if (not (Mem::shown or Net::shown or Proc::shown))
|
||||
height = Term::height/Gpu::shown + (i == 0)*(Term::height%Gpu::shown);
|
||||
else
|
||||
height = max(min_height, (int)ceil((double)Term::height * height_p/Gpu::shown / 100));
|
||||
|
||||
height += (height+Cpu::height == Term::height-1);
|
||||
x_vec[i] = 1; y_vec[i] = 1 + i*height + (not Config::getB("cpu_bottom"))*Cpu::shown*Cpu::height;
|
||||
box[i] = createBox(x_vec[i], y_vec[i], width, height, Theme::c("cpu_box"), true, std::string("gpu") + (char)(shown_panels[i]+'0'), "", (shown_panels[i]+5)%10); // TODO gpu_box
|
||||
|
||||
b_height_vec[i] = 2 + gpu_b_height_offsets[shown_panels[i]];
|
||||
b_width = clamp(width/2, min_width, 64);
|
||||
|
||||
//? Main statistics box
|
||||
b_x_vec[i] = x_vec[i] + width - b_width - 1;
|
||||
b_y_vec[i] = y_vec[i] + ceil((double)(height - 2) / 2) - ceil((double)(b_height_vec[i]) / 2) + 1;
|
||||
|
||||
string name = Config::getS(std::string("custom_gpu_name") + (char)(shown_panels[i]+'0'));
|
||||
if (name.empty()) name = gpu_names[shown_panels[i]];
|
||||
|
||||
box[i] += createBox(b_x_vec[i], b_y_vec[i], b_width, b_height_vec[i], "", false, name.substr(0, b_width-5));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
//* Calculate and draw mem box outlines
|
||||
if (Mem::shown) {
|
||||
using namespace Mem;
|
||||
|
@ -1684,13 +2122,22 @@ namespace Draw {
|
|||
auto mem_graphs = Config::getB("mem_graphs");
|
||||
|
||||
width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100);
|
||||
#ifdef GPU_SUPPORT
|
||||
height = ceil((double)Term::height * (100 - Net::height_p * Net::shown*4 / ((Gpu::shown != 0 and Cpu::shown) + 4)) / 100) - Cpu::height - Gpu::height*Gpu::shown;
|
||||
#else
|
||||
height = ceil((double)Term::height * (100 - Cpu::height_p * Cpu::shown - Net::height_p * Net::shown) / 100) + 1;
|
||||
if (height + Cpu::height > Term::height) height = Term::height - Cpu::height;
|
||||
#endif
|
||||
x = (proc_left and Proc::shown) ? Term::width - width + 1: 1;
|
||||
if (mem_below_net and Net::shown)
|
||||
#ifdef GPU_SUPPORT
|
||||
y = Term::height - height + 1 - (cpu_bottom ? Cpu::height + Gpu::height*Gpu::shown : 0);
|
||||
else
|
||||
y = cpu_bottom ? 1 : Cpu::height + Gpu::height*Gpu::shown + 1;
|
||||
#else
|
||||
y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0);
|
||||
else
|
||||
y = cpu_bottom ? 1 : Cpu::height + 1;
|
||||
#endif
|
||||
|
||||
if (show_disks) {
|
||||
mem_width = ceil((double)(width - 3) / 2);
|
||||
|
@ -1739,10 +2186,18 @@ namespace Draw {
|
|||
if (Net::shown) {
|
||||
using namespace Net;
|
||||
width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100);
|
||||
#ifdef GPU_SUPPORT
|
||||
height = Term::height - Cpu::height - Gpu::height*Gpu::shown - Mem::height;
|
||||
#else
|
||||
height = Term::height - Cpu::height - Mem::height;
|
||||
#endif
|
||||
x = (proc_left and Proc::shown) ? Term::width - width + 1 : 1;
|
||||
if (mem_below_net and Mem::shown)
|
||||
#ifdef GPU_SUPPORT
|
||||
y = cpu_bottom ? 1 : Cpu::height + Gpu::height*Gpu::shown + 1;
|
||||
#else
|
||||
y = cpu_bottom ? 1 : Cpu::height + 1;
|
||||
#endif
|
||||
else
|
||||
y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0);
|
||||
|
||||
|
@ -1761,9 +2216,17 @@ namespace Draw {
|
|||
if (Proc::shown) {
|
||||
using namespace Proc;
|
||||
width = Term::width - (Mem::shown ? Mem::width : (Net::shown ? Net::width : 0));
|
||||
#ifdef GPU_SUPPORT
|
||||
height = Term::height - Cpu::height - Gpu::height*Gpu::shown;
|
||||
#else
|
||||
height = Term::height - Cpu::height;
|
||||
#endif
|
||||
x = proc_left ? 1 : Term::width - width + 1;
|
||||
#ifdef GPU_SUPPORT
|
||||
y = (cpu_bottom and Cpu::shown) ? 1 : Cpu::height + Gpu::height*Gpu::shown + 1;
|
||||
#else
|
||||
y = (cpu_bottom and Cpu::shown) ? 1 : Cpu::height + 1;
|
||||
#endif
|
||||
select_max = height - 3;
|
||||
box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,15 @@ tab-size = 4
|
|||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <signal.h>
|
||||
#include <utility>
|
||||
|
||||
#include "btop_input.hpp"
|
||||
#include "btop_tools.hpp"
|
||||
#include "btop_config.hpp"
|
||||
#include "btop_shared.hpp"
|
||||
#include "btop_menu.hpp"
|
||||
#include "btop_draw.hpp"
|
||||
|
||||
|
||||
#include "btop_input.hpp"
|
||||
#include "btop_tools.hpp"
|
||||
|
@ -264,11 +273,21 @@ namespace Input {
|
|||
Menu::show(Menu::Menus::Options);
|
||||
return;
|
||||
}
|
||||
else if (is_in(key, "1", "2", "3", "4")) {
|
||||
else if (key.size() == 1 and isint(key)) {
|
||||
auto intKey = stoi(key);
|
||||
#ifdef GPU_SUPPORT
|
||||
static const array<string, 10> boxes = {"gpu5", "cpu", "mem", "net", "proc", "gpu0", "gpu1", "gpu2", "gpu3", "gpu4"};
|
||||
if ((intKey == 0 and Gpu::gpu_names.size() < 5) or (intKey >= 5 and std::cmp_less(Gpu::gpu_names.size(), intKey - 4)))
|
||||
return;
|
||||
#else
|
||||
static const array<string, 10> boxes = {"", "cpu", "mem", "net", "proc"};
|
||||
if (intKey == 0 or intKey > 4)
|
||||
return;
|
||||
#endif
|
||||
atomic_wait(Runner::active);
|
||||
Config::current_preset = -1;
|
||||
static const array<string, 4> boxes = {"cpu", "mem", "net", "proc"};
|
||||
Config::toggle_box(boxes.at(std::stoi(key) - 1));
|
||||
|
||||
Config::toggle_box(boxes.at(intKey));
|
||||
Draw::calcSizes();
|
||||
Runner::run("all", false, true);
|
||||
return;
|
||||
|
|
|
@ -173,6 +173,7 @@ namespace Menu {
|
|||
{"2", "Toggle MEM box."},
|
||||
{"3", "Toggle NET box."},
|
||||
{"4", "Toggle PROC box."},
|
||||
{"5", "Toggle GPU box."},
|
||||
{"d", "Toggle disks view in MEM box."},
|
||||
{"F2, o", "Shows options."},
|
||||
{"F1, ?, h", "Shows this window."},
|
||||
|
@ -271,6 +272,9 @@ namespace Menu {
|
|||
"Manually set which boxes to show.",
|
||||
"",
|
||||
"Available values are \"cpu mem net proc\".",
|
||||
#ifdef GPU_SUPPORT
|
||||
"Or \"gpu0\" through \"gpu5\" for GPU boxes.",
|
||||
#endif
|
||||
"Separate values with whitespace.",
|
||||
"",
|
||||
"Toggle between presets with key \"p\"."},
|
||||
|
@ -373,23 +377,49 @@ namespace Menu {
|
|||
{"cpu_graph_upper",
|
||||
"Cpu upper graph.",
|
||||
"",
|
||||
"Sets the CPU stat shown in upper half of",
|
||||
"Sets the CPU/GPU stat shown in upper half of",
|
||||
"the CPU graph.",
|
||||
"",
|
||||
"\"total\" = Total cpu usage.",
|
||||
"CPU:",
|
||||
"\"total\" = Total cpu usage. (Auto)",
|
||||
"\"user\" = User mode cpu usage.",
|
||||
"\"system\" = Kernel mode cpu usage.",
|
||||
"+ more depending on kernel."},
|
||||
"+ more depending on kernel.",
|
||||
#ifdef GPU_SUPPORT
|
||||
"",
|
||||
"GPU:",
|
||||
"\"gpu-totals\" = GPU usage split by device.",
|
||||
"\"gpu-vram-totals\" = VRAM usage split by GPU.",
|
||||
"\"gpu-pwr-totals\" = Power usage split by GPU.",
|
||||
"\"gpu-average\" = Avg usage of all GPUs.",
|
||||
"\"gpu-vram-total\" = VRAM usage of all GPUs.",
|
||||
"\"gpu-pwr-total\" = Power usage of all GPUs.",
|
||||
"Not all stats are supported on all devices."
|
||||
#endif
|
||||
},
|
||||
{"cpu_graph_lower",
|
||||
"Cpu lower graph.",
|
||||
"",
|
||||
"Sets the CPU stat shown in lower half of",
|
||||
"Sets the CPU/GPU stat shown in lower half of",
|
||||
"the CPU graph.",
|
||||
"",
|
||||
"CPU:",
|
||||
"\"total\" = Total cpu usage.",
|
||||
"\"user\" = User mode cpu usage.",
|
||||
"\"system\" = Kernel mode cpu usage.",
|
||||
"+ more depending on kernel."},
|
||||
"+ more depending on kernel.",
|
||||
#ifdef GPU_SUPPORT
|
||||
"",
|
||||
"GPU:",
|
||||
"\"gpu-totals\" = GPU usage split/device. (Auto)",
|
||||
"\"gpu-vram-totals\" = VRAM usage split by GPU.",
|
||||
"\"gpu-pwr-totals\" = Power usage split by GPU.",
|
||||
"\"gpu-average\" = Avg usage of all GPUs.",
|
||||
"\"gpu-vram-total\" = VRAM usage of all GPUs.",
|
||||
"\"gpu-pwr-total\" = Power usage of all GPUs.",
|
||||
"Not all stats are supported on all devices."
|
||||
#endif
|
||||
},
|
||||
{"cpu_invert_lower",
|
||||
"Toggles orientation of the lower CPU graph.",
|
||||
"",
|
||||
|
@ -401,12 +431,24 @@ namespace Menu {
|
|||
"to fit to box height.",
|
||||
"",
|
||||
"True or False."},
|
||||
#ifdef GPU_SUPPORT
|
||||
{"show_gpu_info",
|
||||
"Show gpu info in cpu box.",
|
||||
"",
|
||||
"Toggles gpu stats in cpu box and the",
|
||||
"gpu graph (if \"cpu_graph_lower\" is set to",
|
||||
"\"Auto\").",
|
||||
"",
|
||||
"\"Auto\" to show when no gpu box is shown.",
|
||||
"\"On\" to always show.",
|
||||
"\"Off\" to never show."},
|
||||
#endif
|
||||
{"check_temp",
|
||||
"Enable cpu temperature reporting.",
|
||||
"",
|
||||
"True or False."},
|
||||
{"cpu_sensor",
|
||||
"Cpu temperature sensor",
|
||||
"Cpu temperature sensor.",
|
||||
"",
|
||||
"Select the sensor that corresponds to",
|
||||
"your cpu temperature.",
|
||||
|
@ -446,7 +488,7 @@ namespace Menu {
|
|||
"Rankine, 0 = abosulte zero, 1 degree change",
|
||||
"equals 1 degree change in Fahrenheit."},
|
||||
{"show_cpu_freq",
|
||||
"Show CPU frequency",
|
||||
"Show CPU frequency.",
|
||||
"",
|
||||
"Can cause slowdowns on systems with many",
|
||||
"cores and certain kernel versions."},
|
||||
|
@ -462,6 +504,50 @@ namespace Menu {
|
|||
"",
|
||||
"True or False."},
|
||||
},
|
||||
#ifdef GPU_SUPPORT
|
||||
{
|
||||
{"nvml_measure_pcie_speeds",
|
||||
"Measure PCIe throughput on NVIDIA cards.",
|
||||
"",
|
||||
"May impact performance on certain cards.",
|
||||
"",
|
||||
"True or False."},
|
||||
{"graph_symbol_gpu",
|
||||
"Graph symbol to use for graphs in gpu box.",
|
||||
"",
|
||||
"\"default\", \"braille\", \"block\" or \"tty\".",
|
||||
"",
|
||||
"\"default\" for the general default symbol.",},
|
||||
{"gpu_mirror_graph",
|
||||
"Horizontally mirror the GPU graph.",
|
||||
"",
|
||||
"True or False."},
|
||||
{"custom_gpu_name0",
|
||||
"Custom gpu0 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
{"custom_gpu_name1",
|
||||
"Custom gpu1 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
{"custom_gpu_name2",
|
||||
"Custom gpu2 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
{"custom_gpu_name3",
|
||||
"Custom gpu3 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
{"custom_gpu_name4",
|
||||
"Custom gpu4 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
{"custom_gpu_name5",
|
||||
"Custom gpu5 model name in gpu stats box.",
|
||||
"",
|
||||
"Empty string to disable."},
|
||||
},
|
||||
#endif
|
||||
{
|
||||
{"mem_below_net",
|
||||
"Mem box location.",
|
||||
|
@ -1093,6 +1179,10 @@ namespace Menu {
|
|||
{"cpu_graph_lower", std::cref(Cpu::available_fields)},
|
||||
{"cpu_sensor", std::cref(Cpu::available_sensors)},
|
||||
{"selected_battery", std::cref(Config::available_batteries)},
|
||||
#ifdef GPU_SUPPORT
|
||||
{"show_gpu_info", std::cref(Config::show_gpu_values)},
|
||||
{"graph_symbol_gpu", std::cref(Config::valid_graph_symbols_def)},
|
||||
#endif
|
||||
};
|
||||
auto tty_mode = Config::getB("tty_mode");
|
||||
auto vim_keys = Config::getB("vim_keys");
|
||||
|
@ -1144,7 +1234,8 @@ namespace Menu {
|
|||
const auto& option = categories[selected_cat][item_height * page + selected][0];
|
||||
if (selPred.test(isString) and Config::stringValid(option, editor.text)) {
|
||||
Config::set(option, editor.text);
|
||||
if (option == "custom_cpu_name") screen_redraw = true;
|
||||
if (option == "custom_cpu_name" or option.starts_with("custom_gpu_name"))
|
||||
screen_redraw = true;
|
||||
else if (is_in(option, "shown_boxes", "presets")) {
|
||||
screen_redraw = true;
|
||||
Config::current_preset = -1;
|
||||
|
@ -1225,7 +1316,7 @@ namespace Menu {
|
|||
if (--selected_cat < 0) selected_cat = (int)categories.size() - 1;
|
||||
page = selected = 0;
|
||||
}
|
||||
else if (is_in(key, "1", "2", "3", "4", "5") or key.starts_with("select_cat_")) {
|
||||
else if (is_in(key, "1", "2", "3", "4", "5", "6") or key.starts_with("select_cat_")) {
|
||||
selected_cat = key.back() - '0' - 1;
|
||||
page = selected = 0;
|
||||
}
|
||||
|
@ -1277,7 +1368,7 @@ namespace Menu {
|
|||
Logger::set(optList.at(i));
|
||||
Logger::info("Logger set to " + optList.at(i));
|
||||
}
|
||||
else if (is_in(option, "proc_sorting", "cpu_sensor") or option.starts_with("graph_symbol") or option.starts_with("cpu_graph_"))
|
||||
else if (is_in(option, "proc_sorting", "cpu_sensor", "show_gpu_info") or option.starts_with("graph_symbol") or option.starts_with("cpu_graph_"))
|
||||
screen_redraw = true;
|
||||
}
|
||||
else
|
||||
|
@ -1323,11 +1414,19 @@ namespace Menu {
|
|||
|
||||
//? Category buttons
|
||||
out += Mv::to(y+7, x+4);
|
||||
#ifdef GPU_SUPPORT
|
||||
for (int i = 0; const auto& m : {"general", "cpu", "gpu", "mem", "net", "proc"}) {
|
||||
#else
|
||||
for (int i = 0; const auto& m : {"general", "cpu", "mem", "net", "proc"}) {
|
||||
#endif
|
||||
out += Fx::b + (i == selected_cat
|
||||
? Theme::c("hi_fg") + '[' + Theme::c("title") + m + Theme::c("hi_fg") + ']'
|
||||
: Theme::c("hi_fg") + to_string(i + 1) + Theme::c("title") + m + ' ')
|
||||
#ifdef GPU_SUPPORT
|
||||
+ Mv::r(7);
|
||||
#else
|
||||
+ Mv::r(10);
|
||||
#endif
|
||||
if (string button_name = "select_cat_" + to_string(i + 1); not editing and not mouse_mappings.contains(button_name))
|
||||
mouse_mappings[button_name] = {y+6, x+2 + 15*i, 3, 15};
|
||||
i++;
|
||||
|
|
|
@ -25,6 +25,18 @@ tab-size = 4
|
|||
namespace rng = std::ranges;
|
||||
using namespace Tools;
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
namespace Gpu {
|
||||
vector<string> gpu_names;
|
||||
vector<int> gpu_b_height_offsets;
|
||||
unordered_flat_map<string, deque<long long>> shared_gpu_percent = {
|
||||
{"gpu-average", {}},
|
||||
{"gpu-vram-total", {}},
|
||||
{"gpu-pwr-total", {}},
|
||||
};
|
||||
long long gpu_pwr_total_max;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Proc {
|
||||
void proc_sorter(vector<proc_info>& proc_vec, const string& sorting, bool reverse, bool tree) {
|
||||
|
|
|
@ -86,6 +86,91 @@ namespace Shared {
|
|||
}
|
||||
|
||||
|
||||
namespace Gpu {
|
||||
#ifdef GPU_SUPPORT
|
||||
extern vector<string> box;
|
||||
extern int width, height, min_width, min_height;
|
||||
extern vector<int> x_vec, y_vec;
|
||||
extern vector<bool> redraw;
|
||||
extern int shown;
|
||||
extern vector<char> shown_panels;
|
||||
extern vector<string> gpu_names;
|
||||
extern vector<int> gpu_b_height_offsets;
|
||||
extern long long gpu_pwr_total_max;
|
||||
|
||||
extern unordered_flat_map<string, deque<long long>> shared_gpu_percent; // averages, power/vram total
|
||||
|
||||
const array mem_names { "used"s, "free"s };
|
||||
|
||||
//* Container for process information // TODO
|
||||
/*struct proc_info {
|
||||
unsigned int pid;
|
||||
unsigned long long mem;
|
||||
};*/
|
||||
|
||||
//* Container for supported Gpu::*::collect() functions
|
||||
struct gpu_info_supported {
|
||||
bool gpu_utilization = true,
|
||||
mem_utilization = true,
|
||||
gpu_clock = true,
|
||||
mem_clock = true,
|
||||
pwr_usage = true,
|
||||
pwr_state = true,
|
||||
temp_info = true,
|
||||
mem_total = true,
|
||||
mem_used = true,
|
||||
pcie_txrx = true;
|
||||
};
|
||||
|
||||
//* Per-device container for GPU info
|
||||
struct gpu_info {
|
||||
unordered_flat_map<string, deque<long long>> gpu_percent = {
|
||||
{"gpu-totals", {}},
|
||||
{"gpu-vram-totals", {}},
|
||||
{"gpu-pwr-totals", {}},
|
||||
};
|
||||
unsigned int gpu_clock_speed; // MHz
|
||||
|
||||
long long pwr_usage; // mW
|
||||
long long pwr_max_usage = 255000;
|
||||
long long pwr_state;
|
||||
|
||||
deque<long long> temp = {0};
|
||||
long long temp_max = 110;
|
||||
|
||||
long long mem_total = 0;
|
||||
long long mem_used = 0;
|
||||
deque<long long> mem_utilization_percent = {0}; // TODO: properly handle GPUs that can't report some stats
|
||||
long long mem_clock_speed = 0; // MHz
|
||||
|
||||
long long pcie_tx = 0; // KB/s
|
||||
long long pcie_rx = 0;
|
||||
|
||||
gpu_info_supported supported_functions;
|
||||
|
||||
// vector<proc_info> graphics_processes = {}; // TODO
|
||||
// vector<proc_info> compute_processes = {};
|
||||
};
|
||||
|
||||
namespace Nvml {
|
||||
extern bool shutdown();
|
||||
}
|
||||
namespace Rsmi {
|
||||
extern bool shutdown();
|
||||
}
|
||||
|
||||
//* Collect gpu stats and temperatures
|
||||
auto collect(bool no_update = false) -> vector<gpu_info>&;
|
||||
|
||||
//* Draw contents of gpu box using <gpus> as source
|
||||
string draw(const gpu_info& gpu, unsigned long index, bool force_redraw, bool data_same);
|
||||
#else
|
||||
struct gpu_info {
|
||||
bool supported = false;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace Cpu {
|
||||
extern string box;
|
||||
extern int x, y, width, height, min_width, min_height;
|
||||
|
@ -119,7 +204,7 @@ namespace Cpu {
|
|||
auto collect(bool no_update = false) -> cpu_info&;
|
||||
|
||||
//* Draw contents of cpu box using <cpu> as source
|
||||
string draw(const cpu_info& cpu, bool force_redraw = false, bool data_same = false);
|
||||
string draw(const cpu_info& cpu, const vector<Gpu::gpu_info>& gpu, bool force_redraw = false, bool data_same = false);
|
||||
|
||||
//* Parse /proc/cpu info for mapping of core ids
|
||||
auto get_core_mapping() -> unordered_flat_map<int, int>;
|
||||
|
|
|
@ -99,19 +99,31 @@ namespace Term {
|
|||
}
|
||||
|
||||
auto get_min_size(const string& boxes) -> array<int, 2> {
|
||||
bool cpu = boxes.find("cpu") != string::npos;
|
||||
bool mem = boxes.find("mem") != string::npos;
|
||||
bool net = boxes.find("net") != string::npos;
|
||||
bool proc = boxes.find("proc") != string::npos;
|
||||
int width = 0;
|
||||
bool cpu = boxes.find("cpu") != string::npos;
|
||||
bool mem = boxes.find("mem") != string::npos;
|
||||
bool net = boxes.find("net") != string::npos;
|
||||
bool proc = boxes.find("proc") != string::npos;
|
||||
#ifdef GPU_SUPPORT
|
||||
int gpu = 0;
|
||||
if (not Gpu::gpu_names.empty())
|
||||
for (char i = '0'; i <= '5'; ++i)
|
||||
gpu += (boxes.find(std::string("gpu") + i) != string::npos);
|
||||
#endif
|
||||
int width = 0;
|
||||
if (mem) width = Mem::min_width;
|
||||
else if (net) width = Mem::min_width;
|
||||
width += (proc ? Proc::min_width : 0);
|
||||
if (cpu and width < Cpu::min_width) width = Cpu::min_width;
|
||||
#ifdef GPU_SUPPORT
|
||||
if (gpu != 0 and width < Gpu::min_width) width = Gpu::min_width;
|
||||
#endif
|
||||
|
||||
int height = (cpu ? Cpu::min_height : 0);
|
||||
if (proc) height += Proc::min_height;
|
||||
else height += (mem ? Mem::min_height : 0) + (net ? Net::min_height : 0);
|
||||
#ifdef GPU_SUPPORT
|
||||
height += Gpu::min_height*gpu;
|
||||
#endif
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
@ -535,6 +547,74 @@ namespace Tools {
|
|||
return (user != nullptr ? user : "");
|
||||
}
|
||||
|
||||
DebugTimer::DebugTimer(const string name, bool start, bool delayed_report) : name(name), delayed_report(delayed_report) {
|
||||
if (start)
|
||||
this->start();
|
||||
}
|
||||
|
||||
DebugTimer::~DebugTimer() {
|
||||
if (running)
|
||||
this->stop(true);
|
||||
this->force_report();
|
||||
}
|
||||
|
||||
void DebugTimer::start() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
start_time = time_micros();
|
||||
}
|
||||
|
||||
void DebugTimer::stop(bool report) {
|
||||
if (not running) return;
|
||||
running = false;
|
||||
elapsed_time = time_micros() - start_time;
|
||||
if (report) this->report();
|
||||
}
|
||||
|
||||
void DebugTimer::reset(bool restart) {
|
||||
running = false;
|
||||
start_time = 0;
|
||||
elapsed_time = 0;
|
||||
if (restart) this->start();
|
||||
}
|
||||
|
||||
void DebugTimer::stop_rename_reset(const string &new_name, bool report, bool restart) {
|
||||
this->stop(report);
|
||||
name = new_name;
|
||||
this->reset(restart);
|
||||
}
|
||||
|
||||
void DebugTimer::report() {
|
||||
string report_line;
|
||||
if (start_time == 0 and elapsed_time == 0)
|
||||
report_line = fmt::format("DebugTimer::report() warning -> Timer [{}] has not been started!", name);
|
||||
else if (running)
|
||||
report_line = fmt::format(custom_locale, "Timer [{}] (running) currently at {:L} μs", name, time_micros() - start_time);
|
||||
else
|
||||
report_line = fmt::format(custom_locale, "Timer [{}] took {:L} μs", name, elapsed_time);
|
||||
|
||||
if (delayed_report)
|
||||
report_buffer.emplace_back(report_line);
|
||||
else
|
||||
Logger::log_write(log_level, report_line);
|
||||
}
|
||||
|
||||
void DebugTimer::force_report() {
|
||||
if (report_buffer.empty()) return;
|
||||
for (const auto& line : report_buffer)
|
||||
Logger::log_write(log_level, line);
|
||||
report_buffer.clear();
|
||||
}
|
||||
|
||||
uint64_t DebugTimer::elapsed() {
|
||||
if (running)
|
||||
return time_micros() - start_time;
|
||||
return elapsed_time;
|
||||
}
|
||||
|
||||
bool DebugTimer::is_running() {
|
||||
return running;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Logger {
|
||||
|
@ -566,7 +646,7 @@ namespace Logger {
|
|||
loglevel = v_index(log_levels, level);
|
||||
}
|
||||
|
||||
void log_write(const size_t level, const string& msg) {
|
||||
void log_write(const Level level, const string& msg) {
|
||||
if (loglevel < level or logfile.empty()) return;
|
||||
atomic_lock lck(busy, true);
|
||||
lose_priv neutered{};
|
||||
|
|
|
@ -151,6 +151,12 @@ namespace Term {
|
|||
namespace Tools {
|
||||
constexpr auto SSmax = std::numeric_limits<std::streamsize>::max();
|
||||
|
||||
class MyNumPunct : public std::numpunct<char> {
|
||||
protected:
|
||||
virtual char do_thousands_sep() const { return '\''; }
|
||||
virtual std::string do_grouping() const { return "\03"; }
|
||||
};
|
||||
|
||||
size_t wide_ulen(const string& str);
|
||||
size_t wide_ulen(const std::wstring& w_str);
|
||||
|
||||
|
@ -334,7 +340,6 @@ namespace Tools {
|
|||
|
||||
//* Convert a celsius value to celsius, fahrenheit, kelvin or rankin and return tuple with new value and unit.
|
||||
auto celsius_to(const long long& celsius, const string& scale) -> tuple<long long, string>;
|
||||
|
||||
}
|
||||
|
||||
//* Simple logging implementation
|
||||
|
@ -348,13 +353,56 @@ namespace Logger {
|
|||
};
|
||||
extern std::filesystem::path logfile;
|
||||
|
||||
enum Level : size_t {
|
||||
DISABLED = 0,
|
||||
ERROR = 1,
|
||||
WARNING = 2,
|
||||
INFO = 3,
|
||||
DEBUG = 4,
|
||||
};
|
||||
|
||||
//* Set log level, valid arguments: "DISABLED", "ERROR", "WARNING", "INFO" and "DEBUG"
|
||||
void set(const string& level);
|
||||
|
||||
void log_write(const size_t level, const string& msg);
|
||||
inline void error(const string msg) { log_write(1, msg); }
|
||||
inline void warning(const string msg) { log_write(2, msg); }
|
||||
inline void info(const string msg) { log_write(3, msg); }
|
||||
inline void debug(const string msg) { log_write(4, msg); }
|
||||
void log_write(const Level level, const string& msg);
|
||||
inline void error(const string msg) { log_write(ERROR, msg); }
|
||||
inline void warning(const string msg) { log_write(WARNING, msg); }
|
||||
inline void info(const string msg) { log_write(INFO, msg); }
|
||||
inline void debug(const string msg) { log_write(DEBUG, msg); }
|
||||
}
|
||||
|
||||
namespace Tools {
|
||||
//* Creates a named timer that is started on construct (by default) and reports elapsed time in microseconds to Logger::debug() on destruct if running
|
||||
//* Unless delayed_report is set to false, all reporting is buffered and delayed until DebugTimer is destructed or .force_report() is called
|
||||
//* Usage example: Tools::DebugTimer timer(name:"myTimer", [start:true], [delayed_report:true]) // Create timer and start
|
||||
//* timer.stop(); // Stop timer and report elapsed time
|
||||
//* timer.stop_rename_reset("myTimer2"); // Stop timer, report elapsed time, rename timer, reset and restart
|
||||
class DebugTimer {
|
||||
uint64_t start_time{};
|
||||
uint64_t elapsed_time{};
|
||||
bool running{};
|
||||
std::locale custom_locale = std::locale(std::locale::classic(), new Tools::MyNumPunct);
|
||||
vector<string> report_buffer{};
|
||||
public:
|
||||
string name{};
|
||||
bool delayed_report{};
|
||||
Logger::Level log_level = Logger::DEBUG;
|
||||
DebugTimer() = default;
|
||||
DebugTimer(const string name, bool start = true, bool delayed_report = true);
|
||||
~DebugTimer();
|
||||
|
||||
void start();
|
||||
void stop(bool report = true);
|
||||
void reset(bool restart = true);
|
||||
//* Stops and reports (default), renames timer then resets and restarts (default)
|
||||
void stop_rename_reset(const string& new_name, bool report = true, bool restart = true);
|
||||
void report();
|
||||
void force_report();
|
||||
uint64_t elapsed();
|
||||
bool is_running();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ using namespace Tools;
|
|||
namespace Cpu {
|
||||
vector<long long> core_old_totals;
|
||||
vector<long long> core_old_idles;
|
||||
vector<string> available_fields = {"total"};
|
||||
vector<string> available_fields = {"Auto", "total"};
|
||||
vector<string> available_sensors = {"Auto"};
|
||||
cpu_info current_cpu;
|
||||
bool got_sensors = false, cpu_temp_only = false;
|
||||
|
|
|
@ -30,6 +30,11 @@ tab-size = 4
|
|||
#include <arpa/inet.h> // for inet_ntop()
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#if defined(RSMI_STATIC)
|
||||
#include <rocm_smi/rocm_smi.h>
|
||||
#endif
|
||||
|
||||
#if !(defined(STATIC_BUILD) && defined(__GLIBC__))
|
||||
#include <pwd.h>
|
||||
|
@ -65,7 +70,7 @@ using namespace std::chrono_literals;
|
|||
namespace Cpu {
|
||||
vector<long long> core_old_totals;
|
||||
vector<long long> core_old_idles;
|
||||
vector<string> available_fields;
|
||||
vector<string> available_fields = {"Auto", "total"};
|
||||
vector<string> available_sensors = {"Auto"};
|
||||
cpu_info current_cpu;
|
||||
fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq";
|
||||
|
@ -95,6 +100,106 @@ namespace Cpu {
|
|||
unordered_flat_map<int, int> core_mapping;
|
||||
}
|
||||
|
||||
namespace Gpu {
|
||||
vector<gpu_info> gpus;
|
||||
#ifdef GPU_SUPPORT
|
||||
//? NVIDIA data collection
|
||||
namespace Nvml {
|
||||
//? NVML defines, structs & typedefs
|
||||
#define NVML_DEVICE_NAME_BUFFER_SIZE 64
|
||||
#define NVML_SUCCESS 0
|
||||
#define NVML_TEMPERATURE_THRESHOLD_SHUTDOWN 0
|
||||
#define NVML_CLOCK_GRAPHICS 0
|
||||
#define NVML_CLOCK_MEM 2
|
||||
#define NVML_TEMPERATURE_GPU 0
|
||||
#define NVML_PCIE_UTIL_TX_BYTES 0
|
||||
#define NVML_PCIE_UTIL_RX_BYTES 1
|
||||
|
||||
typedef void* nvmlDevice_t; // we won't be accessing any of the underlying struct's properties, so this is fine
|
||||
typedef int nvmlReturn_t, // enums are basically ints
|
||||
nvmlTemperatureThresholds_t,
|
||||
nvmlClockType_t,
|
||||
nvmlPstates_t,
|
||||
nvmlTemperatureSensors_t,
|
||||
nvmlPcieUtilCounter_t;
|
||||
|
||||
struct nvmlUtilization_t {unsigned int gpu, memory;};
|
||||
struct nvmlMemory_t {unsigned long long total, free, used;};
|
||||
|
||||
//? Function pointers
|
||||
const char* (*nvmlErrorString)(nvmlReturn_t);
|
||||
nvmlReturn_t (*nvmlInit)();
|
||||
nvmlReturn_t (*nvmlShutdown)();
|
||||
nvmlReturn_t (*nvmlDeviceGetCount)(unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetHandleByIndex)(unsigned int, nvmlDevice_t*);
|
||||
nvmlReturn_t (*nvmlDeviceGetName)(nvmlDevice_t, char*, unsigned int);
|
||||
nvmlReturn_t (*nvmlDeviceGetPowerManagementLimit)(nvmlDevice_t, unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetTemperatureThreshold)(nvmlDevice_t, nvmlTemperatureThresholds_t, unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetUtilizationRates)(nvmlDevice_t, nvmlUtilization_t*);
|
||||
nvmlReturn_t (*nvmlDeviceGetClockInfo)(nvmlDevice_t, nvmlClockType_t, unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetPowerUsage)(nvmlDevice_t, unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetPowerState)(nvmlDevice_t, nvmlPstates_t*);
|
||||
nvmlReturn_t (*nvmlDeviceGetTemperature)(nvmlDevice_t, nvmlTemperatureSensors_t, unsigned int*);
|
||||
nvmlReturn_t (*nvmlDeviceGetMemoryInfo)(nvmlDevice_t, nvmlMemory_t*);
|
||||
nvmlReturn_t (*nvmlDeviceGetPcieThroughput)(nvmlDevice_t, nvmlPcieUtilCounter_t, unsigned int*);
|
||||
|
||||
//? Data
|
||||
void* nvml_dl_handle;
|
||||
bool initialized = false;
|
||||
bool init();
|
||||
bool shutdown();
|
||||
template <bool is_init> bool collect(gpu_info* gpus_slice);
|
||||
vector<nvmlDevice_t> devices;
|
||||
unsigned int device_count = 0;
|
||||
}
|
||||
|
||||
//? AMD data collection
|
||||
namespace Rsmi {
|
||||
#if !defined(RSMI_STATIC)
|
||||
//? RSMI defines, structs & typedefs
|
||||
#define RSMI_MAX_NUM_FREQUENCIES 32
|
||||
#define RSMI_STATUS_SUCCESS 0
|
||||
#define RSMI_MEM_TYPE_VRAM 0
|
||||
#define RSMI_TEMP_CURRENT 0
|
||||
#define RSMI_TEMP_TYPE_EDGE 0
|
||||
#define RSMI_CLK_TYPE_MEM 4
|
||||
#define RSMI_CLK_TYPE_SYS 0
|
||||
#define RSMI_TEMP_MAX 1
|
||||
|
||||
typedef int rsmi_status_t,
|
||||
rsmi_temperature_metric_t,
|
||||
rsmi_clk_type_t,
|
||||
rsmi_memory_type_t;
|
||||
|
||||
struct rsmi_frequencies_t {uint32_t num_supported, current, frequency[RSMI_MAX_NUM_FREQUENCIES];};
|
||||
|
||||
//? Function pointers
|
||||
rsmi_status_t (*rsmi_init)(uint64_t);
|
||||
rsmi_status_t (*rsmi_shut_down)();
|
||||
rsmi_status_t (*rsmi_num_monitor_devices)(uint32_t*);
|
||||
rsmi_status_t (*rsmi_dev_name_get)(uint32_t, char*, size_t);
|
||||
rsmi_status_t (*rsmi_dev_power_cap_get)(uint32_t, uint32_t, uint64_t*);
|
||||
rsmi_status_t (*rsmi_dev_temp_metric_get)(uint32_t, uint32_t, rsmi_temperature_metric_t, int64_t*);
|
||||
rsmi_status_t (*rsmi_dev_busy_percent_get)(uint32_t, uint32_t*);
|
||||
rsmi_status_t (*rsmi_dev_memory_busy_percent_get)(uint32_t, uint32_t*);
|
||||
rsmi_status_t (*rsmi_dev_gpu_clk_freq_get)(uint32_t, rsmi_clk_type_t, rsmi_frequencies_t*);
|
||||
rsmi_status_t (*rsmi_dev_power_ave_get)(uint32_t, uint32_t, uint64_t*);
|
||||
rsmi_status_t (*rsmi_dev_memory_total_get)(uint32_t, rsmi_memory_type_t, uint64_t*);
|
||||
rsmi_status_t (*rsmi_dev_memory_usage_get)(uint32_t, rsmi_memory_type_t, uint64_t*);
|
||||
rsmi_status_t (*rsmi_dev_pci_throughput_get)(uint32_t, uint64_t*, uint64_t*, uint64_t*);
|
||||
|
||||
//? Data
|
||||
void* rsmi_dl_handle;
|
||||
#endif
|
||||
bool initialized = false;
|
||||
bool init();
|
||||
bool shutdown();
|
||||
template <bool is_init> bool collect(gpu_info* gpus_slice);
|
||||
uint32_t device_count = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace Mem {
|
||||
double old_uptime;
|
||||
}
|
||||
|
@ -145,7 +250,7 @@ namespace Shared {
|
|||
Cpu::collect();
|
||||
if (Runner::coreNum_reset) Runner::coreNum_reset = false;
|
||||
for (auto& [field, vec] : Cpu::current_cpu.cpu_percent) {
|
||||
if (not vec.empty()) Cpu::available_fields.push_back(field);
|
||||
if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field);
|
||||
}
|
||||
Cpu::cpuName = Cpu::get_cpuName();
|
||||
Cpu::got_sensors = Cpu::get_sensors();
|
||||
|
@ -154,12 +259,32 @@ namespace Shared {
|
|||
}
|
||||
Cpu::core_mapping = Cpu::get_core_mapping();
|
||||
|
||||
//? Init for namespace Gpu
|
||||
#ifdef GPU_SUPPORT
|
||||
Gpu::Nvml::init();
|
||||
Gpu::Rsmi::init();
|
||||
if (not Gpu::gpu_names.empty()) {
|
||||
for (auto const& [key, _] : Gpu::gpus[0].gpu_percent)
|
||||
Cpu::available_fields.push_back(key);
|
||||
for (auto const& [key, _] : Gpu::shared_gpu_percent)
|
||||
Cpu::available_fields.push_back(key);
|
||||
|
||||
using namespace Gpu;
|
||||
gpu_b_height_offsets.resize(gpus.size());
|
||||
for (size_t i = 0; i < gpu_b_height_offsets.size(); ++i)
|
||||
gpu_b_height_offsets[i] = gpus[i].supported_functions.gpu_utilization
|
||||
+ gpus[i].supported_functions.pwr_usage
|
||||
+ (gpus[i].supported_functions.mem_total or gpus[i].supported_functions.mem_used)
|
||||
* (1 + 2*(gpus[i].supported_functions.mem_total and gpus[i].supported_functions.mem_used) + 2*gpus[i].supported_functions.mem_utilization);
|
||||
}
|
||||
#endif
|
||||
|
||||
//? Init for namespace Mem
|
||||
Mem::old_uptime = system_uptime();
|
||||
Mem::collect();
|
||||
|
||||
Logger::debug("Shared::init() : Initialized.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Cpu {
|
||||
|
@ -813,6 +938,549 @@ namespace Cpu {
|
|||
}
|
||||
}
|
||||
|
||||
#ifdef GPU_SUPPORT
|
||||
namespace Gpu {
|
||||
//? NVIDIA
|
||||
namespace Nvml {
|
||||
bool init() {
|
||||
if (initialized) return false;
|
||||
|
||||
//? Dynamic loading & linking
|
||||
nvml_dl_handle = dlopen("libnvidia-ml.so", RTLD_LAZY);
|
||||
if (!nvml_dl_handle) {
|
||||
Logger::info(std::string("Failed to load libnvidia-ml.so, NVIDIA GPUs will not be detected: ") + dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto load_nvml_sym = [&](const char sym_name[]) {
|
||||
auto sym = dlsym(nvml_dl_handle, sym_name);
|
||||
auto err = dlerror();
|
||||
if (err != NULL) {
|
||||
Logger::error(string("NVML: Couldn't find function ") + sym_name + ": " + err);
|
||||
return (void*)nullptr;
|
||||
} else return sym;
|
||||
};
|
||||
|
||||
#define LOAD_SYM(NAME) if ((NAME = (decltype(NAME))load_nvml_sym(#NAME)) == nullptr) return false
|
||||
|
||||
LOAD_SYM(nvmlErrorString);
|
||||
LOAD_SYM(nvmlInit);
|
||||
LOAD_SYM(nvmlShutdown);
|
||||
LOAD_SYM(nvmlDeviceGetCount);
|
||||
LOAD_SYM(nvmlDeviceGetHandleByIndex);
|
||||
LOAD_SYM(nvmlDeviceGetName);
|
||||
LOAD_SYM(nvmlDeviceGetPowerManagementLimit);
|
||||
LOAD_SYM(nvmlDeviceGetTemperatureThreshold);
|
||||
LOAD_SYM(nvmlDeviceGetUtilizationRates);
|
||||
LOAD_SYM(nvmlDeviceGetClockInfo);
|
||||
LOAD_SYM(nvmlDeviceGetPowerUsage);
|
||||
LOAD_SYM(nvmlDeviceGetPowerState);
|
||||
LOAD_SYM(nvmlDeviceGetTemperature);
|
||||
LOAD_SYM(nvmlDeviceGetMemoryInfo);
|
||||
LOAD_SYM(nvmlDeviceGetPcieThroughput);
|
||||
|
||||
#undef LOAD_SYM
|
||||
|
||||
//? Function calls
|
||||
nvmlReturn_t result = nvmlInit();
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::debug(std::string("Failed to initialize NVML, NVIDIA GPUs will not be detected: ") + nvmlErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
//? Device count
|
||||
result = nvmlDeviceGetCount(&device_count);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get device count: ") + nvmlErrorString(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device_count > 0) {
|
||||
devices.resize(device_count);
|
||||
gpus.resize(device_count);
|
||||
gpu_names.resize(device_count);
|
||||
|
||||
initialized = true;
|
||||
|
||||
//? Check supported functions & get maximums
|
||||
Nvml::collect<1>(gpus.data());
|
||||
|
||||
return true;
|
||||
} else {initialized = true; shutdown(); return false;}
|
||||
}
|
||||
|
||||
bool shutdown() {
|
||||
if (!initialized) return false;
|
||||
nvmlReturn_t result = nvmlShutdown();
|
||||
if (NVML_SUCCESS == result) {
|
||||
initialized = false;
|
||||
dlclose(nvml_dl_handle);
|
||||
} else Logger::warning(std::string("Failed to shutdown NVML: ") + nvmlErrorString(result));
|
||||
|
||||
return !initialized;
|
||||
}
|
||||
|
||||
template <bool is_init> // collect<1> is called in Nvml::init(), and populates gpus.supported_functions
|
||||
bool collect(gpu_info* gpus_slice) { // raw pointer to vector data, size == device_count
|
||||
if (!initialized) return false;
|
||||
|
||||
nvmlReturn_t result;
|
||||
std::thread pcie_tx_thread, pcie_rx_thread;
|
||||
// DebugTimer nvTotalTimer("Nvidia Total");
|
||||
for (unsigned int i = 0; i < device_count; ++i) {
|
||||
if constexpr(is_init) {
|
||||
//? Device Handle
|
||||
result = nvmlDeviceGetHandleByIndex(i, devices.data() + i);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get device handle: ") + nvmlErrorString(result));
|
||||
gpus[i].supported_functions = {false, false, false, false, false, false, false, false};
|
||||
continue;
|
||||
}
|
||||
|
||||
//? Device name
|
||||
char name[NVML_DEVICE_NAME_BUFFER_SIZE];
|
||||
result = nvmlDeviceGetName(devices[i], name, NVML_DEVICE_NAME_BUFFER_SIZE);
|
||||
if (result != NVML_SUCCESS)
|
||||
Logger::warning(std::string("NVML: Failed to get device name: ") + nvmlErrorString(result));
|
||||
else {
|
||||
gpu_names[i] = string(name);
|
||||
for (const auto& brand : {"NVIDIA", "Nvidia", "(R)", "(TM)"}) {
|
||||
gpu_names[i] = s_replace(gpu_names[i], brand, "");
|
||||
}
|
||||
gpu_names[i] = trim(gpu_names[i]);
|
||||
}
|
||||
|
||||
//? Power usage
|
||||
unsigned int max_power;
|
||||
result = nvmlDeviceGetPowerManagementLimit(devices[i], &max_power);
|
||||
if (result != NVML_SUCCESS)
|
||||
Logger::warning(std::string("NVML: Failed to get maximum GPU power draw, defaulting to 225W: ") + nvmlErrorString(result));
|
||||
else {
|
||||
gpus[i].pwr_max_usage = max_power; // RSMI reports power in microWatts
|
||||
gpu_pwr_total_max += max_power;
|
||||
}
|
||||
|
||||
//? Get temp_max
|
||||
unsigned int temp_max;
|
||||
result = nvmlDeviceGetTemperatureThreshold(devices[i], NVML_TEMPERATURE_THRESHOLD_SHUTDOWN, &temp_max);
|
||||
if (result != NVML_SUCCESS)
|
||||
Logger::warning(std::string("NVML: Failed to get maximum GPU temperature, defaulting to 110°C: ") + nvmlErrorString(result));
|
||||
else gpus[i].temp_max = (long long)temp_max;
|
||||
}
|
||||
|
||||
//? PCIe link speeds, the data collection takes >=20ms each call so they run on separate threads
|
||||
if (gpus_slice[i].supported_functions.pcie_txrx and (Config::getB("nvml_measure_pcie_speeds") or is_init)) {
|
||||
pcie_tx_thread = std::thread([gpus_slice, i]() {
|
||||
unsigned int tx;
|
||||
nvmlReturn_t result = nvmlDeviceGetPcieThroughput(devices[i], NVML_PCIE_UTIL_TX_BYTES, &tx);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get PCIe TX throughput: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pcie_txrx = false;
|
||||
} else gpus_slice[i].pcie_tx = (long long)tx;
|
||||
});
|
||||
|
||||
pcie_rx_thread = std::thread([gpus_slice, i]() {
|
||||
unsigned int rx;
|
||||
nvmlReturn_t result = nvmlDeviceGetPcieThroughput(devices[i], NVML_PCIE_UTIL_RX_BYTES, &rx);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get PCIe RX throughput: ") + nvmlErrorString(result));
|
||||
} else gpus_slice[i].pcie_rx = (long long)rx;
|
||||
});
|
||||
}
|
||||
|
||||
// DebugTimer nvTimer("Nv utilization");
|
||||
//? GPU & memory utilization
|
||||
if (gpus_slice[i].supported_functions.gpu_utilization) {
|
||||
nvmlUtilization_t utilization;
|
||||
result = nvmlDeviceGetUtilizationRates(devices[i], &utilization);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get GPU utilization: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.gpu_utilization = false;
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_utilization = false;
|
||||
} else {
|
||||
gpus_slice[i].gpu_percent.at("gpu-totals").push_back((long long)utilization.gpu);
|
||||
gpus_slice[i].mem_utilization_percent.push_back((long long)utilization.memory);
|
||||
}
|
||||
}
|
||||
|
||||
// nvTimer.stop_rename_reset("Nv clock");
|
||||
//? Clock speeds
|
||||
if (gpus_slice[i].supported_functions.gpu_clock) {
|
||||
unsigned int gpu_clock;
|
||||
result = nvmlDeviceGetClockInfo(devices[i], NVML_CLOCK_GRAPHICS, &gpu_clock);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get GPU clock speed: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.gpu_clock = false;
|
||||
} else gpus_slice[i].gpu_clock_speed = (long long)gpu_clock;
|
||||
}
|
||||
|
||||
if (gpus_slice[i].supported_functions.mem_clock) {
|
||||
unsigned int mem_clock;
|
||||
result = nvmlDeviceGetClockInfo(devices[i], NVML_CLOCK_MEM, &mem_clock);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get VRAM clock speed: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_clock = false;
|
||||
} else gpus_slice[i].mem_clock_speed = (long long)mem_clock;
|
||||
}
|
||||
|
||||
// nvTimer.stop_rename_reset("Nv power");
|
||||
//? Power usage & state
|
||||
if (gpus_slice[i].supported_functions.pwr_usage) {
|
||||
unsigned int power;
|
||||
result = nvmlDeviceGetPowerUsage(devices[i], &power);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get GPU power usage: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pwr_usage = false;
|
||||
} else {
|
||||
gpus_slice[i].pwr_usage = (long long)power;
|
||||
gpus_slice[i].gpu_percent.at("gpu-pwr-totals").push_back(clamp((long long)round((double)gpus_slice[i].pwr_usage * 100.0 / (double)gpus_slice[i].pwr_max_usage), 0ll, 100ll));
|
||||
}
|
||||
}
|
||||
|
||||
if (gpus_slice[i].supported_functions.pwr_state) {
|
||||
nvmlPstates_t pState;
|
||||
result = nvmlDeviceGetPowerState(devices[i], &pState);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get GPU power state: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pwr_state = false;
|
||||
} else gpus_slice[i].pwr_state = static_cast<int>(pState);
|
||||
}
|
||||
|
||||
// nvTimer.stop_rename_reset("Nv temp");
|
||||
//? GPU temperature
|
||||
if (gpus_slice[i].supported_functions.temp_info) {
|
||||
if (Config::getB("check_temp")) {
|
||||
unsigned int temp;
|
||||
nvmlReturn_t result = nvmlDeviceGetTemperature(devices[i], NVML_TEMPERATURE_GPU, &temp);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get GPU temperature: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.temp_info = false;
|
||||
} else gpus_slice[i].temp.push_back((long long)temp);
|
||||
}
|
||||
}
|
||||
|
||||
// nvTimer.stop_rename_reset("Nv mem");
|
||||
//? Memory info
|
||||
if (gpus_slice[i].supported_functions.mem_total) {
|
||||
nvmlMemory_t memory;
|
||||
result = nvmlDeviceGetMemoryInfo(devices[i], &memory);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get VRAM info: ") + nvmlErrorString(result));
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_total = false;
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_used = false;
|
||||
} else {
|
||||
gpus_slice[i].mem_total = memory.total;
|
||||
gpus_slice[i].mem_used = memory.used;
|
||||
//gpu.mem_free = memory.free;
|
||||
|
||||
auto used_percent = (long long)round((double)memory.used * 100.0 / (double)memory.total);
|
||||
gpus_slice[i].gpu_percent.at("gpu-vram-totals").push_back(used_percent);
|
||||
}
|
||||
}
|
||||
|
||||
//? TODO: Processes using GPU
|
||||
/*unsigned int proc_info_len;
|
||||
nvmlProcessInfo_t* proc_info = 0;
|
||||
result = nvmlDeviceGetComputeRunningProcesses_v3(device, &proc_info_len, proc_info);
|
||||
if (result != NVML_SUCCESS) {
|
||||
Logger::warning(std::string("NVML: Failed to get compute processes: ") + nvmlErrorString(result));
|
||||
} else {
|
||||
for (unsigned int i = 0; i < proc_info_len; ++i)
|
||||
gpus_slice[i].graphics_processes.push_back({proc_info[i].pid, proc_info[i].usedGpuMemory});
|
||||
}*/
|
||||
|
||||
// nvTimer.stop_rename_reset("Nv pcie thread join");
|
||||
//? Join PCIE TX/RX threads
|
||||
if constexpr(is_init) { // there doesn't seem to be a better way to do this, but this should be fine considering it's just 2 lines
|
||||
pcie_tx_thread.join();
|
||||
pcie_rx_thread.join();
|
||||
} else if (gpus_slice[i].supported_functions.pcie_txrx and Config::getB("nvml_measure_pcie_speeds")) {
|
||||
pcie_tx_thread.join();
|
||||
pcie_rx_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//? AMD
|
||||
namespace Rsmi {
|
||||
bool init() {
|
||||
if (initialized) return false;
|
||||
|
||||
//? Dynamic loading & linking
|
||||
#if !defined(RSMI_STATIC)
|
||||
rsmi_dl_handle = dlopen("/opt/rocm/lib/librocm_smi64.so", RTLD_LAZY); // first try /lib and /usr/lib, then /opt/rocm/lib if that fails
|
||||
if (dlerror() != NULL) {
|
||||
rsmi_dl_handle = dlopen("librocm_smi64.so", RTLD_LAZY);
|
||||
}
|
||||
if (!rsmi_dl_handle) {
|
||||
Logger::debug(std::string("Failed to load librocm_smi64.so, AMD GPUs will not be detected: ") + dlerror());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto load_rsmi_sym = [&](const char sym_name[]) {
|
||||
auto sym = dlsym(rsmi_dl_handle, sym_name);
|
||||
auto err = dlerror();
|
||||
if (err != NULL) {
|
||||
Logger::error(string("ROCm SMI: Couldn't find function ") + sym_name + ": " + err);
|
||||
return (void*)nullptr;
|
||||
} else return sym;
|
||||
};
|
||||
|
||||
#define LOAD_SYM(NAME) if ((NAME = (decltype(NAME))load_rsmi_sym(#NAME)) == nullptr) return false
|
||||
|
||||
LOAD_SYM(rsmi_init);
|
||||
LOAD_SYM(rsmi_shut_down);
|
||||
LOAD_SYM(rsmi_num_monitor_devices);
|
||||
LOAD_SYM(rsmi_dev_name_get);
|
||||
LOAD_SYM(rsmi_dev_power_cap_get);
|
||||
LOAD_SYM(rsmi_dev_temp_metric_get);
|
||||
LOAD_SYM(rsmi_dev_busy_percent_get);
|
||||
LOAD_SYM(rsmi_dev_memory_busy_percent_get);
|
||||
LOAD_SYM(rsmi_dev_gpu_clk_freq_get);
|
||||
LOAD_SYM(rsmi_dev_power_ave_get);
|
||||
LOAD_SYM(rsmi_dev_memory_total_get);
|
||||
LOAD_SYM(rsmi_dev_memory_usage_get);
|
||||
LOAD_SYM(rsmi_dev_pci_throughput_get);
|
||||
|
||||
#undef LOAD_SYM
|
||||
#endif
|
||||
|
||||
//? Function calls
|
||||
rsmi_status_t result = rsmi_init(0);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::debug("Failed to initialize ROCm SMI, AMD GPUs will not be detected");
|
||||
return false;
|
||||
}
|
||||
|
||||
//? Device count
|
||||
result = rsmi_num_monitor_devices(&device_count);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to fetch number of devices");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device_count > 0) {
|
||||
gpus.resize(gpus.size() + device_count);
|
||||
gpu_names.resize(gpus.size() + device_count);
|
||||
|
||||
initialized = true;
|
||||
|
||||
//? Check supported functions & get maximums
|
||||
Rsmi::collect<1>(gpus.data() + Nvml::device_count);
|
||||
|
||||
return true;
|
||||
} else {initialized = true; shutdown(); return false;}
|
||||
}
|
||||
|
||||
bool shutdown() {
|
||||
if (!initialized) return false;
|
||||
if (rsmi_shut_down() == RSMI_STATUS_SUCCESS) {
|
||||
initialized = false;
|
||||
#if !defined(RSMI_STATIC)
|
||||
dlclose(rsmi_dl_handle);
|
||||
#endif
|
||||
} else Logger::warning("Failed to shutdown ROCm SMI");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <bool is_init>
|
||||
bool collect(gpu_info* gpus_slice) { // raw pointer to vector data, size == device_count, offset by Nvml::device_count elements
|
||||
if (!initialized) return false;
|
||||
rsmi_status_t result;
|
||||
|
||||
for (uint32_t i = 0; i < device_count; ++i) {
|
||||
if constexpr(is_init) {
|
||||
//? Device name
|
||||
char name[NVML_DEVICE_NAME_BUFFER_SIZE]; // ROCm SMI does not provide a constant for this as far as I can tell, this should be good enough
|
||||
result = rsmi_dev_name_get(i, name, NVML_DEVICE_NAME_BUFFER_SIZE);
|
||||
if (result != RSMI_STATUS_SUCCESS)
|
||||
Logger::warning("ROCm SMI: Failed to get device name");
|
||||
else gpu_names[Nvml::device_count + i] = string(name);
|
||||
|
||||
//? Power usage
|
||||
uint64_t max_power;
|
||||
result = rsmi_dev_power_cap_get(i, 0, &max_power);
|
||||
if (result != RSMI_STATUS_SUCCESS)
|
||||
Logger::warning("ROCm SMI: Failed to get maximum GPU power draw, defaulting to 225W");
|
||||
else {
|
||||
gpus_slice[i].pwr_max_usage = (long long)(max_power/1000); // RSMI reports power in microWatts
|
||||
gpu_pwr_total_max += gpus_slice[i].pwr_max_usage;
|
||||
}
|
||||
|
||||
//? Get temp_max
|
||||
int64_t temp_max;
|
||||
result = rsmi_dev_temp_metric_get(i, RSMI_TEMP_TYPE_EDGE, RSMI_TEMP_MAX, &temp_max);
|
||||
if (result != RSMI_STATUS_SUCCESS)
|
||||
Logger::warning("ROCm SMI: Failed to get maximum GPU temperature, defaulting to 110°C");
|
||||
else gpus_slice[i].temp_max = (long long)temp_max;
|
||||
}
|
||||
|
||||
//? GPU utilization
|
||||
if (gpus_slice[i].supported_functions.gpu_utilization) {
|
||||
uint32_t utilization;
|
||||
result = rsmi_dev_busy_percent_get(i, &utilization);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get GPU utilization");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.gpu_utilization = false;
|
||||
} else gpus_slice[i].gpu_percent.at("gpu-totals").push_back((long long)utilization);
|
||||
}
|
||||
|
||||
//? Memory utilization
|
||||
if (gpus_slice[i].supported_functions.mem_utilization) {
|
||||
uint32_t utilization;
|
||||
result = rsmi_dev_memory_busy_percent_get(i, &utilization);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get VRAM utilization");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_utilization = false;
|
||||
} else gpus_slice[i].mem_utilization_percent.push_back((long long)utilization);
|
||||
}
|
||||
|
||||
//? Clock speeds
|
||||
if (gpus_slice[i].supported_functions.gpu_clock) {
|
||||
rsmi_frequencies_t frequencies;
|
||||
result = rsmi_dev_gpu_clk_freq_get(i, RSMI_CLK_TYPE_SYS, &frequencies);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get GPU clock speed: ");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.gpu_clock = false;
|
||||
} else gpus_slice[i].gpu_clock_speed = (long long)frequencies.frequency[frequencies.current]/1000000; // Hz to MHz
|
||||
}
|
||||
|
||||
if (gpus_slice[i].supported_functions.mem_clock) {
|
||||
rsmi_frequencies_t frequencies;
|
||||
result = rsmi_dev_gpu_clk_freq_get(i, RSMI_CLK_TYPE_MEM, &frequencies);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get VRAM clock speed: ");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_clock = false;
|
||||
} else gpus_slice[i].mem_clock_speed = (long long)frequencies.frequency[frequencies.current]/1000000; // Hz to MHz
|
||||
}
|
||||
|
||||
//? Power usage & state
|
||||
if (gpus_slice[i].supported_functions.pwr_usage) {
|
||||
uint64_t power;
|
||||
result = rsmi_dev_power_ave_get(i, 0, &power);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get GPU power usage");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pwr_usage = false;
|
||||
} else gpus_slice[i].gpu_percent.at("gpu-pwr-totals").push_back(clamp((long long)round((double)gpus_slice[i].pwr_usage * 100.0 / (double)gpus_slice[i].pwr_max_usage), 0ll, 100ll));
|
||||
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pwr_state = false;
|
||||
}
|
||||
|
||||
//? GPU temperature
|
||||
if (gpus_slice[i].supported_functions.temp_info) {
|
||||
if (Config::getB("check_temp") or is_init) {
|
||||
int64_t temp;
|
||||
result = rsmi_dev_temp_metric_get(i, RSMI_TEMP_TYPE_EDGE, RSMI_TEMP_CURRENT, &temp);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get GPU temperature");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.temp_info = false;
|
||||
} else gpus_slice[i].temp.push_back((long long)temp/1000);
|
||||
}
|
||||
}
|
||||
|
||||
//? Memory info
|
||||
if (gpus_slice[i].supported_functions.mem_total) {
|
||||
uint64_t total;
|
||||
result = rsmi_dev_memory_total_get(i, RSMI_MEM_TYPE_VRAM, &total);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get total VRAM");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_total = false;
|
||||
} else gpus_slice[i].mem_total = total;
|
||||
}
|
||||
|
||||
if (gpus_slice[i].supported_functions.mem_used) {
|
||||
uint64_t used;
|
||||
result = rsmi_dev_memory_usage_get(i, RSMI_MEM_TYPE_VRAM, &used);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get VRAM usage");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.mem_used = false;
|
||||
} else {
|
||||
gpus_slice[i].mem_used = used;
|
||||
if (gpus_slice[i].supported_functions.mem_total)
|
||||
gpus_slice[i].gpu_percent.at("gpu-vram-totals").push_back((long long)round((double)used * 100.0 / (double)gpus_slice[i].mem_total));
|
||||
}
|
||||
}
|
||||
|
||||
//? PCIe link speeds
|
||||
if (gpus_slice[i].supported_functions.pcie_txrx) {
|
||||
uint64_t tx, rx;
|
||||
result = rsmi_dev_pci_throughput_get(i, &tx, &rx, 0);
|
||||
if (result != RSMI_STATUS_SUCCESS) {
|
||||
Logger::warning("ROCm SMI: Failed to get PCIe throughput");
|
||||
if constexpr(is_init) gpus_slice[i].supported_functions.pcie_txrx = false;
|
||||
} else {
|
||||
gpus_slice[i].pcie_tx = (long long)tx;
|
||||
gpus_slice[i].pcie_rx = (long long)rx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Intel
|
||||
|
||||
//? Collect data from GPU-specific libraries
|
||||
auto collect(bool no_update) -> vector<gpu_info>& {
|
||||
if (Runner::stopping or (no_update and not gpus.empty())) return gpus;
|
||||
|
||||
// DebugTimer gpu_timer("GPU Total");
|
||||
|
||||
//* Collect data
|
||||
Nvml::collect<0>(gpus.data()); // raw pointer to vector data, size == Nvml::device_count
|
||||
Rsmi::collect<0>(gpus.data() + Nvml::device_count); // size = Rsmi::device_count
|
||||
|
||||
//* Calculate average usage
|
||||
long long avg = 0;
|
||||
long long mem_usage_total = 0;
|
||||
long long mem_total = 0;
|
||||
long long pwr_total = 0;
|
||||
for (auto& gpu : gpus) {
|
||||
if (gpu.supported_functions.gpu_utilization)
|
||||
avg += gpu.gpu_percent.at("gpu-totals").back();
|
||||
if (gpu.supported_functions.mem_used)
|
||||
mem_usage_total += gpu.mem_used;
|
||||
if (gpu.supported_functions.mem_total)
|
||||
mem_total += gpu.mem_total;
|
||||
if (gpu.supported_functions.pwr_usage)
|
||||
mem_total += gpu.pwr_usage;
|
||||
|
||||
//* Trim vectors if there are more values than needed for graphs
|
||||
if (width != 0) {
|
||||
//? GPU & memory utilization
|
||||
while (cmp_greater(gpu.gpu_percent.at("gpu-totals").size(), width * 2)) gpu.gpu_percent.at("gpu-totals").pop_front();
|
||||
while (cmp_greater(gpu.mem_utilization_percent.size(), width)) gpu.mem_utilization_percent.pop_front();
|
||||
//? Power usage
|
||||
while (cmp_greater(gpu.gpu_percent.at("gpu-pwr-totals").size(), width)) gpu.gpu_percent.at("gpu-pwr-totals").pop_front();
|
||||
//? Temperature
|
||||
while (cmp_greater(gpu.temp.size(), 18)) gpu.temp.pop_front();
|
||||
//? Memory usage
|
||||
while (cmp_greater(gpu.gpu_percent.at("gpu-vram-totals").size(), width/2)) gpu.gpu_percent.at("gpu-vram-totals").pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
shared_gpu_percent.at("gpu-average").push_back(avg / gpus.size());
|
||||
if (mem_total != 0)
|
||||
shared_gpu_percent.at("gpu-vram-total").push_back(mem_usage_total / mem_total);
|
||||
if (gpu_pwr_total_max != 0)
|
||||
shared_gpu_percent.at("gpu-pwr-total").push_back(pwr_total / gpu_pwr_total_max);
|
||||
|
||||
if (width != 0) {
|
||||
while (cmp_greater(shared_gpu_percent.at("gpu-average").size(), width * 2)) shared_gpu_percent.at("gpu-average").pop_front();
|
||||
while (cmp_greater(shared_gpu_percent.at("gpu-pwr-total").size(), width * 2)) shared_gpu_percent.at("gpu-pwr-total").pop_front();
|
||||
while (cmp_greater(shared_gpu_percent.at("gpu-vram-total").size(), width * 2)) shared_gpu_percent.at("gpu-vram-total").pop_front();
|
||||
}
|
||||
|
||||
return gpus;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Mem {
|
||||
bool has_swap{}; // defaults to false
|
||||
vector<string> fstab;
|
||||
|
@ -2093,6 +2761,6 @@ namespace Tools {
|
|||
catch (const std::invalid_argument&) {}
|
||||
catch (const std::out_of_range&) {}
|
||||
}
|
||||
throw std::runtime_error("Failed get uptime from from " + string{Shared::procPath} + "/uptime");
|
||||
throw std::runtime_error("Failed to get uptime from " + string{Shared::procPath} + "/uptime");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ using namespace Tools;
|
|||
namespace Cpu {
|
||||
vector<long long> core_old_totals;
|
||||
vector<long long> core_old_idles;
|
||||
vector<string> available_fields = {"total"};
|
||||
vector<string> available_fields = {"Auto", "total"};
|
||||
vector<string> available_sensors = {"Auto"};
|
||||
cpu_info current_cpu;
|
||||
bool got_sensors = false, cpu_temp_only = false;
|
||||
|
|
Loading…
Reference in a new issue