Add rudimentary, fullscreen single-GPU NVML utilization graph

This commit is contained in:
romner 2023-05-12 19:34:47 +02:00
parent ac17f34580
commit d522a91ef4
10 changed files with 238 additions and 30 deletions

View file

@ -164,8 +164,10 @@ override REQFLAGS := -std=c++20
WARNFLAGS := -Wall -Wextra -pedantic
OPTFLAGS := -O2 -ftree-vectorize -flto=$(LTO)
LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -D_FILE_OFFSET_BITS=64 $(GOODFLAGS) $(ADDFLAGS)
override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS)
GPUCXXFLAGS := -I/opt/cuda/include # TODO: there has to be a better way to link NVML than hardcoded dirs
GPULDFLAGS := -L/usr/lib64 -lnvidia-ml
override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) $(GPUCXXFLAGS)
override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) $(GPULDFLAGS)
INC := $(foreach incdir,$(INCDIRS),-isystem $(incdir)) -I$(SRCDIR)
SU_USER := root
@ -202,16 +204,18 @@ all: $(PRE) directories btop
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;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;92mGPUCXXFLAGS \033[1;94m:| \033[0m$(GPUCXXFLAGS)\n"
@printf "\033[1;92mGPULDFLAGS \033[1;94m:| \033[0m$(GPULDFLAGS)\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"
info-quiet:
@sleep 0.1 2>/dev/null || true

View file

@ -655,7 +655,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 gpu", 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.

View file

@ -187,7 +187,7 @@ void term_resize(bool force) {
}
else return;
static const array<string, 4> all_boxes = {"cpu", "mem", "net", "proc"};
static const array<string, 5> all_boxes = {"cpu", "mem", "net", "proc", "gpu"};
Global::resized = true;
if (Runner::active) Runner::stop();
Term::refresh();
@ -583,6 +583,27 @@ namespace Runner {
throw std::runtime_error("Proc:: -> " + string{e.what()});
}
}
//? GPU
if (v_contains(conf.boxes, "gpu")) {
try {
if (Global::debug) debug_timer("gpu", collect_begin);
//? Start collect
auto gpu = Gpu::collect(conf.no_update);
if (Global::debug) debug_timer("gpu", draw_begin);
//? Draw box
if (not pause_output) output += Gpu::draw(gpu, 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()});
}
}
}
catch (const std::exception& e) {
Global::exit_error_msg = "Exception in runner thread -> " + string{e.what()};
@ -613,8 +634,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 {mainFg}| Show GPU box"
"{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),
@ -622,8 +644,9 @@ namespace Runner {
"mv3"_a = Mv::to(y+9, x),
"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)
"mv6"_a = Mv::to(y+12, x),
"mv7"_a = Mv::to(y+13, x-2),
"mv8"_a = Mv::to(y+14, x)
);
}
output += empty_bg;
@ -637,7 +660,7 @@ namespace Runner {
"post"_a = Theme::c("main_fg") + Fx::ub
);
static auto loc = std::locale(std::locale::classic(), new MyNumPunct);
for (const string name : {"cpu", "mem", "net", "proc", "total"}) {
for (const string name : {"cpu", "mem", "net", "proc", "gpu", "total"}) {
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;

View file

@ -78,7 +78,7 @@ namespace Config {
{"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 gpu\", separate values with whitespace."},
{"update_ms", "#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs."},
@ -316,7 +316,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", "gpu")) {
validError = "Invalid box name in config value presets!";
return false;
}

View file

@ -42,7 +42,7 @@ namespace Config {
const vector<string> valid_graph_symbols = { "braille", "block", "tty" };
const vector<string> valid_graph_symbols_def = { "default", "braille", "block", "tty" };
const vector<string> valid_boxes = { "cpu", "mem", "net", "proc" };
const vector<string> valid_boxes = { "cpu", "mem", "net", "proc", "gpu" };
const vector<string> temp_scales = { "celsius", "fahrenheit", "kelvin", "rankine" };
extern vector<string> current_boxes;

View file

@ -28,6 +28,7 @@ tab-size = 4
#include <btop_tools.hpp>
#include <btop_input.hpp>
#include <btop_menu.hpp>
#include <stdexcept>
using std::array;
using std::clamp;
@ -1575,6 +1576,62 @@ namespace Proc {
}
namespace Gpu {
int width_p = 100, height_p = 32;
int min_width = 60, min_height = 8;
int x = 1, y = 1, width = 20, height;
int b_columns, b_column_size;
int b_x, b_y, b_width, b_height;
bool shown = true, redraw = true, mid_line = false;
int graph_height;
Draw::Graph graph_upper;
string box;
string draw(const gpu_info& gpu, bool force_redraw, bool data_same) {
if (Runner::stopping) return "";
if (force_redraw) redraw = true;
auto tty_mode = Config::getB("tty_mode");
auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_cpu")); // TODO graph_symbol_gpu
auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6);
string out;
out.reserve(width * height);
//* Redraw elements not needed to be updated every cycle
if (redraw) {
out += box;
graph_height = height-2;
//out += Gpu::Nvml::initialized ? "NVML initialized" : "NVML not initialized";
graph_upper = Draw::Graph{x + width - b_width - 3, graph_height, "cpu", gpu.gpu_percent, graph_symbol, false, true}; // TODO cpu -> gpu
}
//out += " " + std::to_string(gpu.gpu_percent.back()) + "%";
try {
//? Gpu graphs
out += Fx::ub + Mv::to(y + 1, x + 1) + graph_upper(gpu.gpu_percent, (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));
/*out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(cpu.cpu_percent.at("total").back())
+ Theme::g("cpu").at(clamp(cpu.cpu_percent.at("total").back(), 0ll, 100ll)) + rjust(to_string(cpu.cpu_percent.at("total").back()), 4) + Theme::c("main_fg") + '%';
if (show_temps) {
const auto [temp, unit] = celsius_to(cpu.temp.at(0).back(), temp_scale);
const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(0).back() * 100 / cpu.temp_max, 0ll, 100ll));
if (b_column_size > 1 or b_columns > 1)
out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color
+ temp_graphs.at(0)(cpu.temp.at(0), data_same or redraw);
out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit;
}*/
out += Theme::c("div_line") + Symbols::v_line;
} catch (const std::exception& e) {
throw std::runtime_error("graphs: " + string{e.what()});
}
redraw = false;
return out + Fx::reset;
}
}
namespace Draw {
void calcSizes() {
atomic_wait(Runner::active);
@ -1588,6 +1645,7 @@ namespace Draw {
Mem::box.clear();
Net::box.clear();
Proc::box.clear();
Gpu::box.clear();
Global::clock.clear();
Global::overlay.clear();
Runner::pause_output = false;
@ -1598,16 +1656,17 @@ namespace Draw {
Input::mouse_mappings.clear();
Cpu::x = Mem::x = Net::x = Proc::x = 1;
Cpu::y = Mem::y = Net::y = Proc::y = 1;
Cpu::width = Mem::width = Net::width = Proc::width = 0;
Cpu::height = Mem::height = Net::height = Proc::height = 0;
Cpu::redraw = Mem::redraw = Net::redraw = Proc::redraw = true;
Cpu::x = Mem::x = Net::x = Proc::y = Gpu::x = 1;
Cpu::y = Mem::y = Net::y = Proc::y = Gpu::y = 1;
Cpu::width = Mem::width = Net::width = Proc::width = Gpu::width = 0;
Cpu::height = Mem::height = Net::height = Proc::height = Gpu::height = 0;
Cpu::redraw = Mem::redraw = Net::redraw = Proc::redraw = Gpu::redraw = true;
Cpu::shown = s_contains(boxes, "cpu");
Mem::shown = s_contains(boxes, "mem");
Net::shown = s_contains(boxes, "net");
Proc::shown = s_contains(boxes, "proc");
Gpu::shown = s_contains(boxes, "gpu");
//* Calculate and draw cpu box outlines
if (Cpu::shown) {
@ -1739,5 +1798,15 @@ namespace Draw {
select_max = height - 3;
box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4);
}
//* Calculate and draw gpu box outlines
if (Gpu::shown) {
using namespace Gpu;
width = Term::width;
height = Term::height;
x = 1;
y = 1;
box = createBox(x, y, width, height, Theme::c("cpu_box"), true, "gpu", "", 5); // TODO gpu_box
}
}
}

View file

@ -260,10 +260,10 @@ namespace Input {
Menu::show(Menu::Menus::Options);
return;
}
else if (is_in(key, "1", "2", "3", "4")) {
else if (is_in(key, "1", "2", "3", "4", "5")) {
atomic_wait(Runner::active);
Config::current_preset = -1;
static const array<string, 4> boxes = {"cpu", "mem", "net", "proc"};
static const array<string, 5> boxes = {"cpu", "mem", "net", "proc", "gpu"};
Config::toggle_box(boxes.at(std::stoi(key) - 1));
Draw::calcSizes();
Runner::run("all", false, true);

View file

@ -1263,7 +1263,7 @@ namespace Menu {
//? Category buttons
out += Mv::to(y+7, x+4);
for (int i = 0; const auto& m : {"general", "cpu", "mem", "net", "proc"}) {
for (int i = 0; const auto& m : {"general", "cpu", "mem", "net", "proc", "gpu"}) {
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 + ' ')

View file

@ -313,3 +313,26 @@ namespace Proc {
int cur_depth, bool collapsed, const string& filter,
bool found = false, bool no_update = false, bool should_filter = false);
}
namespace Gpu {
extern string box;
extern int x, y, width, height, min_width, min_height;
extern bool shown, redraw;
struct gpu_info {
deque<long long> gpu_percent = {};
//deque<long long> temp;
//long long temp_max = 0;
//array<float, 3> load_avg;
};
namespace Nvml {
extern bool initialized;
}
//* Collect gpu stats and temperatures
auto collect(bool no_update = false) -> gpu_info&;
//* Draw contents of gpu box using <gpu> as source
string draw(const gpu_info& gpu, bool force_redraw, bool data_same);
}

View file

@ -28,6 +28,7 @@ tab-size = 4
#include <ifaddrs.h>
#include <net/if.h>
#include <arpa/inet.h> // for inet_ntop()
#include <nvml.h>
#if !(defined(STATIC_BUILD) && defined(__GLIBC__))
@ -93,6 +94,19 @@ namespace Mem {
double old_uptime;
}
namespace Gpu {
gpu_info current_gpu;
unsigned int device_count;
nvmlDevice_t device;
//? NVIDIA data collection
namespace Nvml {
bool initialized = false;
bool init();
bool shutdown();
}
}
namespace Shared {
fs::path procPath, passwd_path;
@ -152,6 +166,9 @@ namespace Shared {
Mem::old_uptime = system_uptime();
Mem::collect();
//? Init for namespace Gpu
Gpu::Nvml::init();
}
}
@ -2073,3 +2090,75 @@ namespace Tools {
throw std::runtime_error("Failed get uptime from from " + string{Shared::procPath} + "/uptime");
}
}
namespace Gpu {
//? NVIDIA
namespace Nvml {
bool init() {
if (initialized) {return false;}
nvmlReturn_t result = nvmlInit();
if (result != NVML_SUCCESS) {
Logger::warning(std::string("Failed to initialize NVML, NVIDIA GPUs will not be detected: ") + nvmlErrorString(result));
return false;
}
result = nvmlDeviceGetCount(&device_count);
if (result != NVML_SUCCESS) {
Logger::error(std::string("Failed to get NVML device count: ") + nvmlErrorString(result));
return false;
}
result = nvmlDeviceGetHandleByIndex(0, &device); // TODO: multi-GPU support
if (result != NVML_SUCCESS) {
Logger::error(std::string("Failed to get NVML device handle: ") + nvmlErrorString(result));
return false;
}
initialized = true;
return true;
}
bool shutdown() {
if (!initialized) {return false;}
nvmlReturn_t result = nvmlShutdown();
if (NVML_SUCCESS == result) {
initialized = true;
} else Logger::warning(std::string("Failed to shutdown NVML: ") + nvmlErrorString(result));
return !initialized;
}
}
// TODO: AMD
// TODO: Intel
//? Collect data from GPU-specific libraries
auto collect(bool no_update) -> gpu_info& {
if (Runner::stopping or (no_update and not current_gpu.gpu_percent.empty())) return current_gpu;
auto& gpu = current_gpu;
//if (Config::getB("show_gpu_freq"))
// TODO gpuHz = get_gpuHz();
//? Get GPU utilization
if (Nvml::initialized) {
nvmlUtilization_t utilization;
nvmlReturn_t result = nvmlDeviceGetUtilizationRates(device, &utilization);
if (result != NVML_SUCCESS) {
throw std::runtime_error(std::string("Failed to get GPU utilization: ") + nvmlErrorString(result));
}
//? Total usage of gpu
gpu.gpu_percent.push_back((long long)utilization.gpu);
//? Reduce size if there are more values than needed for graph
while (cmp_greater(gpu.gpu_percent.size(), width * 2)) gpu.gpu_percent.pop_front();
}
/*if (Config::getB("check_temp")) {
}
*/
return gpu;
}
}