diff --git a/.gitignore b/.gitignore index 14aa9cd..7e2ed8f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ bin btop .*/ +# Optional libraries +lib/rocm_smi_lib + # Don't ignore .github directory !.github/ diff --git a/Makefile b/Makefile index 01e98d5..d4d44bc 100644 --- a/Makefile +++ b/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" diff --git a/README.md b/README.md index c632091..4f30445 100644 --- a/README.md +++ b/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 + ``` +
@@ -347,6 +396,9 @@ Also needs a UTF8 locale and a font that covers: Append `ARCH=` 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. diff --git a/src/btop.cpp b/src/btop.cpp index e53ec02..b58127c 100644 --- a/src/btop.cpp +++ b/src/btop.cpp @@ -183,8 +183,11 @@ void term_resize(bool force) { if (force and refreshed) force = false; } else return; - - static const array all_boxes = {"cpu", "mem", "net", "proc"}; +#ifdef GPU_SUPPORT + static const array all_boxes = {"gpu5", "cpu", "mem", "net", "proc", "gpu0", "gpu1", "gpu2", "gpu3", "gpu4"}; +#else + static const array 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 gpu_panels = {}; + for (auto& box : conf.boxes) + if (box.starts_with("gpu")) + gpu_panels.push_back(box.back()-'0'); + + vector 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 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; diff --git a/src/btop_config.cpp b/src/btop_config.cpp index fc71e41..0f723b7 100644 --- a/src/btop_config.cpp +++ b/src/btop_config.cpp @@ -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 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 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 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; diff --git a/src/btop_config.hpp b/src/btop_config.hpp index e5f4ac1..c71d29c 100644 --- a/src/btop_config.hpp +++ b/src/btop_config.hpp @@ -43,9 +43,16 @@ namespace Config { const vector valid_graph_symbols = { "braille", "block", "tty" }; const vector valid_graph_symbols_def = { "default", "braille", "block", "tty" }; - const vector valid_boxes = { "cpu", "mem", "net", "proc" }; + const vector valid_boxes = { + "cpu", "mem", "net", "proc" +#ifdef GPU_SUPPORT + ,"gpu0", "gpu1", "gpu2", "gpu3", "gpu4", "gpu5" +#endif + }; const vector temp_scales = { "celsius", "fahrenheit", "kelvin", "rankine" }; - +#ifdef GPU_SUPPORT + const vector show_gpu_values = { "Auto", "On", "Off" }; +#endif extern vector current_boxes; extern vector preset_list; extern vector available_batteries; diff --git a/src/btop_draw.cpp b/src/btop_draw.cpp index b7ddc74..edb1eec 100644 --- a/src/btop_draw.cpp +++ b/src/btop_draw.cpp @@ -20,6 +20,7 @@ tab-size = 4 #include #include #include +#include #include #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 graphs_upper; + vector graphs_lower; Draw::Meter cpu_meter; + vector gpu_meters; vector core_graphs; vector temp_graphs; + vector gpu_temp_graphs; + vector gpu_mem_graphs; - string draw(const cpu_info& cpu, bool force_redraw, bool data_same) { + string draw(const cpu_info& cpu, const vector& 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& 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& 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 x_vec = {}, y_vec = {}, b_height_vec = {}; + int b_width; + vector b_x_vec = {}, b_y_vec = {}; + vector redraw = {}; + int shown = 0; + vector shown_panels = {}; + int graph_up_height; + vector graph_upper_vec = {}, graph_lower_vec = {}; + vector temp_graph_vec = {}; + vector mem_used_graph_vec = {}, mem_util_graph_vec = {}; + vector gpu_meter_vec = {}; + vector pwr_meter_vec = {}; + vector 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); } diff --git a/src/btop_input.cpp b/src/btop_input.cpp index bf68566..88f307b 100644 --- a/src/btop_input.cpp +++ b/src/btop_input.cpp @@ -22,6 +22,15 @@ tab-size = 4 #include #include #include +#include + +#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 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 boxes = {"", "cpu", "mem", "net", "proc"}; + if (intKey == 0 or intKey > 4) + return; + #endif atomic_wait(Runner::active); Config::current_preset = -1; - static const array 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; diff --git a/src/btop_menu.cpp b/src/btop_menu.cpp index e17463f..206052f 100644 --- a/src/btop_menu.cpp +++ b/src/btop_menu.cpp @@ -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++; diff --git a/src/btop_shared.cpp b/src/btop_shared.cpp index 98c24ce..fe0f334 100644 --- a/src/btop_shared.cpp +++ b/src/btop_shared.cpp @@ -25,6 +25,18 @@ tab-size = 4 namespace rng = std::ranges; using namespace Tools; +#ifdef GPU_SUPPORT +namespace Gpu { + vector gpu_names; + vector gpu_b_height_offsets; + unordered_flat_map> 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_vec, const string& sorting, bool reverse, bool tree) { diff --git a/src/btop_shared.hpp b/src/btop_shared.hpp index ecf97f2..383e189 100644 --- a/src/btop_shared.hpp +++ b/src/btop_shared.hpp @@ -86,6 +86,91 @@ namespace Shared { } +namespace Gpu { +#ifdef GPU_SUPPORT + extern vector box; + extern int width, height, min_width, min_height; + extern vector x_vec, y_vec; + extern vector redraw; + extern int shown; + extern vector shown_panels; + extern vector gpu_names; + extern vector gpu_b_height_offsets; + extern long long gpu_pwr_total_max; + + extern unordered_flat_map> 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> 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 temp = {0}; + long long temp_max = 110; + + long long mem_total = 0; + long long mem_used = 0; + deque 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 graphics_processes = {}; // TODO + // vector compute_processes = {}; + }; + + namespace Nvml { + extern bool shutdown(); + } + namespace Rsmi { + extern bool shutdown(); + } + + //* Collect gpu stats and temperatures + auto collect(bool no_update = false) -> vector&; + + //* Draw contents of gpu box using 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 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, bool force_redraw = false, bool data_same = false); //* Parse /proc/cpu info for mapping of core ids auto get_core_mapping() -> unordered_flat_map; diff --git a/src/btop_tools.cpp b/src/btop_tools.cpp index f6be174..88bf711 100644 --- a/src/btop_tools.cpp +++ b/src/btop_tools.cpp @@ -99,19 +99,31 @@ namespace Term { } auto get_min_size(const string& boxes) -> array { - 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{}; diff --git a/src/btop_tools.hpp b/src/btop_tools.hpp index f4bc36d..c4d9670 100644 --- a/src/btop_tools.hpp +++ b/src/btop_tools.hpp @@ -151,6 +151,12 @@ namespace Term { namespace Tools { constexpr auto SSmax = std::numeric_limits::max(); + class MyNumPunct : public std::numpunct { + 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; - } //* 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 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(); + }; + +} + + + diff --git a/src/freebsd/btop_collect.cpp b/src/freebsd/btop_collect.cpp index 47598f0..7f93322 100644 --- a/src/freebsd/btop_collect.cpp +++ b/src/freebsd/btop_collect.cpp @@ -74,7 +74,7 @@ using namespace Tools; namespace Cpu { vector core_old_totals; vector core_old_idles; - vector available_fields = {"total"}; + vector available_fields = {"Auto", "total"}; vector available_sensors = {"Auto"}; cpu_info current_cpu; bool got_sensors = false, cpu_temp_only = false; diff --git a/src/linux/btop_collect.cpp b/src/linux/btop_collect.cpp index b5b1cea..4bb5e71 100644 --- a/src/linux/btop_collect.cpp +++ b/src/linux/btop_collect.cpp @@ -30,6 +30,11 @@ tab-size = 4 #include // for inet_ntop() #include #include +#include + +#if defined(RSMI_STATIC) + #include +#endif #if !(defined(STATIC_BUILD) && defined(__GLIBC__)) #include @@ -65,7 +70,7 @@ using namespace std::chrono_literals; namespace Cpu { vector core_old_totals; vector core_old_idles; - vector available_fields; + vector available_fields = {"Auto", "total"}; vector 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 core_mapping; } +namespace Gpu { + vector 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 collect(gpu_info* gpus_slice); + vector 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 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 // 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(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 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& { + 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 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"); } } diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp index 26a93d3..0a1f338 100644 --- a/src/osx/btop_collect.cpp +++ b/src/osx/btop_collect.cpp @@ -70,7 +70,7 @@ using namespace Tools; namespace Cpu { vector core_old_totals; vector core_old_idles; - vector available_fields = {"total"}; + vector available_fields = {"Auto", "total"}; vector available_sensors = {"Auto"}; cpu_info current_cpu; bool got_sensors = false, cpu_temp_only = false;