From 85873cbb9cddf7fe29c4dd60bfc22f599314b598 Mon Sep 17 00:00:00 2001 From: Bogdan Radulescu Date: Fri, 14 Aug 2026 16:40:59 +0300 Subject: [PATCH] Linux: read GPU usage from drm-cycles when there is no ns counter Drivers may report engine usage either as a busy time in nanoseconds (drm-engine-), or as busy cycles plus the cycles elapsed on that engine (drm-cycles- and drm-total-cycles-). We only ever handled the first form. Intel's Xe driver, which took over from i915 on recent hardware, only implements the second one. On a Lunar Lake laptop the GPU meter and the GPU_PERCENT/GPU_TIME columns therefore sit at 0.0% while nvtop and gputop happily show the GPU pegged. So take the ratio of busy to elapsed cycles and scale it with the sampling interval, which lets it be accounted just like the nanosecond values. Nice side effect: the ratio doesn't depend on the wall clock, and the GPU frequency moves around a lot. drm-total-cycles is a device wide counter that every client repeats, so take the maximum of it instead of summing it up. While here, compute the meter residue from the per engine deltas instead of keeping a second absolute counter around, and skip the very first sample, where there is no interval to divide by yet - it used to report nonsense such as "20679d" of total GPU time in the first frame. --- linux/GPU.c | 179 +++++++++++++++++++++++++++++++------- linux/LinuxMachine.h | 4 +- linux/LinuxProcess.h | 6 ++ linux/LinuxProcessTable.c | 4 + linux/Platform.c | 42 ++++++--- 5 files changed, 190 insertions(+), 45 deletions(-) diff --git a/linux/GPU.c b/linux/GPU.c index ce9dee891..a5b746a45 100644 --- a/linux/GPU.c +++ b/linux/GPU.c @@ -45,7 +45,7 @@ static bool is_duplicate_client(const ClientInfo* parsed, ClientID id, const cha return false; } -static void update_machine_gpu(LinuxProcessTable* lpt, unsigned long long int time, const char* engine, size_t engine_len) { +static GPUEngineData* get_machine_gpu_engine(LinuxProcessTable* lpt, const char* engine, size_t engine_len) { Machine* host = lpt->super.super.host; LinuxMachine* lhost = (LinuxMachine*) host; GPUEngineData** engineData = &lhost->gpuEngineData; @@ -60,19 +60,88 @@ static void update_machine_gpu(LinuxProcessTable* lpt, unsigned long long int ti if (!*engineData) { GPUEngineData* newData = xMalloc(sizeof(*newData)); *newData = (GPUEngineData) { - .prevTime = 0, - .curTime = 0, - .key = xStrndup(engine, engine_len), - .next = NULL, + .prevTime = 0, + .curTime = 0, + .prevCycles = 0, + .curCycles = 0, + .prevTotalCycles = 0, + .curTotalCycles = 0, + .key = xStrndup(engine, engine_len), + .next = NULL, }; *engineData = newData; } - (*engineData)->curTime += time; + return *engineData; +} + +static void update_machine_gpu(LinuxProcessTable* lpt, unsigned long long int time, const char* engine, size_t engine_len) { + LinuxMachine* lhost = (LinuxMachine*) lpt->super.super.host; + + get_machine_gpu_engine(lpt, engine, engine_len)->curTime += time; lhost->curGpuTime += time; } +static void update_machine_gpu_cycles(LinuxProcessTable* lpt, unsigned long long int cycles, const char* engine, size_t engine_len) { + get_machine_gpu_engine(lpt, engine, engine_len)->curCycles += cycles; +} + +/* drm-total-cycles-* is a device global counter, all clients of a device + * report the same value, so aggregate by taking the maximum. */ +static void update_machine_gpu_total_cycles(LinuxProcessTable* lpt, unsigned long long int totalCycles, const char* engine, size_t engine_len) { + GPUEngineData* engineData = get_machine_gpu_engine(lpt, engine, engine_len); + + if (totalCycles > engineData->curTotalCycles) + engineData->curTotalCycles = totalCycles; +} + +static bool count_section(enum section_state* sstate, ClientID client_id, const char* pdev, const ClientInfo* parsed_ids) { + 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; + } + + return *sstate == SECST_NEW; +} + +/* + * Parses a ": " line, e.g. "engine-rcs: 1234 ns". + * An empty unit requires the value to be the last item on the line. + */ +static bool parse_engine_value(const char* line, const char* prefix, const char* unit, + const char** engine, size_t* engine_len, unsigned long long int* value) { + const char* engineStart = line + strlen(prefix); + + const char* delim = strchr(engineStart, ':'); + if (!delim) + return false; + + const char* numStart = delim + 1; + while (isspace((unsigned char)*numStart)) + numStart++; + + /* strtoull() would accept a sign and wrap the result around */ + if (!isdigit((unsigned char)*numStart)) + return false; + + char* endptr; + errno = 0; + unsigned long long int parsed = strtoull(numStart, &endptr, 10); + if (errno != 0) + return false; + + if (unit[0] ? !String_startsWith(endptr, unit) : *endptr != '\0') + return false; + + *engine = engineStart; + *engine_len = delim - engineStart; + *value = parsed; + return true; +} + /* * Documentation reference: * https://www.kernel.org/doc/html/latest/gpu/drm-usage-stats.html @@ -83,6 +152,8 @@ void GPU_readProcessData(LinuxProcessTable* lpt, LinuxProcess* lp, openat_arg_t DIR* fdinfoDir = NULL; ClientInfo* parsed_ids = NULL; unsigned long long int new_gpu_time = 0; + unsigned long long int new_gpu_cycles = 0; + unsigned long long int new_gpu_totalCycles = 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) { @@ -170,27 +241,48 @@ void GPU_readProcessData(LinuxProcessTable* lpt, LinuxProcess* lp, openat_arg_t if (sstate == SECST_DUPLICATE) continue; - const char* engineStart = line + strlen("engine-"); - - if (String_startsWith(engineStart, "capacity-")) + if (String_startsWith(line + strlen("engine-"), "capacity-")) continue; - const char* delim = strchr(line, ':'); + const char* engine; + size_t engine_len; + unsigned long long int value; + if (parse_engine_value(line, "engine-", " ns", &engine, &engine_len, &value)) { + if (count_section(&sstate, client_id, pdev, parsed_ids)) { + new_gpu_time += value; + update_machine_gpu(lpt, value, engine, engine_len); + } + } + } else if (line[0] == 'c' && String_startsWith(line, "cycles-")) { + /* Drivers that cannot provide a nanosecond resolution timestamp + * (e.g. Intel Xe) export the busy cycles of an engine together with + * the cycles elapsed on that engine. */ + if (sstate == SECST_DUPLICATE) + continue; - 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; + const char* engine; + size_t engine_len; + unsigned long long int value; + if (parse_engine_value(line, "cycles-", "", &engine, &engine_len, &value)) { + if (count_section(&sstate, client_id, pdev, parsed_ids)) { + new_gpu_cycles += value; + update_machine_gpu_cycles(lpt, value, engine, engine_len); } + } + } else if (line[0] == 't' && String_startsWith(line, "total-cycles-")) { + if (sstate == SECST_DUPLICATE) + continue; - if (sstate == SECST_NEW) { - new_gpu_time += value; - update_machine_gpu(lpt, value, engineStart, delim - engineStart); + const char* engine; + size_t engine_len; + unsigned long long int value; + if (parse_engine_value(line, "total-cycles-", "", &engine, &engine_len, &value)) { + if (count_section(&sstate, client_id, pdev, parsed_ids)) { + /* The same free running counter is reported for all engines + * of a device, so don't accumulate it. */ + if (value > new_gpu_totalCycles) + new_gpu_totalCycles = value; + update_machine_gpu_total_cycles(lpt, value, engine, engine_len); } } } @@ -213,21 +305,44 @@ void GPU_readProcessData(LinuxProcessTable* lpt, LinuxProcess* lp, openat_arg_t free(pdev); } /* finished parsing fdinfo entries */ - if (new_gpu_time > 0) { - unsigned long long int gputimeDelta; - uint64_t monotonicTimeDelta; + { + uint64_t monotonicTimeDelta = host->monotonicMs - host->prevMonotonicMs; + unsigned long long int gputimeDelta = saturatingSub(new_gpu_time, lp->gpu_timeRaw); + + /* Cycle based accounting only yields a ratio of busy to elapsed cycles, + * which is turned into a busy time using the sampling interval. */ + unsigned long long int cyclesDelta = saturatingSub(new_gpu_cycles, lp->gpu_cycles); + unsigned long long int totalCyclesDelta = lp->gpu_totalCycles ? saturatingSub(new_gpu_totalCycles, lp->gpu_totalCycles) : 0; + if (cyclesDelta > 0 && totalCyclesDelta > 0) + gputimeDelta += (unsigned long long int)((double)cyclesDelta / totalCyclesDelta * monotonicTimeDelta * (1000 * 1000)); + + if (gputimeDelta > 0 && monotonicTimeDelta > 0) { + lp->gpu_time += gputimeDelta; + lp->gpu_percent = 100.0F * gputimeDelta / (1000 * 1000) / monotonicTimeDelta; + } else { + lp->gpu_percent = 0.0F; + } - gputimeDelta = saturatingSub(new_gpu_time, lp->gpu_time); - monotonicTimeDelta = host->monotonicMs - host->prevMonotonicMs; - lp->gpu_percent = 100.0F * gputimeDelta / (1000 * 1000) / monotonicTimeDelta; + /* Keep visiting a process as long as it holds a counter, even while it is + * idle: the machine wide totals are summed up from the counters of the + * processes seen in this pass, so skipping one makes the sum drop. */ + if (new_gpu_time > 0 || new_gpu_cycles > 0) + lp->gpu_activityMs = 0; - lp->gpu_activityMs = 0; - } else - lp->gpu_percent = 0.0F; + lp->gpu_timeRaw = new_gpu_time; + lp->gpu_cycles = new_gpu_cycles; + lp->gpu_totalCycles = new_gpu_totalCycles; + } + + goto cleanup; out: + /* Hold on to the counters of the last successful read: failing to look at a + * process is not the same as it having released the GPU, and starting over + * from zero would account for the whole counter a second time. */ + lp->gpu_percent = 0.0F; - lp->gpu_time = new_gpu_time; +cleanup: while (parsed_ids) { ClientInfo* next = parsed_ids->next; diff --git a/linux/LinuxMachine.h b/linux/LinuxMachine.h index efcf8f86f..8035fa77e 100644 --- a/linux/LinuxMachine.h +++ b/linux/LinuxMachine.h @@ -61,7 +61,9 @@ typedef struct CPUData_ { } CPUData; typedef struct GPUEngineData_ { - unsigned long long int prevTime, curTime; /* absolute GPU time in nano seconds */ + unsigned long long int prevTime, curTime; /* absolute GPU time in nano seconds (drm-engine-*) */ + unsigned long long int prevCycles, curCycles; /* absolute busy cycles (drm-cycles-*) */ + unsigned long long int prevTotalCycles, curTotalCycles; /* absolute elapsed cycles (drm-total-cycles-*) */ char* key; /* engine name */ struct GPUEngineData_* next; } GPUEngineData; diff --git a/linux/LinuxProcess.h b/linux/LinuxProcess.h index 0a25bc70b..715b13807 100644 --- a/linux/LinuxProcess.h +++ b/linux/LinuxProcess.h @@ -102,6 +102,12 @@ typedef struct LinuxProcess_ { /* Total GPU time used in nano seconds */ unsigned long long int gpu_time; + /* Last raw sum of the drm-engine-* counters in nano seconds */ + unsigned long long int gpu_timeRaw; + /* Last raw sum of the drm-cycles-* counters */ + unsigned long long int gpu_cycles; + /* Last raw value of the drm-total-cycles-* counters */ + unsigned long long int gpu_totalCycles; /* GPU utilization in percent */ float gpu_percent; /* Activity of GPU: 0 if active, otherwise time of last scan in milliseconds */ diff --git a/linux/LinuxProcessTable.c b/linux/LinuxProcessTable.c index c12cebbed..62b15b32b 100644 --- a/linux/LinuxProcessTable.c +++ b/linux/LinuxProcessTable.c @@ -1859,6 +1859,10 @@ void ProcessTable_goThroughEntries(ProcessTable* super) { for (GPUEngineData* engine = lhost->gpuEngineData; engine; engine = engine->next) { engine->prevTime = engine->curTime; engine->curTime = 0; + engine->prevCycles = engine->curCycles; + engine->curCycles = 0; + engine->prevTotalCycles = engine->curTotalCycles; + engine->curTotalCycles = 0; } } diff --git a/linux/Platform.c b/linux/Platform.c index dd4121e8e..e135fba5b 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -405,32 +405,50 @@ void Platform_setGPUValues(Meter* this, double* totalUsage, unsigned long long* static uint64_t prevMonotonicMs; static double residuePercentage; - static unsigned long long int prevResidueTime; // The results are cached so that we can update values of multiple meter // instances. We also need a local cache of the monotonic timestamp, thus we // don't use host->prevMonotonicMs. - if (host->monotonicMs > prevMonotonicMs) { + if (prevMonotonicMs == 0) { + // First call, there's no sampling interval to relate the counters to yet + prevMonotonicMs = host->monotonicMs; + } else if (host->monotonicMs > prevMonotonicMs) { uint64_t monotonictimeDelta = host->monotonicMs - prevMonotonicMs; - unsigned long long int curResidueTime = lhost->curGpuTime; + unsigned long long int totalTimeDiff = saturatingSub(lhost->curGpuTime, lhost->prevGpuTime); + unsigned long long int namedTimeDiff = 0; const GPUEngineData* gpuEngineData; size_t i; - 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); - GPUMeter_engineData[i].percentage = 100.0 * GPUMeter_engineData[i].timeDiff / (1000 * 1000) / monotonictimeDelta; + for (gpuEngineData = lhost->gpuEngineData, i = 0; gpuEngineData; gpuEngineData = gpuEngineData->next, i++) { + unsigned long long int timeDiff = saturatingSub(gpuEngineData->curTime, gpuEngineData->prevTime); + + // Drivers reporting cycles instead of nanoseconds (e.g. Intel Xe) only + // provide a ratio of busy to elapsed cycles; scale it to the sampling + // interval to get a comparable busy time. + unsigned long long int totalCyclesDiff = gpuEngineData->prevTotalCycles ? saturatingSub(gpuEngineData->curTotalCycles, gpuEngineData->prevTotalCycles) : 0; + if (totalCyclesDiff > 0) { + unsigned long long int cyclesDiff = saturatingSub(gpuEngineData->curCycles, gpuEngineData->prevCycles); + unsigned long long int cyclesTime = (unsigned long long int)((double)cyclesDiff / totalCyclesDiff * monotonictimeDelta * (1000 * 1000)); + + timeDiff += cyclesTime; + totalTimeDiff += cyclesTime; + } - curResidueTime = saturatingSub(curResidueTime, gpuEngineData->curTime); + if (i < ARRAYSIZE(GPUMeter_engineData)) { + GPUMeter_engineData[i].key = gpuEngineData->key; + GPUMeter_engineData[i].timeDiff = timeDiff; + GPUMeter_engineData[i].percentage = 100.0 * timeDiff / (1000 * 1000) / monotonictimeDelta; + + namedTimeDiff += timeDiff; + } } - residuePercentage = 100.0 * saturatingSub(curResidueTime, prevResidueTime) / (1000 * 1000) / monotonictimeDelta; + residuePercentage = 100.0 * saturatingSub(totalTimeDiff, namedTimeDiff) / (1000 * 1000) / monotonictimeDelta; - *totalGPUTimeDiff = saturatingSub(lhost->curGpuTime, lhost->prevGpuTime); - *totalUsage = 100.0 * (*totalGPUTimeDiff) / (1000 * 1000) / monotonictimeDelta; + *totalGPUTimeDiff = totalTimeDiff; + *totalUsage = 100.0 * totalTimeDiff / (1000 * 1000) / monotonictimeDelta; - prevResidueTime = curResidueTime; prevMonotonicMs = host->monotonicMs; }