diff --git a/CRT.c b/CRT.c index bd5453bc4..6a875c3e6 100644 --- a/CRT.c +++ b/CRT.c @@ -199,6 +199,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = ColorPair(Magenta, Black), [CPU_STEAL] = ColorPair(Cyan, Black), [CPU_GUEST] = ColorPair(Cyan, Black), + [GPU_ENGINE_1] = ColorPair(Green, Black), + [GPU_ENGINE_2] = ColorPair(Yellow, Black), + [GPU_ENGINE_3] = ColorPair(Red, Black), + [GPU_ENGINE_4] = A_BOLD | ColorPair(Blue, Black), + [GPU_RESIDUE] = ColorPair(Magenta, Black), [PANEL_EDIT] = ColorPair(White, Blue), [SCREENS_OTH_BORDER] = ColorPair(Blue, Blue), [SCREENS_OTH_TEXT] = ColorPair(Black, Blue), @@ -312,6 +317,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = A_BOLD, [CPU_STEAL] = A_DIM, [CPU_GUEST] = A_DIM, + [GPU_ENGINE_1] = A_BOLD, + [GPU_ENGINE_2] = A_NORMAL, + [GPU_ENGINE_3] = A_REVERSE | A_BOLD, + [GPU_ENGINE_4] = A_REVERSE, + [GPU_RESIDUE] = A_BOLD, [PANEL_EDIT] = A_BOLD, [SCREENS_OTH_BORDER] = A_DIM, [SCREENS_OTH_TEXT] = A_DIM, @@ -425,6 +435,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = ColorPair(Blue, White), [CPU_STEAL] = ColorPair(Cyan, White), [CPU_GUEST] = ColorPair(Cyan, White), + [GPU_ENGINE_1] = ColorPair(Green, White), + [GPU_ENGINE_2] = ColorPair(Yellow, White), + [GPU_ENGINE_3] = ColorPair(Red, White), + [GPU_ENGINE_4] = ColorPair(Blue, White), + [GPU_RESIDUE] = ColorPair(Magenta, White), [PANEL_EDIT] = ColorPair(White, Blue), [SCREENS_OTH_BORDER] = A_BOLD | ColorPair(Black, White), [SCREENS_OTH_TEXT] = A_BOLD | ColorPair(Black, White), @@ -538,6 +553,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = ColorPair(Blue, Black), [CPU_STEAL] = ColorPair(Black, Black), [CPU_GUEST] = ColorPair(Black, Black), + [GPU_ENGINE_1] = ColorPair(Green, Black), + [GPU_ENGINE_2] = ColorPair(Yellow, Black), + [GPU_ENGINE_3] = ColorPair(Red, Black), + [GPU_ENGINE_4] = ColorPair(Blue, Black), + [GPU_RESIDUE] = ColorPair(Magenta, Black), [PANEL_EDIT] = ColorPair(White, Blue), [SCREENS_OTH_BORDER] = ColorPair(Blue, Black), [SCREENS_OTH_TEXT] = ColorPair(Blue, Black), @@ -651,6 +671,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = ColorPair(Black, Blue), [CPU_STEAL] = ColorPair(White, Blue), [CPU_GUEST] = ColorPair(White, Blue), + [GPU_ENGINE_1] = A_BOLD | ColorPair(Green, Blue), + [GPU_ENGINE_2] = A_BOLD | ColorPair(Yellow, Blue), + [GPU_ENGINE_3] = A_BOLD | ColorPair(Red, Blue), + [GPU_ENGINE_4] = A_BOLD | ColorPair(White, Blue), + [GPU_RESIDUE] = A_BOLD | ColorPair(Magenta, Blue), [PANEL_EDIT] = ColorPair(White, Blue), [SCREENS_OTH_BORDER] = A_BOLD | ColorPair(Yellow, Blue), [SCREENS_OTH_TEXT] = ColorPair(Cyan, Blue), @@ -762,6 +787,11 @@ static int CRT_colorSchemes[LAST_COLORSCHEME][LAST_COLORELEMENT] = { [CPU_SOFTIRQ] = ColorPair(Blue, Black), [CPU_STEAL] = ColorPair(Cyan, Black), [CPU_GUEST] = ColorPair(Cyan, Black), + [GPU_ENGINE_1] = ColorPair(Green, Black), + [GPU_ENGINE_2] = ColorPair(Yellow, Black), + [GPU_ENGINE_3] = ColorPair(Red, Black), + [GPU_ENGINE_4] = ColorPair(Blue, Black), + [GPU_RESIDUE] = ColorPair(Magenta, Black), [PANEL_EDIT] = ColorPair(White, Cyan), [SCREENS_OTH_BORDER] = ColorPair(White, Black), [SCREENS_OTH_TEXT] = ColorPair(Cyan, Black), diff --git a/CRT.h b/CRT.h index 580979865..2e8be20cf 100644 --- a/CRT.h +++ b/CRT.h @@ -121,6 +121,11 @@ typedef enum ColorElements_ { CPU_SOFTIRQ, CPU_STEAL, CPU_GUEST, + GPU_ENGINE_1, + GPU_ENGINE_2, + GPU_ENGINE_3, + GPU_ENGINE_4, + GPU_RESIDUE, PANEL_EDIT, SCREENS_OTH_BORDER, SCREENS_OTH_TEXT, diff --git a/Machine.c b/Machine.c index 44aa27c81..6beb47b47 100644 --- a/Machine.c +++ b/Machine.c @@ -99,10 +99,14 @@ void Machine_scanTables(Machine* this) { // set scan timestamp static bool firstScanDone = false; - if (firstScanDone) - Platform_gettime_monotonic(&this->monotonicMs); - else + if (firstScanDone) { + this->prevMonotonicMs = this->monotonicMs; + } else { + this->prevMonotonicMs = 0; firstScanDone = true; + } + Platform_gettime_monotonic(&this->monotonicMs); + assert(this->monotonicMs > this->prevMonotonicMs); this->maxUserId = 0; Row_resetFieldWidths(); diff --git a/Machine.h b/Machine.h index 419911c31..f7f82afa5 100644 --- a/Machine.h +++ b/Machine.h @@ -41,6 +41,7 @@ typedef struct Machine_ { struct timeval realtime; /* time of the current sample */ uint64_t realtimeMs; /* current time in milliseconds */ uint64_t monotonicMs; /* same, but from monotonic clock */ + uint64_t prevMonotonicMs; /* time in milliseconds from monotonic clock of previous scan */ int64_t iterationsRemaining; diff --git a/Makefile.am b/Makefile.am index ed92afac4..0d95e3701 100644 --- a/Makefile.am +++ b/Makefile.am @@ -172,6 +172,8 @@ linux_platform_headers = \ generic/hostname.h \ generic/uname.h \ linux/CGroupUtils.h \ + linux/GPU.h \ + linux/GPUMeter.h \ linux/HugePageMeter.h \ linux/IOPriority.h \ linux/IOPriorityPanel.h \ @@ -196,6 +198,8 @@ linux_platform_sources = \ generic/hostname.c \ generic/uname.c \ linux/CGroupUtils.c \ + linux/GPU.c \ + linux/GPUMeter.c \ linux/HugePageMeter.c \ linux/IOPriorityPanel.c \ linux/LibSensors.c \ diff --git a/Process.h b/Process.h index 3903a4077..6a80bc51a 100644 --- a/Process.h +++ b/Process.h @@ -210,6 +210,9 @@ typedef struct ProcessFieldData_ { /* Whether the column width is dynamically adjusted (the minimum width is determined by the title length) */ bool autoWidth; + + /* Whether the title of a column with dynamically adjusted width is right aligned (default is left aligned) */ + bool autoTitleRightAlign; } ProcessFieldData; #define LAST_PROCESSFIELD LAST_RESERVED_FIELD diff --git a/Row.c b/Row.c index 9ea3f0731..705bfa8b3 100644 --- a/Row.c +++ b/Row.c @@ -154,7 +154,7 @@ static const char* alignedTitleProcessField(ProcessField field, char* titleBuffe } if (Process_fields[field].autoWidth) { - if (field == PERCENT_CPU) + if (Process_fields[field].autoTitleRightAlign) xSnprintf(titleBuffer, titleBufferSize, "%*s ", Row_fieldWidths[field], title); else xSnprintf(titleBuffer, titleBufferSize, "%-*.*s ", Row_fieldWidths[field], Row_fieldWidths[field], title); @@ -337,6 +337,14 @@ void Row_printTime(RichString* str, unsigned long long totalHundredths, bool col char buffer[10]; int len; + if (totalHundredths == 0) { + int shadowColor = coloring ? CRT_colors[PROCESS_SHADOW] : CRT_colors[PROCESS]; + + len = xSnprintf(buffer, sizeof(buffer), " 0:00.00 "); + RichString_appendnAscii(str, shadowColor, buffer, len); + return; + } + int yearColor = coloring ? CRT_colors[LARGE_NUMBER] : CRT_colors[PROCESS]; int dayColor = coloring ? CRT_colors[PROCESS_GIGABYTES] : CRT_colors[PROCESS]; int hourColor = coloring ? CRT_colors[PROCESS_MEGABYTES] : CRT_colors[PROCESS]; diff --git a/XUtils.h b/XUtils.h index 68b948361..32bc282dc 100644 --- a/XUtils.h +++ b/XUtils.h @@ -54,6 +54,16 @@ static inline bool String_eq(const char* s1, const char* s2) { return strcmp(s1, s2) == 0; } +static inline bool String_eq_nullable(const char* s1, const char* s2) { + if (s1 == s2) + return true; + + if (s1 && s2) + return String_eq(s1, s2); + + return false; +} + ATTR_NONNULL char* String_cat(const char* s1, const char* s2) ATTR_MALLOC; diff --git a/darwin/DarwinProcess.c b/darwin/DarwinProcess.c index 1e315ebaf..29563a648 100644 --- a/darwin/DarwinProcess.c +++ b/darwin/DarwinProcess.c @@ -40,7 +40,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_VIRT] = { .name = "M_VIRT", .title = " VIRT ", .description = "Total program size in virtual memory", .flags = 0, .defaultSortDesc = true, }, [M_RESIDENT] = { .name = "M_RESIDENT", .title = " RES ", .description = "Resident set size, size of the text and data sections, plus stack usage", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, diff --git a/dragonflybsd/DragonFlyBSDProcess.c b/dragonflybsd/DragonFlyBSDProcess.c index 4be2198d3..915ecd094 100644 --- a/dragonflybsd/DragonFlyBSDProcess.c +++ b/dragonflybsd/DragonFlyBSDProcess.c @@ -38,7 +38,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_VIRT] = { .name = "M_VIRT", .title = " VIRT ", .description = "Total program size in virtual memory", .flags = 0, .defaultSortDesc = true, }, [M_RESIDENT] = { .name = "M_RESIDENT", .title = " RES ", .description = "Resident set size, size of the text and data sections, plus stack usage", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, diff --git a/freebsd/FreeBSDProcess.c b/freebsd/FreeBSDProcess.c index e7582a75d..901f9dad8 100644 --- a/freebsd/FreeBSDProcess.c +++ b/freebsd/FreeBSDProcess.c @@ -40,7 +40,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_VIRT] = { .name = "M_VIRT", .title = " VIRT ", .description = "Total program size in virtual memory", .flags = 0, .defaultSortDesc = true, }, [M_RESIDENT] = { .name = "M_RESIDENT", .title = " RES ", .description = "Resident set size, size of the text and data sections, plus stack usage", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, diff --git a/linux/GPU.c b/linux/GPU.c new file mode 100644 index 000000000..37d5d7c48 --- /dev/null +++ b/linux/GPU.c @@ -0,0 +1,246 @@ +/* +htop - GPU.c +(C) 2023 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "linux/GPU.h" + +#include +#include +#include +#include +#include + +#include "XUtils.h" + +#include "linux/LinuxMachine.h" + + +typedef unsigned long long int ClientID; +#define INVALID_CLIENT_ID ((ClientID)-1) + + +typedef struct ClientInfo_ { + char* pdev; + ClientID id; + struct ClientInfo_* next; +} ClientInfo; + +enum section_state { + SECST_UNKNOWN, + SECST_DUPLICATE, + SECST_NEW, +}; + +static bool is_duplicate_client(const ClientInfo* parsed, ClientID id, const char* pdev) { + for (; parsed; parsed = parsed->next) { + if (id == parsed->id && String_eq_nullable(pdev, parsed->pdev)) { + return true; + } + } + + return false; +} + +static void update_machine_gpu(LinuxProcessTable* lpt, unsigned long long int time, const char* engine, size_t engine_len) { + Machine* host = lpt->super.super.host; + LinuxMachine* lhost = (LinuxMachine*) host; + GPUEngineData** engineData = &lhost->gpuEngineData; + + while (*engineData) { + if (strncmp((*engineData)->key, engine, engine_len) == 0 && (*engineData)->key[engine_len] == '\0') + break; + + engineData = &((*engineData)->next); + } + + if (!*engineData) { + GPUEngineData* newData = xMalloc(sizeof(*newData)); + *newData = (GPUEngineData) { + .prevTime = 0, + .curTime = 0, + .key = xStrndup(engine, engine_len), + .next = NULL, + }; + + *engineData = newData; + } + + (*engineData)->curTime += time; + lhost->curGpuTime += time; +} + +/* + * Documentation reference: + * https://www.kernel.org/doc/html/latest/gpu/drm-usage-stats.html + */ +void GPU_readProcessData(LinuxProcessTable* lpt, LinuxProcess* lp, openat_arg_t procFd) { + const Machine* host = lp->super.super.host; + int fdinfoFd = -1; + DIR* fdinfoDir = NULL; + ClientInfo* parsed_ids = NULL; + unsigned long long int new_gpu_time = 0; + + /* check only if active in last check or last scan was more than 5s ago */ + if (lp->gpu_activityMs != 0 && host->monotonicMs - lp->gpu_activityMs < 5000) { + lp->gpu_percent = 0.0f; + return; + } + lp->gpu_activityMs = host->monotonicMs; + + fdinfoFd = Compat_openat(procFd, "fdinfo", O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + if (fdinfoFd == -1) + goto out; + + fdinfoDir = fdopendir(fdinfoFd); + if (!fdinfoDir) + goto out; + fdinfoFd = -1; + +#ifndef HAVE_OPENAT + char fdinfoPathBuf[32]; + xSnprintf(fdinfoPathBuf, sizeof(fdinfoPathBuf), PROCDIR "/%u/fdinfo", Process_getPid(&lp->super)); +#endif + + while (true) { + char* pdev = NULL; + ClientID client_id = INVALID_CLIENT_ID; + enum section_state sstate = SECST_UNKNOWN; + + const struct dirent* entry = readdir(fdinfoDir); + if (!entry) + break; + const char* ename = entry->d_name; + + if (ename[0] == '.' && (ename[1] == '\0' || (ename[1] == '.' && ename[2] == '\0'))) + continue; + + char buffer[4096]; +#ifdef HAVE_OPENAT + ssize_t ret = xReadfileat(dirfd(fdinfoDir), ename, buffer, sizeof(buffer)); +#else + ssize_t ret = xReadfileat(fdinfoPathBuf, ename, buffer, sizeof(buffer)); +#endif + /* eventfd information can be huge */ + if (ret <= 0 || (size_t)ret >= sizeof(buffer) - 1) + continue; + + char* buf = buffer; + const char* line; + while ((line = strsep(&buf, "\n")) != NULL) { + if (!String_startsWith(line, "drm-")) + continue; + line += strlen("drm-"); + + if (line[0] == 'c' && String_startsWith(line, "client-id:")) { + if (sstate == SECST_NEW) { + assert(client_id != INVALID_CLIENT_ID); + + ClientInfo* new = xMalloc(sizeof(*new)); + *new = (ClientInfo) { + .id = client_id, + .pdev = pdev, + .next = parsed_ids, + }; + pdev = NULL; + + parsed_ids = new; + } + + sstate = SECST_UNKNOWN; + + char *endptr; + errno = 0; + client_id = strtoull(line + strlen("client-id:"), &endptr, 10); + if (errno || *endptr != '\0') + client_id = INVALID_CLIENT_ID; + } else if (line[0] == 'p' && String_startsWith(line, "pdev:")) { + const char* p = line + strlen("pdev:"); + + while (isspace((unsigned char)*p)) + p++; + + assert(!pdev || String_eq(pdev, p)); + if (!pdev) + pdev = xStrdup(p); + } else if (line[0] == 'e' && String_startsWith(line, "engine-")) { + if (sstate == SECST_DUPLICATE) + continue; + + const char* engineStart = line + strlen("engine-"); + + if (String_startsWith(engineStart, "capacity-")) + continue; + + const char* delim = strchr(line, ':'); + + char* endptr; + errno = 0; + unsigned long long int value = strtoull(delim + 1, &endptr, 10); + if (errno == 0 && String_startsWith(endptr, " ns")) { + if (sstate == SECST_UNKNOWN) { + if (client_id != INVALID_CLIENT_ID && !is_duplicate_client(parsed_ids, client_id, pdev)) + sstate = SECST_NEW; + else + sstate = SECST_DUPLICATE; + } + + if (sstate == SECST_NEW) { + new_gpu_time += value; + update_machine_gpu(lpt, value, engineStart, delim - engineStart); + } + } + } + } /* finished parsing lines */ + + if (sstate == SECST_NEW) { + assert(client_id != INVALID_CLIENT_ID); + + ClientInfo* new = xMalloc(sizeof(*new)); + *new = (ClientInfo) { + .id = client_id, + .pdev = pdev, + .next = parsed_ids, + }; + pdev = NULL; + + parsed_ids = new; + } + + free(pdev); + } /* finished parsing fdinfo entries */ + + if (new_gpu_time > 0) { + unsigned long long int gputimeDelta; + uint64_t monotonicTimeDelta; + + Row_updateFieldWidth(GPU_TIME, ceil(log10(new_gpu_time))); + + gputimeDelta = saturatingSub(new_gpu_time, lp->gpu_time); + monotonicTimeDelta = host->monotonicMs - host->prevMonotonicMs; + lp->gpu_percent = 100.0f * gputimeDelta / (1000 * 1000) / monotonicTimeDelta; + + lp->gpu_activityMs = 0; + } else + lp->gpu_percent = 0.0f; + +out: + + lp->gpu_time = new_gpu_time; + + while (parsed_ids) { + ClientInfo* next = parsed_ids->next; + free(parsed_ids->pdev); + free(parsed_ids); + parsed_ids = next; + } + + if (fdinfoDir) + closedir(fdinfoDir); + if (fdinfoFd != -1) + close(fdinfoFd); +} diff --git a/linux/GPU.h b/linux/GPU.h new file mode 100644 index 000000000..b502d1471 --- /dev/null +++ b/linux/GPU.h @@ -0,0 +1,17 @@ +#ifndef HEADER_GPU +#define HEADER_GPU +/* +htop - GPU.h +(C) 2023 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Compat.h" +#include "linux/LinuxProcess.h" +#include "linux/LinuxProcessTable.h" + + +void GPU_readProcessData(LinuxProcessTable* lpl, LinuxProcess* lp, openat_arg_t procFd); + +#endif /* HEADER_GPU */ diff --git a/linux/GPUMeter.c b/linux/GPUMeter.c new file mode 100644 index 000000000..fbd32ea82 --- /dev/null +++ b/linux/GPUMeter.c @@ -0,0 +1,179 @@ +/* +htop - GPUMeter.c +(C) 2023 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "linux/GPUMeter.h" + +#include "CRT.h" +#include "RichString.h" +#include "linux/LinuxMachine.h" + + +static size_t activeMeters; + +bool GPUMeter_active(void) { + return activeMeters > 0; +} + +struct EngineData { + const char* key; /* owned by LinuxMachine */ + unsigned long long int timeDiff; +}; + +static struct EngineData GPUMeter_engineData[4]; +static unsigned long long int prevResidueTime, curResidueTime; +static double totalUsage; +static unsigned long long int totalGPUTimeDiff; + +static const int GPUMeter_attributes[] = { + GPU_ENGINE_1, + GPU_ENGINE_2, + GPU_ENGINE_3, + GPU_ENGINE_4, + GPU_RESIDUE, +}; + +static int humanTimeUnit(char* buffer, size_t size, unsigned long long int value) { + + if (value < 1000) + return xSnprintf(buffer, size, "%3lluns", value); + + if (value < 10000) + return xSnprintf(buffer, size, "%1llu.%1lluus", value / 1000, (value % 1000) / 100); + + value /= 1000; + + if (value < 1000) + return xSnprintf(buffer, size, "%3lluus", value); + + if (value < 10000) + return xSnprintf(buffer, size, "%1llu.%1llums", value / 1000, (value % 1000) / 100); + + value /= 1000; + + if (value < 1000) + return xSnprintf(buffer, size, "%3llums", value); + + if (value < 10000) + return xSnprintf(buffer, size, "%1llu.%1llus", value / 1000, (value % 1000) / 100); + + value /= 1000; + + if (value < 600) + return xSnprintf(buffer, size, "%3llus", value); + + value /= 60; + + if (value < 600) + return xSnprintf(buffer, size, "%3llum", value); + + value /= 60; + + if (value < 96) + return xSnprintf(buffer, size, "%3lluh", value); + + value /= 24; + + return xSnprintf(buffer, size, "%3llud", value); +} + +static void GPUMeter_updateValues(Meter* this) { + const Machine* host = this->host; + const LinuxMachine* lhost = (const LinuxMachine*) host; + const GPUEngineData* gpuEngineData; + char* buffer = this->txtBuffer; + size_t size = sizeof(this->txtBuffer); + int written; + unsigned int i; + uint64_t monotonictimeDelta; + + assert(ARRAYSIZE(GPUMeter_engineData) + 1 == ARRAYSIZE(GPUMeter_attributes)); + + totalGPUTimeDiff = saturatingSub(lhost->curGpuTime, lhost->prevGpuTime); + monotonictimeDelta = host->monotonicMs - host->prevMonotonicMs; + + prevResidueTime = curResidueTime; + curResidueTime = lhost->curGpuTime; + + for (gpuEngineData = lhost->gpuEngineData, i = 0; gpuEngineData && i < ARRAYSIZE(GPUMeter_engineData); gpuEngineData = gpuEngineData->next, i++) { + GPUMeter_engineData[i].key = gpuEngineData->key; + GPUMeter_engineData[i].timeDiff = saturatingSub(gpuEngineData->curTime, gpuEngineData->prevTime); + + curResidueTime = saturatingSub(curResidueTime, gpuEngineData->curTime); + + this->values[i] = 100.0 * GPUMeter_engineData[i].timeDiff / (1000 * 1000) / monotonictimeDelta; + } + + this->values[ARRAYSIZE(GPUMeter_engineData)] = 100.0 * saturatingSub(curResidueTime, prevResidueTime) / (1000 * 1000) / monotonictimeDelta; + + totalUsage = 100.0 * totalGPUTimeDiff / (1000 * 1000) / monotonictimeDelta; + written = snprintf(buffer, size, "%.1f", totalUsage); + METER_BUFFER_CHECK(buffer, size, written); + + METER_BUFFER_APPEND_CHR(buffer, size, '%'); +} + +static void GPUMeter_display(const Object* cast, RichString* out) { + char buffer[50]; + int written; + const Meter* this = (const Meter*)cast; + unsigned int i; + + RichString_writeAscii(out, CRT_colors[METER_TEXT], ":"); + written = xSnprintf(buffer, sizeof(buffer), "%4.1f", totalUsage); + RichString_appendnAscii(out, CRT_colors[METER_VALUE], buffer, written); + RichString_appendnAscii(out, CRT_colors[METER_TEXT], "%(", 2); + written = humanTimeUnit(buffer, sizeof(buffer), totalGPUTimeDiff); + RichString_appendnAscii(out, CRT_colors[METER_VALUE], buffer, written); + RichString_appendnAscii(out, CRT_colors[METER_TEXT], ")", 1); + + for (i = 0; i < ARRAYSIZE(GPUMeter_engineData); i++) { + if (!GPUMeter_engineData[i].key) + break; + + RichString_appendnAscii(out, CRT_colors[METER_TEXT], " ", 1); + RichString_appendAscii(out, CRT_colors[METER_TEXT], GPUMeter_engineData[i].key); + RichString_appendnAscii(out, CRT_colors[METER_TEXT], ":", 1); + if (isNonnegative(this->values[i])) + written = xSnprintf(buffer, sizeof(buffer), "%4.1f", this->values[i]); + else + written = xSnprintf(buffer, sizeof(buffer), " N/A"); + RichString_appendnAscii(out, CRT_colors[METER_VALUE], buffer, written); + RichString_appendnAscii(out, CRT_colors[METER_TEXT], "%(", 2); + written = humanTimeUnit(buffer, sizeof(buffer), GPUMeter_engineData[i].timeDiff); + RichString_appendnAscii(out, CRT_colors[METER_VALUE], buffer, written); + RichString_appendnAscii(out, CRT_colors[METER_TEXT], ")", 1); + } +} + +static void GPUMeter_init(Meter* this ATTR_UNUSED) { + activeMeters++; +} + +static void GPUMeter_done(Meter* this ATTR_UNUSED) { + assert(activeMeters > 0); + activeMeters--; +} + +const MeterClass GPUMeter_class = { + .super = { + .extends = Class(Meter), + .delete = Meter_delete, + .display = GPUMeter_display, + }, + .init = GPUMeter_init, + .done = GPUMeter_done, + .updateValues = GPUMeter_updateValues, + .defaultMode = BAR_METERMODE, + .maxItems = ARRAYSIZE(GPUMeter_engineData) + 1, + .total = 100.0, + .attributes = GPUMeter_attributes, + .name = "GPU", + .uiName = "GPU usage", + .caption = "GPU" +}; diff --git a/linux/GPUMeter.h b/linux/GPUMeter.h new file mode 100644 index 000000000..a770ec779 --- /dev/null +++ b/linux/GPUMeter.h @@ -0,0 +1,19 @@ +#ifndef HEADER_GPUMeter +#define HEADER_GPUMeter +/* +htop - GPUMeter.h +(C) 2023 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include + +#include "Meter.h" + + +extern const MeterClass GPUMeter_class; + +bool GPUMeter_active(void); + +#endif /* HEADER_GPUMeter */ diff --git a/linux/LinuxMachine.c b/linux/LinuxMachine.c index ae2930d40..4432a75e4 100644 --- a/linux/LinuxMachine.c +++ b/linux/LinuxMachine.c @@ -682,7 +682,17 @@ Machine* Machine_new(UsersTable* usersTable, uid_t userId) { void Machine_delete(Machine* super) { LinuxMachine* this = (LinuxMachine*) super; + GPUEngineData* gpuEngineData = this->gpuEngineData; + Machine_done(super); + + while (gpuEngineData) { + GPUEngineData* next = gpuEngineData->next; + free(gpuEngineData->key); + free(gpuEngineData); + gpuEngineData = next; + } + free(this->cpuData); free(this); } diff --git a/linux/LinuxMachine.h b/linux/LinuxMachine.h index 309b4850f..398d1d774 100644 --- a/linux/LinuxMachine.h +++ b/linux/LinuxMachine.h @@ -53,6 +53,12 @@ typedef struct CPUData_ { bool online; } CPUData; +typedef struct GPUEngineData_ { + unsigned long long int prevTime, curTime; /* absolute GPU time in nano seconds */ + char* key; /* engine name */ + struct GPUEngineData_* next; +} GPUEngineData; + typedef struct LinuxMachine_ { Machine super; @@ -73,6 +79,9 @@ typedef struct LinuxMachine_ { memory_t availableMem; + unsigned long long int prevGpuTime, curGpuTime; /* total absolute GPU time in nano seconds */ + GPUEngineData* gpuEngineData; + ZfsArcStats zfs; ZramStats zram; ZswapStats zswap; diff --git a/linux/LinuxProcess.c b/linux/LinuxProcess.c index c20397964..b70a1d6a1 100644 --- a/linux/LinuxProcess.c +++ b/linux/LinuxProcess.c @@ -62,7 +62,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_DRS] = { .name = "M_DRS", .title = " DATA ", .description = "Size of the .data segment plus stack usage of the process (DATA)", .flags = 0, .defaultSortDesc = true, }, [M_LRS] = { .name = "M_LRS", .title = " LIB ", .description = "The library size of the process (calculated from memory maps)", .flags = PROCESS_FLAG_LINUX_LRS_FIX, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, @@ -109,6 +109,8 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { #ifdef SCHEDULER_SUPPORT [SCHEDULERPOLICY] = { .name = "SCHEDULERPOLICY", .title = "SCHED ", .description = "Current scheduling policy of the process", .flags = PROCESS_FLAG_SCHEDPOL, }, #endif + [GPU_TIME] = { .name = "GPU_TIME", .title = " GPU_TIME", .description = "Total GPU time in nano seconds", .flags = PROCESS_FLAG_LINUX_GPU, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, + [GPU_PERCENT] = { .name = "GPU_PERCENT", .title = "GPU% ", .description = "Percentage of the GPU time the process used in the last sampling", .flags = PROCESS_FLAG_LINUX_GPU, .defaultSortDesc = true, }, }; Process* LinuxProcess_new(const Machine* host) { @@ -242,6 +244,8 @@ static void LinuxProcess_rowWriteField(const Row* super, RichString* str, Proces switch (field) { case CMINFLT: Row_printCount(str, lp->cminflt, coloring); return; case CMAJFLT: Row_printCount(str, lp->cmajflt, coloring); return; + case GPU_PERCENT: Row_printPercentage(lp->gpu_percent, buffer, n, 5, &attr); break; + case GPU_TIME: Row_printTime(str, lp->gpu_time / 1000 / 1000 / 10 /* nano to centi seconds */, coloring); return; case M_DRS: Row_printBytes(str, lp->m_drs * lhost->pageSize, coloring); return; case M_LRS: if (lp->m_lrs) { @@ -425,6 +429,15 @@ static int LinuxProcess_compareByKey(const Process* v1, const Process* v2, Proce return SPACESHIP_NUMBER(p1->autogroup_id, p2->autogroup_id); case AUTOGROUP_NICE: return SPACESHIP_NUMBER(p1->autogroup_nice, p2->autogroup_nice); + case GPU_PERCENT: { + int r = compareRealNumbers(p1->gpu_percent, p2->gpu_percent); + if (r) + return r; + + return SPACESHIP_NUMBER(p1->gpu_time, p2->gpu_time); + } + case GPU_TIME: + return SPACESHIP_NUMBER(p1->gpu_time, p2->gpu_time); default: return Process_compareByKey_Base(v1, v2, key); } diff --git a/linux/LinuxProcess.h b/linux/LinuxProcess.h index 388e50d37..5a1e62725 100644 --- a/linux/LinuxProcess.h +++ b/linux/LinuxProcess.h @@ -29,6 +29,7 @@ in the source distribution for its full text. #define PROCESS_FLAG_LINUX_LRS_FIX 0x00010000 #define PROCESS_FLAG_LINUX_DELAYACCT 0x00040000 #define PROCESS_FLAG_LINUX_AUTOGROUP 0x00080000 +#define PROCESS_FLAG_LINUX_GPU 0x00100000 typedef struct LinuxProcess_ { Process super; @@ -106,6 +107,13 @@ typedef struct LinuxProcess_ { char* secattr; unsigned long long int last_mlrs_calctime; + /* Total GPU time used in nano seconds */ + unsigned long long int gpu_time; + /* GPU utilization in percent */ + float gpu_percent; + /* Activity of GPU: 0 if active, otherwise time of last scan in milliseconds */ + uint64_t gpu_activityMs; + /* Autogroup scheduling (CFS) information */ long int autogroup_id; int autogroup_nice; diff --git a/linux/LinuxProcessTable.c b/linux/LinuxProcessTable.c index 039a64ec3..92f4a27a1 100644 --- a/linux/LinuxProcessTable.c +++ b/linux/LinuxProcessTable.c @@ -49,6 +49,8 @@ in the source distribution for its full text. #include "UsersTable.h" #include "XUtils.h" #include "linux/CGroupUtils.h" +#include "linux/GPU.h" +#include "linux/GPUMeter.h" #include "linux/LinuxMachine.h" #include "linux/LinuxProcess.h" #include "linux/Platform.h" // needed for GNU/hurd to get PATH_MAX // IWYU pragma: keep @@ -1585,6 +1587,14 @@ static bool LinuxProcessTable_recurseProcTree(LinuxProcessTable* this, openat_ar } #endif + if (ss->flags & PROCESS_FLAG_LINUX_GPU || GPUMeter_active()) { + if (parent) { + lp->gpu_time = ((const LinuxProcess*)parent)->gpu_time; + } else { + GPU_readProcessData(this, lp, procFd); + } + } + if (!proc->cmdline && statCommand[0] && (proc->state == ZOMBIE || Process_isKernelThread(proc) || settings->showThreadNames)) { Process_updateCmdline(proc, statCommand, 0, strlen(statCommand)); @@ -1642,9 +1652,9 @@ static bool LinuxProcessTable_recurseProcTree(LinuxProcessTable* this, openat_ar void ProcessTable_goThroughEntries(ProcessTable* super) { LinuxProcessTable* this = (LinuxProcessTable*) super; - const Machine* host = super->super.host; + Machine* host = super->super.host; const Settings* settings = host->settings; - const LinuxMachine* lhost = (const LinuxMachine*) host; + LinuxMachine* lhost = (LinuxMachine*) host; if (settings->ss->flags & PROCESS_FLAG_LINUX_AUTOGROUP) { // Refer to sched(7) 'autogroup feature' section @@ -1656,6 +1666,17 @@ void ProcessTable_goThroughEntries(ProcessTable* super) { this->haveAutogroup = false; } + /* Shift GPU values */ + { + lhost->prevGpuTime = lhost->curGpuTime; + lhost->curGpuTime = 0; + + for (GPUEngineData* engine = lhost->gpuEngineData; engine; engine = engine->next) { + engine->prevTime = engine->curTime; + engine->curTime = 0; + } + } + /* PROCDIR is an absolute path */ assert(PROCDIR[0] == '/'); #ifdef HAVE_OPENAT diff --git a/linux/Platform.c b/linux/Platform.c index 8dc8bb595..175b3e155 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -51,6 +51,7 @@ in the source distribution for its full text. #include "TasksMeter.h" #include "UptimeMeter.h" #include "XUtils.h" +#include "linux/GPUMeter.h" #include "linux/IOPriority.h" #include "linux/IOPriorityPanel.h" #include "linux/LinuxMachine.h" @@ -252,6 +253,7 @@ const MeterClass* const Platform_meterTypes[] = { &SystemdMeter_class, &SystemdUserMeter_class, &FileDescriptorMeter_class, + &GPUMeter_class, NULL }; diff --git a/linux/ProcessField.h b/linux/ProcessField.h index 581a982e9..735423b97 100644 --- a/linux/ProcessField.h +++ b/linux/ProcessField.h @@ -48,6 +48,8 @@ in the source distribution for its full text. CCGROUP = 129, \ CONTAINER = 130, \ M_PRIV = 131, \ + GPU_TIME = 132, \ + GPU_PERCENT = 133, \ // End of list diff --git a/netbsd/NetBSDProcess.c b/netbsd/NetBSDProcess.c index f58cdf2cd..3af5a8728 100644 --- a/netbsd/NetBSDProcess.c +++ b/netbsd/NetBSDProcess.c @@ -151,6 +151,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { .flags = 0, .defaultSortDesc = true, .autoWidth = true, + .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", diff --git a/openbsd/OpenBSDProcess.c b/openbsd/OpenBSDProcess.c index 681d516d9..4ac7aef18 100644 --- a/openbsd/OpenBSDProcess.c +++ b/openbsd/OpenBSDProcess.c @@ -149,6 +149,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { .flags = 0, .defaultSortDesc = true, .autoWidth = true, + .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", diff --git a/pcp/PCPProcess.c b/pcp/PCPProcess.c index 69e2972f3..27630fc52 100644 --- a/pcp/PCPProcess.c +++ b/pcp/PCPProcess.c @@ -57,7 +57,7 @@ const ProcessFieldData Process_fields[] = { [M_LRS] = { .name = "M_LRS", .title = " LIB ", .description = "The library size of the process (unused since Linux 2.6; always 0)", .flags = 0, .defaultSortDesc = true, }, [M_DT] = { .name = "M_DT", .title = " DIRTY ", .description = "Size of the dirty pages of the process (unused since Linux 2.6; always 0)", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, diff --git a/solaris/SolarisProcess.c b/solaris/SolarisProcess.c index 449861b25..84090284a 100644 --- a/solaris/SolarisProcess.c +++ b/solaris/SolarisProcess.c @@ -42,7 +42,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_VIRT] = { .name = "M_VIRT", .title = " VIRT ", .description = "Total program size in virtual memory", .flags = 0, .defaultSortDesc = true, }, [M_RESIDENT] = { .name = "M_RESIDENT", .title = " RES ", .description = "Resident set size, size of the text and data sections, plus stack usage", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, }, diff --git a/unsupported/UnsupportedProcess.c b/unsupported/UnsupportedProcess.c index 3d6d883f3..0eb6ce937 100644 --- a/unsupported/UnsupportedProcess.c +++ b/unsupported/UnsupportedProcess.c @@ -35,7 +35,7 @@ const ProcessFieldData Process_fields[LAST_PROCESSFIELD] = { [M_VIRT] = { .name = "M_VIRT", .title = " VIRT ", .description = "Total program size in virtual memory", .flags = 0, .defaultSortDesc = true, }, [M_RESIDENT] = { .name = "M_RESIDENT", .title = " RES ", .description = "Resident set size, size of the text and data sections, plus stack usage", .flags = 0, .defaultSortDesc = true, }, [ST_UID] = { .name = "ST_UID", .title = "UID", .description = "User ID of the process owner", .flags = 0, }, - [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, + [PERCENT_CPU] = { .name = "PERCENT_CPU", .title = " CPU%", .description = "Percentage of the CPU time the process used in the last sampling", .flags = 0, .defaultSortDesc = true, .autoWidth = true, .autoTitleRightAlign = true, }, [PERCENT_NORM_CPU] = { .name = "PERCENT_NORM_CPU", .title = "NCPU%", .description = "Normalized percentage of the CPU time the process used in the last sampling (normalized by cpu count)", .flags = 0, .defaultSortDesc = true, .autoWidth = true, }, [PERCENT_MEM] = { .name = "PERCENT_MEM", .title = "MEM% ", .description = "Percentage of the memory the process is using, based on resident memory size", .flags = 0, .defaultSortDesc = true, }, [USER] = { .name = "USER", .title = "USER ", .description = "Username of the process owner (or user ID if name cannot be determined)", .flags = 0, },