Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 147 additions & 32 deletions linux/GPU.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required GPU_ function naming convention.

The new helper names use snake_case. Rename them to GPU_<functionName> names and update their local call sites. For example, use GPU_getMachineGpuEngine() instead of get_machine_gpu_engine().

As per coding guidelines: "**/*.c: Use ModuleName_functionName() naming convention for functions."

Also applies to: 79-79, 86-86, 92-92, 99-99, 114-115

Source: Coding guidelines

Machine* host = lpt->super.super.host;
LinuxMachine* lhost = (LinuxMachine*) host;
GPUEngineData** engineData = &lhost->gpuEngineData;
Expand All @@ -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 "<prefix><engine>: <value><unit>" 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;
Comment on lines +118 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty engine name.

parse_engine_value accepts drm-engine-: 1 ns because delim == engineStart is valid. The caller then creates and updates an engine with an empty key. Reject this malformed metric.

Proposed fix
-   if (!delim)
+   if (!delim || delim == engineStart)
       return false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const char* delim = strchr(engineStart, ':');
if (!delim)
return false;
const char* delim = strchr(engineStart, ':');
if (!delim || delim == engineStart)
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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

*engine = engineStart;
*engine_len = delim - engineStart;
*value = parsed;
return true;
}

/*
* Documentation reference:
* https://www.kernel.org/doc/html/latest/gpu/drm-usage-stats.html
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +250 to +285

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep raw baselines after a rejected metric.

If parse_engine_value rejects a matching metric, its accumulator remains zero or partial. Lines 332-334 then replace the prior raw baseline. The next valid absolute counter can be reported as a full-uptime delta in one scan.

Track parse success per counter family. If a matching metric is rejected, retain that family’s previous raw counter and clear gpu_percent for the scan.

Also applies to: 332-334

}
}
}
Expand All @@ -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));
Comment on lines +309 to +317

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the raw GPU counter fields and process initialization paths.
ast-grep outline linux/LinuxProcess.h --items all --view expanded
fd -t f '^LinuxProcess\.(c|h)$' linux | xargs -r rg -n -C 4 \
  '\bgpu_timeRaw\b|\bgpu_cycles\b|\bgpu_totalCycles\b|GPU_readProcessData'

Repository: htop-dev/htop

Length of output: 3142


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Trace LinuxProcess allocation and all assignments to the GPU raw fields.
fd -t f . linux | sort | xargs -r rg -n -C 5 \
  'LinuxProcess_new|gpu_timeRaw|gpu_cycles|gpu_totalCycles|xCalloc|calloc|memset'

Repository: htop-dev/htop

Length of output: 14396


Skip delta accounting on the first GPU sample.

LinuxProcess_new() zero-initializes gpu_timeRaw, so the first sample subtracts zero from the absolute new_gpu_time counter and reports lifetime GPU time as interval usage. Add explicit baseline state and skip delta calculation until the raw counters are initialized.


if (gputimeDelta > 0 && monotonicTimeDelta > 0) {
lp->gpu_time += gputimeDelta;
lp->gpu_percent = 100.0F * gputimeDelta / (1000 * 1000) / monotonicTimeDelta;
} else {
lp->gpu_percent = 0.0F;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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;
Expand Down
4 changes: 3 additions & 1 deletion linux/LinuxMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions linux/LinuxProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
4 changes: 4 additions & 0 deletions linux/LinuxProcessTable.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
42 changes: 30 additions & 12 deletions linux/Platform.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down