1 Commits
Author SHA1 Message Date
Mikhail Yevchenko 7c06a8089d Add mixed-vendor GPU summary and fix startup heap corruption
Collect AMD amdgpu sysfs and optional NVIDIA nvidia-smi stats in the
background, auto-detecting supported adapters in PCI order and rotating
additional pairs alongside CPU/RAM. Explicit PCI selection and off mode
remain supported; missing metrics render as N/A.

Initialize the bitmap-only canvas locally instead of the ABI-dependent
libg15render initializer: the installed TTF-enabled renderer wrote an
8-byte FreeType pointer past the smaller bitmap-only allocation, corrupting
the heap before GPU threads started. Add collector, rendering and full-main
regression tests.
2026-09-07 17:06:32 +00:00
12 changed files with 1417 additions and 4 deletions

No files matched your search

+13
View File
@@ -11,5 +11,18 @@ LIBS = -lg15daemon_client -lg15render -lpthread -lm -lrt
IMCLUDES = -I
g15stats_LDFLAGS = -avoid-version
g15stats_SOURCES = g15stats.c g15stats.h
EXTRA_DIST = gpu.h tests/gpu-test.c tests/render-test.c tests/runtime-test.c tests/render-layouts.sh tests/render-images.sh
noinst_HEADERS += gpu.h
AUTOMAKE_OPTIONS = subdir-objects
check_PROGRAMS = gpu-test render-test runtime-test
TESTS = $(check_PROGRAMS) tests/render-layouts.sh
TEST_EXTENSIONS = .sh
SH_LOG_COMPILER = $(SHELL)
gpu_test_SOURCES = tests/gpu-test.c
gpu_test_LDADD = -lpthread
render_test_SOURCES = tests/render-test.c
render_test_LDADD = -lg15daemon_client -lg15render -lpthread -lm -lrt $(GTOP_LIBS)
runtime_test_SOURCES = tests/runtime-test.c
runtime_test_LDADD = -lg15render -lpthread -lm -lrt $(GTOP_LIBS)
+107
View File
@@ -2,6 +2,113 @@ G15Stats - a CPU/Memory/Swap/Network/Battery/Temperature/Fan Speed/CPU Frequenci
The G15Stats requires libgtop development packages to be installed before compilation.
GPU summary columns:
The default --gpu auto collects ALL PCI devices (sorted PCI address) bound
to amdgpu or nvidia at startup, including mixed vendors. Use --gpu dddd:bb:ss.f (also -gpu), for example
--gpu 0000:01:00.0, to select a particular card. --gpu off restores the original
layout and disables collection. An explicit missing/unsupported card shows N/A;
auto with no supported card leaves the original layout unchanged. Restart to
rediscover cards after hotplug. Intel and the nouveau/radeon drivers are not
supported. Missing tools, permissions or individual metrics produce N/A, not 0.
With one GPU the 160x43 summary retains its existing 4/5 indicator rows and
labels. Graphs end at x=96 instead of 153; TOTAL moves to x=98. A divider
at x=105 separates the column at x=108..159. With two or more GPUs the left
x=0..50 shows compact CPU percent, R used system RAM, T total system RAM,
SWP percent, and (in five-row mode) GPU count. Four-row mode uses y=1,10,19,28;
five-row mode uses y=1,8,15,22,29. System RAM uses the same explicit units as VRAM.
Two GPU columns at x=54..104 and x=108..159 are visible together, with dividers
at x=51 and x=105 (the first column's glyphs end before its right divider).
Each GPU has five lines at y=1,8,15,22,29: vendor/index plus PCI bus:slot.function,
UTL percent, U used VRAM, T total VRAM, TEM temperature in Celsius (C).
For example NV1 01:00.0 and AMD2 11:00.0 identify different cards. Indices are
one-based positions in the selected PCI-sorted inventory, not NVIDIA ordinals;
startup diagnostics map each index to its full domain:bus:slot.function address.
More than two GPUs cycle through consecutive pairs every five seconds from
startup (1/2, 3/4, ..., then 1/2); the last odd card occupies the first column.
All cards are collected continuously, not just the visible pair. The display
refresh interval can delay a page change: each draw advances at most one pair,
so slow refresh never skips pairs. Inventory is bounded at 64 cards;
overflow/enumeration failure is an explicit startup error, never silent omission.
Use explicit PCI selection or off to recover. Hotplug requires restart.
The bottom info bar at y=37..42 and all non-summary screens retain their geometry.
Used and total are on separate lines so each retains an explicit MiB/GiB unit
(TiB/PiB for very large values), rounded to one decimal. Other screens do not
change. U/T refer to VRAM, not memory-controller utilization or system RAM;
on AMD APUs the driver may report only the reserved VRAM carveout.
AMD reads gpu_busy_percent, mem_info_vram_used/total (bytes), and the selected
PCI device's hwmon/temp1_input (millidegrees C). NVIDIA optionally executes
/usr/bin/nvidia-smi with pci.bus_id,utilization.gpu,memory.used,memory.total,temperature.gpu
and --format=csv,noheader,nounits: one query for all NVIDIA cards, matched by PCI
identity (including NVIDIA's eight-digit domain form), not response order. No CUDA/NVML SDK
or additional build dependency is needed. Run as an ordinary user.
Independent AMD workers and one batched NVIDIA worker wait two seconds between
queries; drawing uses only a mutex-protected snapshot array. A stalled AMD read
does not block another card or NVIDIA. NVIDIA queries share one 750ms monotonic deadline, fixed
arguments/environment, no shell, bounded output, and SIGKILL on timeout or
parent death. An unreaped child prevents further launches. Samples older than
five seconds display N/A per card, including when a sysfs read stalls. Worker
shutdown has one shared one-second join/reap budget; a stuck kernel read can
outlive that budget in static storage until process exit (no restart after a
failed stop). Normal application termination exits the process; no driver waits
are added to signal handling. The -r display
refresh interval still applies independently. No GPU settings are written.
Official interface references:
https://docs.kernel.org/gpu/amdgpu/thermal.html
https://docs.kernel.org/gpu/amdgpu/driver-misc.html
https://docs.nvidia.com/deploy/nvidia-smi/index.html
GPU validation:
After configuring/building, make check runs compiled parsing, units, missing-data,
expiry, selection, subprocess deadline/reaping/parent-death and descriptor checks,
plus real libg15render buffer checks. No daemon, input devices or synthetic GPU
sysfs fixtures are used. ./gpu-test auto additionally checks every real GPU,
its sysfs vendor identity and background cache (requires all four metrics).
Explicit PCI arguments still test one card. ./render-test auto output writes
real live 160x43 buffers to output-0.pgm and output-1.pgm (five/four rows), plus
output (four rows). ./render-test off output renders the original GPU-off layout.
For in-memory cases, ./render-test off output COUNT PAGE [large|unknown] selects
1..64 cards and a zero-based pair page. Omit PAGE to export the last pair.
The default values exercise LLONG_MAX VRAM and -273C/1000C; large uses
100.0/192.0GiB and 1000C, while unknown uses N/A for all GPU metrics.
For example ./render-test off output 2 tests worst-width dual-card values;
./render-test off output 3 1 tests the odd last page. With no arguments,
make check retains the worst-width two-card case. These are rendering inputs,
not simulated hardware or claims about the installed GPUs.
These do not connect to g15daemon and are not physical LCD tests.
make check also runs tests/render-layouts.sh for off, single, dual, large,
unknown, odd-last and 64-card layouts, each in both summary modes. In addition
to isolated-column comparisons, the test compares final GPU text, vertical
TOTAL and time-footer cells directly with the installed font's bitmap bits.
This independent oracle includes blank cells and checks full glyph bounds;
repeating renderString on another canvas alone cannot detect shared clipping.
Canvas initialization is tested starting from poisoned storage.
Startup regression: make check also runs runtime-test through the full main
loop for three frames with real libgtop and bitmap rendering. Only daemon
transport is replaced by a private socketpair; no keyboard events are sent.
./runtime-test auto 21 additionally requires valid live GPU samples after
twenty seconds. This is not a physical LCD or real daemon integration test.
The bitmap-only client initializes its own canvas, avoiding g15r_initCanvas:
a TTF-enabled renderer can otherwise write beyond a non-TTF caller's canvas
and corrupt malloc metadata at startup, even with --gpu off.
Each visual export also writes output-0.bin and output-1.bin: the actual 860
packed framebuffer bytes (one means black). With ImageMagick installed, run
sh tests/render-images.sh /absolute/output/directory after generating frames
there. It compares packed PBM, PGM and native PNG pixels, generates exact
nearest-neighbor 8x images, and checks the 8x-to-native round trip. Per-scenario
contact sheets use 4x tiles with labels outside the LCD and one column, avoiding
very wide sheets that viewers may downscale. No trim or crop is applied.
Inspect the individual native and 8x files as well as the contact sheets.
The supplied default small font has only three ink columns plus a blank column
per character; its N, M and other shapes can look incomplete when enlarged.
Use the bitmap checks to distinguish those font shapes from missing pixels.
Features:
Summary Screen displays 4 or 5 indicators from CPU/Frequency/Memory/NET/Temperature/Fan Speed/Swap,
along with current time.
+34
View File
@@ -30,6 +30,40 @@ Requirements
- libgtop
GPU monitoring
--------------
The summary automatically collects ALL supported PCI addresses (amdgpu or nvidia),
including mixed vendors. ``--gpu dddd:bb:ss.f`` (alias ``-gpu``) selects
a card; ``--gpu off`` disables collection and restores the original layout.
``--gpu auto`` is the default. Intel, nouveau and radeon are not supported.
Missing metrics show N/A. No supported GPU in auto mode leaves the layout intact.
An explicitly selected missing card keeps an N/A column. Selection is at startup.
Each column shows vendor/index and PCI bus:slot.function, UTL %, U used VRAM, T total VRAM, and TEM in C on five
lines. VRAM uses explicit MiB/GiB units (TiB/PiB if needed), with one decimal.
One GPU retains summary labels and shortened graphs. Two GPUs appear together
beside compact CPU/RAM-used/RAM-total/SWP rows (plus GPU count in five-row mode).
RAM also has explicit units. More than two cycle in PCI-sorted pairs every five
seconds from startup; all continue collecting. NV1/AMD2-style indices are one-based
selected-inventory positions, mapped to full PCI addresses in startup diagnostics.
The 64-card bound fails explicitly on overflow. The bottom bar and other screens
are unchanged. AMD APU VRAM may mean its reserved
carveout, not all shared system memory.
AMD uses read-only PCI-device sysfs/hwmon files. NVIDIA uses the optional
``/usr/bin/nvidia-smi`` without a shell or SDK dependency. Independent AMD workers
and one PCI-keyed all-NVIDIA query wait two seconds between samples, with a shared
750ms NVIDIA deadline and per-card five-second cache expiry. Shutdown has one
one-second worker join/reap budget. Display refresh remains controlled by ``-r``. See README for
exact layout, interfaces and subprocess lifecycle details. Run as an ordinary user.
``make check`` covers GPU-off, single-card and multi-card native framebuffers.
Final GPU, vertical TOTAL and time-footer glyphs are checked against font bitmap
bits independently of the text renderer. See README for packed framebuffer
exports and ``tests/render-images.sh``, which generates verified native/nearest-8x
PNGs and narrow contact sheets without cropping the LCD area.
=======
Options
=======
+12
View File
@@ -1,5 +1,17 @@
g15stats (3.0.3+git20240609.95de11a-1~local1) UNRELEASED; urgency=medium
* Add cached multi-vendor GPU summary columns: independent read-only amdgpu
workers and optional, deadline-bounded batched shell-free nvidia-smi
queries.
Collect all supported cards by default, display two columns concurrently
beside compact CPU/RAM data, and cycle larger inventories in sorted pairs.
Document PCI selection, bounds, units, missing metrics and unchanged no-GPU
layouts; add multi-adapter isolation, live collector and framebuffer tests.
Check complete GPU/TOTAL/footer glyphs against font bits independently;
exercise off/single/paged layouts and verify lossless visual exports.
* Initialize the bitmap canvas locally, avoiding the optional FreeType
canvas ABI mismatch that corrupts the heap during application startup.
Add a full-main regression with isolated daemon transport.
* Import Debian 1.9.2-2 packaging and manual, retaining its changelog,
attribution and checksum-recorded archive provenance.
* Package Menelkir master 95de11a526346b3d9fd33df051b330c36b1c8a16,
+32
View File
@@ -28,6 +28,14 @@ additional statistics where the relevant system sensors are available.
.SH "OPTIONS"
g15stats understands the following options:
.TP
.BI "\-gpu, --gpu " "auto|off|dddd:bb:ss.f"
Select the GPU summary columns. Default auto collects all PCI addresses
bound to amdgpu or nvidia at startup, including mixed vendors; off disables collection and retains
the original layout. A PCI address selects one card explicitly.
No supported card in auto mode leaves the original layout intact; an explicit
missing card shows N/A. Restart after hotplug to rediscover devices.
Intel, nouveau and radeon are not supported.
.br
.P
.HP
@@ -72,6 +80,30 @@ Recalculate the CPU count for systems supporting CPU hotplug.
L2 selects the previous screen; L3 selects the next screen.
L4 selects an alternative view where available; L5 changes the bottom bar.
.SH "NOTES"
Each GPU column shows vendor/index and PCI bus:slot.function, UTL percent,
U used VRAM, T total VRAM, and TEM in C.
Used and total occupy separate rows with explicit MiB/GiB units (TiB/PiB
if needed), rounded to one decimal. On AMD APUs VRAM can mean only the reserved
carveout. With one GPU the summary retains its 4/5 indicators and shortened
graphs. With two GPUs both columns appear together beside compact CPU percent,
R used system RAM, T total system RAM and SWP percent (plus GPU count in five-row
mode). RAM has explicit units too. More than two cycle in PCI-sorted pairs every
five seconds from startup; all cards continue collecting. NV1/AMD2-style indices
are one-based selected-inventory positions, not NVIDIA ordinals; startup diagnostics
map them to full PCI addresses. The 64-card bound fails explicitly on overflow.
The bottom bar and other screens are unchanged. See README for pixel coordinates.
.PP
AMD reads device-local sysfs/hwmon metrics. NVIDIA optionally uses
/usr/bin/nvidia-smi with no shell or SDK dependency. Independent AMD workers and
one batched NVIDIA worker wait two seconds between queries. All NVIDIA cards
share one PCI-keyed query with a 750ms deadline;
timeouts kill the child, and an unreaped child prevents additional launches.
The child is also killed on parent death. Data older than five seconds and
unavailable metrics show N/A, never fabricated zero. Display refresh remains
controlled by the refresh option. Worker shutdown has one shared one-second
join/reap budget; a stuck kernel read can outlive it until process exit.
No GPU settings are written.
.PP
Normal operation requires a running g15daemon. This package does not install
or enable a g15stats service. Run the client as an ordinary user.
The help options exit before connecting to g15daemon.
+119 -4
View File
@@ -55,6 +55,53 @@ This is a simple stats client showing graphs for CPU, MEM & Swap usage, Network
#include <glibtop/uptime.h>
#include <glibtop/sysinfo.h>
#include "g15stats.h"
#include "gpu.h"
/* Main-thread-only summary geometry; other screens retain their full width. */
static int summary_gpu;
static void init_canvas(g15canvas *canvas)
{
/* We use bitmap fonts only. libg15render's g15r_initCanvas can write a
* FreeType handle beyond sizeof(g15canvas) when the library was built
* with TTF_SUPPORT but its consumer was not. Initialize our own storage
* instead; the bitmap API uses only the common buffer/mode prefix. */
memset(canvas, 0, sizeof(*canvas));
}
static void gpu_lines(size_t index, gpu_sample sample, char lines[5][32])
{
char memory[24];
snprintf(lines[0], 32, "%s%zu %.7s",
gpu_devices[index].backend == 1 ? "AMD" : gpu_devices[index].backend == 2 ? "NV" : "GPU",
index + 1, gpu_devices[index].bus + 5);
snprintf(lines[1], sizeof(lines[1]), "UTL N/A");
if (sample.busy >= 0)
snprintf(lines[1], sizeof(lines[1]), "UTL %lld%%", sample.busy);
gpu_memory(memory, sizeof(memory), sample.used);
snprintf(lines[2], sizeof(lines[2]), "U %s", memory);
gpu_memory(memory, sizeof(memory), sample.total);
snprintf(lines[3], sizeof(lines[3]), "T %s", memory);
snprintf(lines[4], sizeof(lines[4]), "TEM N/A");
if (sample.temp != LLONG_MIN)
snprintf(lines[4], sizeof(lines[4]), "TEM %lldC", sample.temp);
}
static void draw_gpu_column(g15canvas *canvas)
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_display_first(frame.count, gpu_milliseconds());
size_t i;
for (i = first; i < frame.count && i < first + 2; i++) {
char lines[5][32];
int row, x = frame.count == 1 ? 108 : 54 + (int)(i - first) * 54;
gpu_lines(i, frame.samples[i], lines);
g15r_drawLine(canvas, x - 3, 0, x - 3, 34, G15_COLOR_BLACK);
for (row = 0; row < 5; row++)
g15r_renderString(canvas, (unsigned char *)lines[row], 0,
G15_TEXT_SMALL, x, 1 + row * 7);
}
}
int g15screen_fd;
@@ -213,7 +260,7 @@ void print_vert_label_logic(g15canvas *canvas, char *label, unsigned int sx){
}
void print_vert_label(g15canvas *canvas, char *label){
print_vert_label_logic(canvas, label, TEXT_RIGHT);
print_vert_label_logic(canvas, label, summary_gpu ? 98 : TEXT_RIGHT);
}
void drawBar_reversed (g15canvas * canvas, int x1, int y1, int x2, int y2, int color,
@@ -264,8 +311,9 @@ void drawBar_reversed (g15canvas * canvas, int x1, int y1, int x2, int y2, int c
}
void drawBar_both(g15canvas *canvas, int y1, int y2, int bar_current, int bar_total, int rbar_current, int rbar_total) {
g15r_drawBar(canvas, BAR_START, y1, BAR_END, y2, G15_COLOR_BLACK, bar_current, bar_total, 4);
drawBar_reversed(canvas, BAR_START, y1, BAR_END, y2, G15_COLOR_BLACK, rbar_current, rbar_total, 5);
int end = summary_gpu ? 96 : BAR_END;
g15r_drawBar(canvas, BAR_START, y1, end, y2, G15_COLOR_BLACK, bar_current, bar_total, 4);
drawBar_reversed(canvas, BAR_START, y1, end, y2, G15_COLOR_BLACK, rbar_current, rbar_total, 5);
}
void drawLine_both(g15canvas *canvas, int y1, int y2) {
@@ -826,6 +874,42 @@ void draw_cpu_screen_unicore(g15canvas *canvas, char *tmpstr, int drawgraph, int
draw_cpu_screen_unicore_logic(canvas, cpu, tmpstr, drawgraph, printlabels, 0);
}
/* Two 52px GPU columns leave 51px for a compact CPU/system-RAM summary.
* Keep this separate from the legacy geometry and generic sensor probing. */
static void compact_ram_lines(char lines[5][32], long long used, long long total)
{
char value[24];
gpu_memory(value, sizeof(value), used);
snprintf(lines[1], 32, "R %s", value);
gpu_memory(value, sizeof(value), total);
snprintf(lines[2], 32, "T %s", value);
}
static void draw_compact_summary(g15canvas *canvas)
{
glibtop_cpu cpu;
glibtop_mem mem;
glibtop_swap swap;
static guint64 last_total, last_idle;
char lines[5][32];
int row, rows = mode[SCREEN_SUMMARY] ? 4 : 5;
glibtop_get_cpu(&cpu);
glibtop_get_mem(&mem);
glibtop_get_swap(&swap);
snprintf(lines[0], 32, "CPU N/A");
if (cpu.total > last_total && cpu.idle >= last_idle && cpu.idle - last_idle <= cpu.total - last_total)
snprintf(lines[0], 32, "CPU %.0f%%", 100.0 * (1.0 - (double)(cpu.idle - last_idle) / (cpu.total - last_total)));
last_total = cpu.total;
last_idle = cpu.idle;
compact_ram_lines(lines, mem.total - mem.free, mem.total);
snprintf(lines[3], 32, "SWP %.0f%%", swap.total ? 100.0 * swap.used / swap.total : 0.0);
snprintf(lines[4], 32, "GPUs %zu", gpu_count);
g15r_clearScreen(canvas, G15_COLOR_WHITE);
for (row = 0; row < rows; row++)
g15r_renderString(canvas, (unsigned char *)lines[row], 0,
G15_TEXT_SMALL, 1, 1 + row * (rows == 5 ? 7 : 9));
}
void draw_cpu_screen_multicore(g15canvas *canvas, char *tmpstr, int unicore) {
glibtop_cpu cpu;
int core,ncpumax;
@@ -839,6 +923,10 @@ void draw_cpu_screen_multicore(g15canvas *canvas, char *tmpstr, int unicore) {
b_nice[GLIBTOP_NCPU],b_sys[GLIBTOP_NCPU],b_idle[GLIBTOP_NCPU],
b_irq[GLIBTOP_NCPU],b_iowait[GLIBTOP_NCPU];
if (summary_gpu && gpu_count > 1 && cycle == SCREEN_SUMMARY) {
draw_compact_summary(canvas);
return;
}
init_cpu_count();
ncpumax = ncpu;
@@ -1694,8 +1782,21 @@ int main(int argc, char *argv[]){
unsigned char interface[128];
static char tmpstr[MAX_LINES];
int unicore = 0;
const char *gpu_selection = "auto";
for (i=0;i<argc;i++) {
if (!strcmp(argv[i], "--gpu") || !strcmp(argv[i], "-gpu")) {
if (++i >= argc) {
fprintf(stderr, "--gpu requires auto, off or a PCI address\n");
return 1;
}
gpu_selection = argv[i];
if (!gpu_selection_valid(gpu_selection)) {
fprintf(stderr, "Invalid GPU selection; use auto, off or dddd:bb:ss.f\n");
return 1;
}
continue;
}
if(0==strncmp(argv[i],"-d",2)||0==strncmp(argv[i],"--daemon",8)) {
go_daemon=1;
}
@@ -1722,6 +1823,8 @@ int main(int argc, char *argv[]){
printf("%s %s - (c) 2008-2010 Mike Lampard, Piotr Czarnecki\n",PACKAGE_NAME,VERSION);
printf("Usage: %s [Options]\n", PACKAGE_NAME);
printf("Options:\n");
printf("--gpu (-gpu) [auto|off|dddd:bb:ss.f] GPU columns (default: auto, all supported GPUs).\n");
printf(" Two GPUs side by side; more cycle in PCI-sorted pairs every 5 seconds.\n");
printf("--daemon (-d) run in background\n");
printf("--unicore (-u) display unicore graphs only on the CPU screen\n");
printf("--help (-h) this help text.\n");
@@ -1785,6 +1888,13 @@ int main(int argc, char *argv[]){
}
}
}
if (gpu_detect(gpu_selection)) {
fprintf(stderr, "GPU enumeration failed or exceeds %d adapters; use --gpu PCI or off\n", GPU_MAX);
return 1;
}
for (size_t gpu = 0; gpu < gpu_count; gpu++)
fprintf(stderr, "GPU %zu: %s (%s)\n", gpu + 1, gpu_devices[gpu].bus,
gpu_devices[gpu].backend == 1 ? "AMD" : gpu_devices[gpu].backend == 2 ? "NV" : "unsupported/missing");
if((g15screen_fd = new_g15_screen(G15_G15RBUF))<0){
printf("Sorry, cant connect to the G15daemon\n");
return -1;
@@ -1795,11 +1905,12 @@ int main(int argc, char *argv[]){
daemonise(0,0);
if(canvas != NULL)
g15r_initCanvas(canvas);
init_canvas(canvas);
else
return -1;
glibtop_init();
if (gpu_start()) fprintf(stderr, "GPU collector unavailable; affected cards display N/A\n");
pthread_create(&keys_thread,NULL,(void*)keyboard_watch,NULL);
if(have_nic==1)
@@ -1813,6 +1924,7 @@ int main(int argc, char *argv[]){
int cycle_old = cycle;
while(1) {
calc_info_cycle();
summary_gpu = cycle == SCREEN_SUMMARY && gpu_count;
switch (cycle) {
case SCREEN_SUMMARY:
case SCREEN_CPU:
@@ -1919,6 +2031,8 @@ int main(int argc, char *argv[]){
break;
}
cycle_old = cycle;
if (summary_gpu) draw_gpu_column(canvas);
summary_gpu = 0;
print_info_label(canvas, tmpstr);
canvas->mode_xor = 0;
@@ -1926,6 +2040,7 @@ int main(int argc, char *argv[]){
g15_send(g15screen_fd,(char *)canvas->buffer,G15_BUFFER_LEN);
g15stats_wait(wait_seconds);
}
gpu_stop();
glibtop_close();
close(g15screen_fd);
+443
View File
@@ -0,0 +1,443 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Read-only Linux GPU telemetry. Included privately by g15stats.c.
*/
#ifndef G15STATS_GPU_H
#define G15STATS_GPU_H
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
typedef struct {
long long busy, used, total, temp; /* percent, bytes, bytes, Celsius */
} gpu_sample;
static const gpu_sample gpu_unknown = {-1, -1, -1, LLONG_MIN};
#define GPU_MAX 64
typedef struct {
int backend; /* 0 missing, 1 amdgpu, 2 nvidia */
char device[PATH_MAX], bus[32];
} gpu_adapter;
typedef struct {
size_t count;
gpu_sample samples[GPU_MAX];
} gpu_frame;
static gpu_adapter gpu_devices[GPU_MAX]; /* immutable while workers run */
static size_t gpu_count;
static gpu_sample gpu_cache[GPU_MAX];
static long long gpu_stamp[GPU_MAX];
static pthread_mutex_t gpu_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_t gpu_threads[GPU_MAX];
static size_t gpu_thread_count;
static atomic_int gpu_stopping;
static pid_t gpu_pending = -1; /* NVIDIA worker owns until joined. */
static long long gpu_epoch;
static size_t gpu_page;
static long long gpu_page_due;
static int gpu_selection_valid(const char *selection)
{
size_t i;
if (!strcmp(selection, "auto") || !strcmp(selection, "off")) return 1;
if (strlen(selection) != 12) return 0;
for (i = 0; i < 12; i++) {
if (i == 4 || i == 7) { if (selection[i] != ':') return 0; }
else if (i == 10) { if (selection[i] != '.') return 0; }
else if (!isxdigit((unsigned char)selection[i])) return 0;
}
return selection[8] <= '1' && selection[11] <= '7';
}
static long long gpu_milliseconds(void)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now))
return 0;
return (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000;
}
/* Reject partial values, overflow and unsupported textual fields. */
static long long gpu_number(const char *text, long long min, long long max,
long long missing)
{
char *end;
long long value;
while (isspace((unsigned char)*text)) text++;
if (!isdigit((unsigned char)*text) && *text != '-') return missing;
errno = 0;
value = strtoll(text, &end, 10);
if (end == text || errno) return missing;
while (isspace((unsigned char)*end)) end++;
return *end || value < min || value > max ? missing : value;
}
static long long gpu_file(const char *path, long long min, long long max,
long long missing)
{
char text[96];
FILE *file = fopen(path, "re");
long long value = missing;
if (!file) return missing;
if (fgets(text, sizeof(text), file) && !ferror(file)) {
/* sysfs values fit in one short line; do not accept truncated input. */
if (fgetc(file) == EOF && !ferror(file))
value = gpu_number(text, min, max, missing);
}
fclose(file);
return value;
}
/* Select once, in sorted PCI-address order, not unstable DRM card order.
* Explicit PCI selection keeps an N/A column even for missing devices.
*/
static int gpu_detect(const char *selection)
{
glob_t paths = {0};
size_t i;
gpu_count = 0;
gpu_epoch = gpu_milliseconds();
gpu_page = 0;
gpu_page_due = gpu_epoch + 5000;
memset(gpu_stamp, 0, sizeof(gpu_stamp));
for (i = 0; i < GPU_MAX; i++) gpu_cache[i] = gpu_unknown;
if (!strcmp(selection, "off")) return 0;
int error = glob("/sys/bus/pci/devices/*", 0, NULL, &paths);
if (error && error != GLOB_NOMATCH) {
globfree(&paths);
return -1;
}
for (i = 0; i < paths.gl_pathc; i++) {
char path[PATH_MAX], driver[PATH_MAX];
const char *bus = strrchr(paths.gl_pathv[i], '/') + 1;
ssize_t len;
int backend;
if (strcmp(selection, "auto") && strcasecmp(selection, bus)) continue;
snprintf(path, sizeof(path), "%s/driver", paths.gl_pathv[i]);
len = readlink(path, driver, sizeof(driver) - 1);
if (len < 0) continue;
driver[len] = 0;
if (!strcmp(strrchr(driver, '/') + 1, "amdgpu"))
backend = 1;
else if (!strcmp(strrchr(driver, '/') + 1, "nvidia"))
backend = 2;
else continue;
if (gpu_count == GPU_MAX) {
globfree(&paths);
gpu_count = 0;
return -1; /* Never silently truncate the inventory. */
}
gpu_devices[gpu_count].backend = backend;
snprintf(gpu_devices[gpu_count].device, PATH_MAX, "%s", paths.gl_pathv[i]);
snprintf(gpu_devices[gpu_count].bus, 32, "%s", bus);
gpu_count++;
}
globfree(&paths);
if (!gpu_count && strcmp(selection, "auto")) {
memset(&gpu_devices[0], 0, sizeof(gpu_devices[0]));
snprintf(gpu_devices[0].bus, 32, "%s", selection);
gpu_count = 1;
}
return 0;
}
static gpu_sample gpu_amd(const gpu_adapter *adapter)
{
gpu_sample sample = gpu_unknown;
char path[PATH_MAX + 64];
glob_t paths = {0};
snprintf(path, sizeof(path), "%s/gpu_busy_percent", adapter->device);
sample.busy = gpu_file(path, 0, 100, -1);
snprintf(path, sizeof(path), "%s/mem_info_vram_used", adapter->device);
sample.used = gpu_file(path, 0, LLONG_MAX, -1);
snprintf(path, sizeof(path), "%s/mem_info_vram_total", adapter->device);
sample.total = gpu_file(path, 1, LLONG_MAX, -1);
snprintf(path, sizeof(path), "%s/hwmon/hwmon*/temp1_input", adapter->device);
if (!glob(path, 0, NULL, &paths) && paths.gl_pathc) {
long long temp = gpu_file(paths.gl_pathv[0], -273000, 1000000, LLONG_MIN);
if (temp != LLONG_MIN) sample.temp = temp / 1000;
}
globfree(&paths);
if (sample.total >= 0 && sample.used > sample.total) sample.used = -1;
return sample;
}
static gpu_sample gpu_csv(char *text)
{
gpu_sample sample = gpu_unknown;
char *fields[4], *cursor = text;
int i;
for (i = 0; i < 4; i++) {
fields[i] = strsep(&cursor, ",");
if (!fields[i]) return sample;
}
if (cursor) return sample;
sample.busy = gpu_number(fields[0], 0, 100, -1);
sample.used = gpu_number(fields[1], 0, LLONG_MAX / 1048576, -1);
sample.total = gpu_number(fields[2], 1, LLONG_MAX / 1048576, -1);
sample.temp = gpu_number(fields[3], -273, 1000, LLONG_MIN);
if (sample.used >= 0) sample.used *= 1048576;
if (sample.total >= 0) sample.total *= 1048576;
if (sample.total >= 0 && sample.used > sample.total) sample.used = -1;
return sample;
}
/* Only the collector calls this. Never wait for a driver in the draw thread.
* A killed but uninterruptible child remains owned, preventing process buildup.
*/
static int gpu_above_stdio(int fd)
{
int copy;
if (fd < 0 || fd >= 3) return fd;
copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
close(fd);
return copy;
}
static int gpu_command(const char *executable, char *const args[], char *text, size_t capacity)
{
int pipes[2], status, nullfd;
pid_t pid, parent = getpid(), result;
size_t used = 0;
long long deadline;
long maxfd = sysconf(_SC_OPEN_MAX);
char *const env[] = {"LC_ALL=C", NULL};
if (gpu_pending > 0) {
result = waitpid(gpu_pending, &status, WNOHANG);
if (!result || (result < 0 && errno == EINTR)) return 0;
gpu_pending = -1;
}
if (pipe2(pipes, O_CLOEXEC | O_NONBLOCK)) return 0;
pipes[0] = gpu_above_stdio(pipes[0]);
pipes[1] = gpu_above_stdio(pipes[1]);
if (pipes[0] < 0 || pipes[1] < 0) {
if (pipes[0] >= 0) close(pipes[0]);
if (pipes[1] >= 0) close(pipes[1]);
return 0;
}
/* Only the reader is nonblocking. A large all-card response must not
* make nvidia-smi fail with EAGAIN when the pipe temporarily fills. */
if (fcntl(pipes[1], F_SETFL, 0) < 0) {
close(pipes[0]);
close(pipes[1]);
return 0;
}
nullfd = gpu_above_stdio(open("/dev/null", O_RDWR | O_CLOEXEC));
if (nullfd < 0) { close(pipes[0]); close(pipes[1]); return 0; }
pid = fork();
if (!pid) {
/* Async-signal-safe child path: no stdio, allocation or shell. */
if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent) _exit(127);
if (dup2(nullfd, 0) < 0 || dup2(pipes[1], 1) < 0 || dup2(nullfd, 2) < 0)
_exit(127);
#ifdef SYS_close_range
if (syscall(SYS_close_range, 3U, ~0U, 0) < 0)
#endif
{
long fd;
if (maxfd < 0) _exit(127);
for (fd = 3; fd < maxfd; fd++) close((int)fd);
}
execve(executable, args, env);
_exit(127);
}
close(nullfd);
close(pipes[1]);
if (pid < 0) { close(pipes[0]); return 0; }
deadline = gpu_milliseconds() + 750;
for (;;) {
ssize_t n = read(pipes[0], text + used, capacity - 1 - used);
if (n > 0) used += (size_t)n;
result = waitpid(pid, &status, WNOHANG);
if (result == pid) {
/* Drain the final bytes after exit before parsing. */
while (used < capacity - 1 &&
(n = read(pipes[0], text + used, capacity - 1 - used)) > 0)
used += (size_t)n;
close(pipes[0]);
text[used] = 0;
return used < capacity - 1 && WIFEXITED(status) && !WEXITSTATUS(status);
}
if (result < 0 && errno != EINTR) { close(pipes[0]); return 0; }
if (atomic_load(&gpu_stopping) || gpu_milliseconds() >= deadline || used == capacity - 1) break;
/* Avoid spinning on POLLHUP if stdout closes before the process exits. */
poll(NULL, 0, 20);
}
kill(pid, SIGKILL);
gpu_pending = pid;
close(pipes[0]);
return 0;
}
/* Match numeric PCI identity: NVIDIA commonly prints an eight-digit domain.
* Malformed/duplicate rows must never contaminate another adapter's sample. */
static int gpu_bus_equal(const char *a, const char *b)
{
unsigned ad, ab, as, af, bd, bb, bs, bf;
int n = 0, m = 0;
if (sscanf(a, "%8x:%2x:%2x.%1x %n", &ad, &ab, &as, &af, &n) != 4 || a[n]) return 0;
if (sscanf(b, "%8x:%2x:%2x.%1x %n", &bd, &bb, &bs, &bf, &m) != 4 || b[m]) return 0;
return ad == bd && ab == bb && as == bs && af == bf;
}
static void gpu_nvidia_parse(char *text, gpu_frame *frame)
{
char *line;
unsigned char seen[GPU_MAX] = {0};
size_t i;
while ((line = strsep(&text, "\n"))) {
char *bus = strsep(&line, ",");
if (!line) continue;
for (i = 0; i < gpu_count; i++) {
if (gpu_devices[i].backend != 2 || !gpu_bus_equal(bus, gpu_devices[i].bus)) continue;
frame->samples[i] = seen[i] ? gpu_unknown : gpu_csv(line);
seen[i] = 1;
break;
}
}
}
static void gpu_nvidia(gpu_frame *frame)
{
char text[GPU_MAX * 256];
char *const args[] = {"nvidia-smi",
"--query-gpu=pci.bus_id,utilization.gpu,memory.used,memory.total,temperature.gpu",
"--format=csv,noheader,nounits", NULL};
if (gpu_command("/usr/bin/nvidia-smi", args, text, sizeof(text)))
gpu_nvidia_parse(text, frame);
}
static void gpu_publish(size_t i, gpu_sample sample)
{
pthread_mutex_lock(&gpu_lock);
gpu_cache[i] = sample;
gpu_stamp[i] = gpu_milliseconds();
pthread_mutex_unlock(&gpu_lock);
}
/* Each AMD adapter has an independent worker; one NVIDIA worker queries all
* NVIDIA cards within one deadline. A stalled sysfs driver cannot stall peers. */
static void *gpu_watch(void *arg)
{
size_t index = (gpu_adapter *)arg - gpu_devices, i;
while (!atomic_load(&gpu_stopping)) {
if (gpu_devices[index].backend == 1) {
gpu_publish(index, gpu_amd(&gpu_devices[index]));
} else {
gpu_frame frame = {.count = gpu_count};
for (i = 0; i < gpu_count; i++) frame.samples[i] = gpu_unknown;
gpu_nvidia(&frame);
pthread_mutex_lock(&gpu_lock);
for (i = 0; i < gpu_count; i++) if (gpu_devices[i].backend == 2) {
gpu_cache[i] = frame.samples[i];
gpu_stamp[i] = gpu_milliseconds();
}
pthread_mutex_unlock(&gpu_lock);
}
for (i = 0; i < 100 && !atomic_load(&gpu_stopping); i++) poll(NULL, 0, 20);
}
return NULL;
}
static int gpu_start(void)
{
size_t i;
int nvidia = 0, failed = 0;
atomic_store(&gpu_stopping, 0);
for (i = 0; i < gpu_count; i++) {
if (!gpu_devices[i].backend || (gpu_devices[i].backend == 2 && nvidia++)) continue;
if (pthread_create(&gpu_threads[gpu_thread_count], NULL, gpu_watch, &gpu_devices[i])) failed = 1;
else gpu_thread_count++;
}
return failed;
}
/* One shared shutdown deadline, not one per adapter. A stuck kernel read may
* outlive this call; its static storage remains valid until process exit.
* Never redetect/restart after a failed stop. */
static int gpu_stop(void)
{
struct timespec deadline;
long long end = gpu_milliseconds() + 1000;
size_t i;
int failed = 0;
atomic_store(&gpu_stopping, 1);
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec++;
for (i = 0; i < gpu_thread_count; i++) {
if (pthread_timedjoin_np(gpu_threads[i], NULL, &deadline)) {
pthread_detach(gpu_threads[i]);
failed = 1;
}
}
gpu_thread_count = 0;
if (!failed && gpu_pending > 0) {
int status;
pid_t result;
do {
result = waitpid(gpu_pending, &status, WNOHANG);
if (result == gpu_pending || (result < 0 && errno == ECHILD)) {
gpu_pending = -1;
break;
}
poll(NULL, 0, 5);
} while (gpu_milliseconds() < end);
if (gpu_pending > 0) failed = 1;
}
return failed;
}
static gpu_frame gpu_snapshot(void)
{
gpu_frame frame = {.count = gpu_count};
size_t i;
pthread_mutex_lock(&gpu_lock);
for (i = 0; i < gpu_count; i++)
frame.samples[i] = !gpu_stamp[i] || gpu_milliseconds() - gpu_stamp[i] > 5000 ? gpu_unknown : gpu_cache[i];
pthread_mutex_unlock(&gpu_lock);
return frame;
}
static size_t gpu_page_first(size_t count, unsigned long long elapsed)
{
return count > 2 ? (elapsed / 5000 % ((count + 1) / 2)) * 2 : 0;
}
/* Main thread only. Advance at most one pair per actual draw, so -r 10 (or
* leaving the summary screen) cannot alias the cycle and hide half the GPUs. */
static size_t gpu_display_first(size_t count, long long now)
{
if (count > 2 && now >= gpu_page_due) {
gpu_page = (gpu_page + 1) % ((count + 1) / 2);
gpu_page_due = now + 5000;
}
return gpu_page_first(count, gpu_page * 5000ULL);
}
/* Each line fits the 52-pixel column (13 small-font character cells).
* Separate used/total rows retain useful precision, even on large cards.
*/
static void gpu_memory(char *text, size_t size, long long bytes)
{
const char *unit = "MiB";
double value = (double)bytes / 1048576;
if (bytes < 0) { snprintf(text, size, "N/A"); return; }
if (value >= 1024) { value /= 1024; unit = "GiB"; }
if (value >= 1024) { value /= 1024; unit = "TiB"; }
if (value >= 1024) { value /= 1024; unit = "PiB"; }
snprintf(text, size, "%.1f%s", value, unit);
}
#endif
+284
View File
@@ -0,0 +1,284 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#define _GNU_SOURCE 1
#include "../gpu.h"
#include <assert.h>
#include <dirent.h>
static int descriptors(void)
{
DIR *dir = opendir("/proc/self/fd");
struct dirent *entry;
int count = 0;
assert(dir);
while ((entry = readdir(dir)))
if (entry->d_name[0] != '.') count++;
closedir(dir);
return count;
}
static void unknown(gpu_sample s)
{
assert(s.busy == -1 && s.used == -1 && s.total == -1 && s.temp == LLONG_MIN);
}
static gpu_sample test_command(const char *path, char *const args[])
{
char text[256];
return gpu_command(path, args, text, sizeof(text)) ? gpu_csv(text) : gpu_unknown;
}
static void *cache_writer(void *arg)
{
size_t index = *(size_t *)arg;
int i;
for (i = 0; i < 20000; i++)
gpu_publish(index, (gpu_sample){i % 101, i, i + 1, (long long)index});
return NULL;
}
static void cache_concurrency(void)
{
pthread_t writers[2];
size_t indices[2] = {0, 1}, i;
int repeat;
gpu_count = 2;
for (i = 0; i < 2; i++) {
gpu_publish(i, (gpu_sample){0, 0, 1, (long long)i});
assert(!pthread_create(&writers[i], NULL, cache_writer, &indices[i]));
}
for (repeat = 0; repeat < 20000; repeat++) {
gpu_frame frame = gpu_snapshot();
assert(frame.count == 2);
for (i = 0; i < 2; i++) {
assert(frame.samples[i].total == frame.samples[i].used + 1);
assert(frame.samples[i].busy == frame.samples[i].used % 101);
assert(frame.samples[i].temp == (long long)i);
}
}
for (i = 0; i < 2; i++) assert(!pthread_join(writers[i], NULL));
}
static void parent_death(void)
{
pid_t child, grandchild = 0, result;
long long deadline;
char path[96];
int status;
assert(!prctl(PR_SET_CHILD_SUBREAPER, 1));
child = fork();
assert(child >= 0);
if (!child) {
char *const args[] = {"sleep", "10", NULL};
test_command("/usr/bin/sleep", args);
_exit(0);
}
snprintf(path, sizeof(path), "/proc/%ld/task/%ld/children", (long)child, (long)child);
deadline = gpu_milliseconds() + 500;
do {
FILE *file = fopen(path, "r");
long id;
if (file) {
if (fscanf(file, "%ld", &id) == 1) grandchild = (pid_t)id;
fclose(file);
}
if (!grandchild) poll(NULL, 0, 5);
} while (!grandchild && gpu_milliseconds() < deadline);
assert(grandchild > 0);
assert(!kill(child, SIGKILL));
assert(waitpid(child, &status, 0) == child);
deadline = gpu_milliseconds() + 1000;
do {
result = waitpid(grandchild, &status, WNOHANG);
if (!result) poll(NULL, 0, 5);
} while (!result && gpu_milliseconds() < deadline);
assert(result == grandchild && WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL);
assert(!prctl(PR_SET_CHILD_SUBREAPER, 0));
}
int main(int argc, char **argv)
{
gpu_sample s;
char text[256], memory[32];
char *const sleeper[] = {"sleep", "10", NULL};
char *const failure[] = {"false", NULL};
long long start;
int status;
pid_t result;
int fd_count, repeat;
assert(gpu_page_first(0, 5000) == 0);
assert(gpu_page_first(2, 5000) == 0);
assert(gpu_page_first(3, 5000) == 2);
assert(gpu_page_first(3, 10000) == 0);
for (repeat = 0; repeat < GPU_MAX; repeat += 2)
assert(gpu_page_first(GPU_MAX, (unsigned long long)repeat / 2 * 5000) == (size_t)repeat);
gpu_page = 0;
gpu_page_due = 5000;
assert(gpu_display_first(5, 0) == 0);
assert(gpu_display_first(5, 10000) == 2);
assert(gpu_display_first(5, 20000) == 4);
assert(gpu_display_first(5, 30000) == 0);
assert(gpu_display_first(5, 30001) == 0);
assert(gpu_selection_valid("auto") && gpu_selection_valid("off"));
assert(gpu_selection_valid("0000:AB:1f.7"));
assert(!gpu_selection_valid("0000:01:20.0"));
assert(!gpu_selection_valid("0000:01:00.8"));
assert(!gpu_selection_valid("-001:01:00.0"));
assert(!gpu_selection_valid("../../passwd"));
assert(!gpu_selection_valid("0000:01:00.0;id"));
assert(gpu_number(" 0\n", 0, 100, -1) == 0);
assert(gpu_number("100", 0, 100, -1) == 100);
assert(gpu_number("101", 0, 100, -1) == -1);
assert(gpu_number("12oops", 0, 100, -1) == -1);
assert(gpu_number("", 0, 100, -1) == -1);
assert(gpu_number("N/A", 0, 100, -1) == -1);
assert(gpu_number("999999999999999999999999", 0, LLONG_MAX, -1) == -1);
strcpy(text, "100, 12288, 16384, -5\n");
s = gpu_csv(text);
assert(s.busy == 100 && s.used == 12884901888LL &&
s.total == 17179869184LL && s.temp == -5);
strcpy(text, "N/A, [Not Supported], 16384, 0\n");
s = gpu_csv(text);
assert(s.busy == -1 && s.used == -1 && s.total == 17179869184LL && s.temp == 0);
strcpy(text, "0, 0, 0, N/A\n");
s = gpu_csv(text);
assert(s.busy == 0 && s.used == 0 && s.total == -1 && s.temp == LLONG_MIN);
strcpy(text, "0, 20, 10, 35\n");
assert(gpu_csv(text).used == -1);
strcpy(text, "0, 1, 2"); unknown(gpu_csv(text));
strcpy(text, "0, 1, 2, 3, 4"); unknown(gpu_csv(text));
strcpy(text, "N/A, N/A, N/A, N/A"); unknown(gpu_csv(text));
gpu_memory(memory, sizeof(memory), -1); assert(!strcmp(memory, "N/A"));
gpu_memory(memory, sizeof(memory), 19 * 1048576); assert(!strcmp(memory, "19.0MiB"));
gpu_memory(memory, sizeof(memory), 17179869184LL); assert(!strcmp(memory, "16.0GiB"));
gpu_memory(memory, sizeof(memory), LLONG_MAX); assert(strlen(memory) <= 11);
assert(!gpu_detect("off") && !gpu_count);
assert(!gpu_detect("ffff:ff:1f.7") && gpu_count == 1 && !gpu_devices[0].backend);
gpu_cache[0] = s;
gpu_stamp[0] = gpu_milliseconds() - 6000;
unknown(gpu_snapshot().samples[0]);
{
gpu_frame frame = {.count = 3};
gpu_count = 3;
for (repeat = 0; repeat < 3; repeat++) {
snprintf(gpu_devices[repeat].bus, 32, "0000:%02x:00.0", repeat + 1);
gpu_devices[repeat].backend = repeat == 1 ? 1 : 2;
frame.samples[repeat] = gpu_unknown;
}
frame.samples[1] = (gpu_sample){77, 1, 2, 42};
strcpy(text, "00000000:03:00.0, 90, 4, 8, 55\n00000000:01:00.0, N/A, 1, 2, N/A\n");
gpu_nvidia_parse(text, &frame);
assert(frame.samples[0].busy == -1 && frame.samples[0].used == 1048576);
assert(frame.samples[1].busy == 77 && frame.samples[1].used == 1);
assert(frame.samples[2].busy == 90 && frame.samples[2].temp == 55);
strcpy(text, "00000000:03:00.0, 1, 4, 8, 55\n0000:03:00.0, 2, 4, 8, 55\n");
gpu_nvidia_parse(text, &frame);
unknown(frame.samples[2]);
assert(!gpu_bus_equal("0000:01:00.0junk", "0000:01:00.0"));
assert(gpu_bus_equal("00000000:AB:00.0 ", "0000:ab:00.0"));
assert(!gpu_bus_equal("0001:01:00.0", "0000:01:00.0"));
for (repeat = 0; repeat < 3; repeat++) gpu_publish(repeat, frame.samples[repeat]);
gpu_stamp[0] = gpu_milliseconds() - 6000;
frame = gpu_snapshot();
unknown(frame.samples[0]);
assert(frame.samples[1].busy == 77);
unknown(frame.samples[2]);
/* Missing AMD path is independent of the populated neighboring cache. */
snprintf(gpu_devices[0].device, PATH_MAX, "/nonexistent/g15stats-test");
gpu_publish(0, gpu_amd(&gpu_devices[0]));
assert(gpu_snapshot().samples[1].busy == 77);
}
/* Real processes exercise the deadline and reaping, not fake GPU files. */
start = gpu_milliseconds();
unknown(test_command("/usr/bin/sleep", sleeper));
assert(gpu_milliseconds() - start >= 750);
assert(gpu_milliseconds() - start < 1500);
poll(NULL, 0, 50);
unknown(test_command("/usr/bin/false", failure));
result = waitpid(-1, &status, WNOHANG);
assert(result == -1 && errno == ECHILD);
unknown(test_command("/nonexistent/g15stats-test", failure));
fd_count = descriptors();
for (repeat = 0; repeat < 20; repeat++)
unknown(test_command("/usr/bin/false", failure));
assert(descriptors() == fd_count);
{
char payload[12001], output[GPU_MAX * 256];
char *const args[] = {"printf", "%s", payload, NULL};
memset(payload, 'x', sizeof(payload) - 1);
payload[sizeof(payload) - 1] = 0;
assert(gpu_command("/usr/bin/printf", args, output, sizeof(output)));
assert(!strcmp(payload, output));
assert(!gpu_command("/usr/bin/printf", args, output, 256));
poll(NULL, 0, 50);
unknown(test_command("/usr/bin/false", failure));
}
parent_death();
cache_concurrency();
if (argc == 2) {
gpu_frame frame;
size_t i;
assert(!gpu_detect(argv[1]) && gpu_count);
assert(!gpu_start());
start = gpu_milliseconds();
do {
frame = gpu_snapshot();
for (i = 0; i < frame.count; i++) if (frame.samples[i].total <= 0) break;
if (i == frame.count) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
for (i = 0; i < frame.count; i++) {
char path[PATH_MAX + 32];
s = frame.samples[i];
if (i) assert(strcmp(gpu_devices[i-1].bus, gpu_devices[i].bus) < 0);
snprintf(path, sizeof(path), "%s/vendor", gpu_devices[i].device);
FILE *vendor = fopen(path, "r");
unsigned id;
assert(vendor && fscanf(vendor, "%x", &id) == 1);
fclose(vendor);
assert(id == (gpu_devices[i].backend == 1 ? 0x1002U : 0x10deU));
printf("LIVE %zu %s vendor=%04x backend=%d utilization=%lld used=%lld total=%lld temp=%lld\n",
i+1, gpu_devices[i].bus, id, gpu_devices[i].backend, s.busy, s.used, s.total, s.temp);
assert(s.busy >= 0 && s.used >= 0 && s.total > 0 && s.temp != LLONG_MIN);
}
for (repeat = 0; repeat < 100000; repeat++) {
frame = gpu_snapshot();
assert(frame.count == gpu_count);
for (i = 0; i < frame.count; i++) assert(frame.samples[i].total > 0);
}
start = gpu_milliseconds();
assert(!gpu_stop());
assert(gpu_milliseconds() - start < 1200);
puts("PASS all live adapters, sysfs vendor identities, concurrent snapshots and bounded shutdown");
/* Read-only failure injection: point just the stopped AMD collector at
* a nonexistent path, then prove the actual NVIDIA peer remains live. */
if (gpu_count > 1) {
size_t amd = GPU_MAX, nv = GPU_MAX;
char saved[PATH_MAX];
for (i = 0; i < gpu_count; i++) {
if (gpu_devices[i].backend == 1) amd = i;
if (gpu_devices[i].backend == 2) nv = i;
}
if (amd < GPU_MAX && nv < GPU_MAX) {
strcpy(saved, gpu_devices[amd].device);
strcpy(gpu_devices[amd].device, "/nonexistent/g15stats-test");
for (i = 0; i < gpu_count; i++) gpu_publish(i, gpu_unknown);
assert(!gpu_start());
start = gpu_milliseconds();
do {
frame = gpu_snapshot();
if (frame.samples[nv].total > 0) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
assert(frame.samples[nv].total > 0 && frame.samples[nv].busy >= 0);
unknown(frame.samples[amd]);
assert(!gpu_stop());
strcpy(gpu_devices[amd].device, saved);
puts("PASS failed AMD collector isolated from real live NVIDIA peer");
}
}
}
puts("PASS GPU parsing, units, missing data, expiry, selection, deadline, reap and parent death");
return 0;
}
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# Convert existing render-test exports without trimming, resampling or labels
# inside the framebuffer. ImageMagick is optional, not a make check dependency.
set -eu
if [ "$#" -ne 1 ]; then
echo "Usage: sh tests/render-images.sh ABSOLUTE_OUTPUT_DIRECTORY" >&2
exit 2
fi
cd "$1"
for pgm in ./*-[01].pgm; do
[ -f "$pgm" ] || continue
base=${pgm%.pgm}
[ "$(wc -c < "$base.bin")" -eq 860 ]
{ printf 'P4\n160 43\n'; cat "$base.bin"; } > "$base.pbm"
# Compare the packed 1-bit buffer independently with the PGM export.
compare -metric AE "$base.pbm" "$pgm" null: 2> "$base-pixels.log"
convert "$pgm" +repage -type TrueColor "PNG24:$base-native.png"
convert "$base-native.png" -sample 800% +repage "PNG24:$base-8x.png"
convert "$base-8x.png" -sample 160x43! +repage "PNG24:$base-roundtrip.png"
compare -metric AE "$pgm" "$base-native.png" null: 2>> "$base-pixels.log"
compare -metric AE "$pgm" "$base-roundtrip.png" null: 2>> "$base-pixels.log"
rm "$base-roundtrip.png"
# Smaller contact tiles avoid shrinking wide sheets in image viewers.
# Keep the eightfold originals available for individual pixel inspection.
convert "$base-native.png" -sample 400% +repage "PNG24:$base-4x.png"
done
for group in off nvidia amd single dual large unknown odd maximum; do
[ -f "$group-0-4x.png" ] || continue
montage -font DejaVu-Sans -pointsize 14 -background '#dddddd' \
-label "$group: five-row summary (4x)" "$group-0-4x.png" \
-label "$group: four-row summary (4x)" "$group-1-4x.png" \
-tile 1x2 -geometry +8+8 "PNG24:contact-$group.png"
height=$(identify -format '%h' "contact-$group.png")
for variant in 0 1; do
top=$((8 + variant * height / 2))
convert "contact-$group.png" -crop "640x172+8+$top" +repage \
"PNG24:$group-tile-check.png"
compare -metric AE "$group-$variant-4x.png" "$group-tile-check.png" \
null: 2>> "$group-contact-pixels.log"
done
rm "$group-tile-check.png"
done
if [ -f contact-off.png ] && [ -f contact-nvidia.png ] && [ -f contact-amd.png ]; then
convert contact-off.png contact-nvidia.png contact-amd.png -append +repage \
PNG24:contact-legacy.png
fi
if [ -f contact-dual.png ] && [ -f contact-large.png ] && [ -f contact-unknown.png ]; then
convert contact-dual.png contact-large.png contact-unknown.png -append +repage \
PNG24:contact-metrics.png
fi
if [ -f contact-odd.png ] && [ -f contact-maximum.png ]; then
convert contact-odd.png contact-maximum.png -append +repage PNG24:contact-paging.png
fi
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Native-pixel checks only: no image conversion, daemon or input-device access.
set -eu
renderer=${RENDER_TEST:-./render-test}
out=$(mktemp -d "${TMPDIR:-/tmp}/g15stats-layouts-XXXXXX")
trap 'rm -rf "$out"' EXIT HUP INT TERM
"$renderer" off "$out/off"
"$renderer" off "$out/single" 1
"$renderer" off "$out/dual" 2
"$renderer" off "$out/large" 2 0 large
"$renderer" off "$out/unknown" 2 0 unknown
"$renderer" off "$out/odd" 3 1
"$renderer" off "$out/maximum" 64 31
+230
View File
@@ -0,0 +1,230 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Exercise the real renderer without any daemon connection or input thread.
*/
#define main g15stats_application_main
#include "../g15stats.c"
#undef main
#include <assert.h>
/* Independent font-bitmap oracle: do not repeat renderString on a second
* canvas, since that also repeats any clipping/erasure in the renderer.
* The default fonts have zero gap and one blank trailing pixel column.
* Compare every cell, including blank pixels, against the font file's bits. */
static void assert_text(g15canvas *canvas, const char *text, int size, int x, int y, int right)
{
g15font *font = g15r_requestG15DefaultFont(size);
assert(font && font->default_gap == 0);
assert(font->font_height == font->ascender_height);
for (; *text; text++) {
unsigned char c = (unsigned char)*text;
int width = font->glyph[c].width;
int stride = (width + 7) / 8;
assert(font->active[c] && font->glyph[c].buffer);
assert(x >= 0 && x + width <= 160);
assert(y >= 0 && y + font->font_height <= 43);
for (int dy = 0; dy < font->font_height; dy++)
for (int dx = 0; dx < width; dx++) {
int bit = !!(font->glyph[c].buffer[dy * stride + dx / 8] &
(0x80 >> (dx % 8)));
/* A 13-character first-column header's final blank advance
* pixel shares x=105 with the next divider; no ink may do so. */
if (x + dx >= right) {
assert(!text[1] && dx == width - 1 && !bit);
continue;
}
if (g15r_getPixel(canvas, x + dx, y + dy) != bit) {
fprintf(stderr, "Glyph %c pixel (%d,%d): expected %d, got %d\n",
c, x + dx, y + dy, bit,
g15r_getPixel(canvas, x + dx, y + dy));
assert(0);
}
}
x += width;
}
}
int main(int argc, char **argv)
{
g15canvas canvas, before;
char tmp[MAX_LINES];
int x, y, variant;
size_t i;
g15font *font = g15r_requestG15DefaultFont(G15_TEXT_SMALL);
assert(font);
assert(g15r_testG15FontWidth(font, "T 8192.0PiB") <= 52);
assert(g15r_testG15FontWidth(font, "UTL 100%") <= 52);
{
char lines[5][32];
compact_ram_lines(lines, LLONG_MAX, LLONG_MAX);
assert(strstr(lines[1], "PiB") && strstr(lines[2], "PiB"));
assert(g15r_testG15FontWidth(font, lines[1]) <= 50);
assert(g15r_testG15FontWidth(font, lines[2]) <= 50);
compact_ram_lines(lines, 19 * 1048576, 17179869184LL);
assert(!strcmp(lines[1], "R 19.0MiB"));
assert(!strcmp(lines[2], "T 16.0GiB"));
}
memset(&canvas, 0xa5, sizeof(canvas));
init_canvas(&canvas);
assert(!canvas.mode_xor && !canvas.mode_reverse && !canvas.mode_cache);
for (y = 0; y < 43; y++) for (x = 0; x < 160; x++)
assert(!g15r_getPixel(&canvas, x, y));
/* Original full-width geometry must remain exact with the column off. */
init_canvas(&before);
summary_gpu = 0;
drawBar_both(&canvas, 1, 8, 37, 100, 63, 100);
g15r_drawBar(&before, BAR_START, 1, BAR_END, 8, G15_COLOR_BLACK, 37, 100, 4);
drawBar_reversed(&before, BAR_START, 1, BAR_END, 8, G15_COLOR_BLACK, 63, 100, 5);
assert(!memcmp(canvas.buffer, before.buffer, sizeof(canvas.buffer)));
glibtop_init();
/* No device fixtures: sample the actual selected GPU when requested. */
assert(!gpu_detect(argc > 1 ? argv[1] : "off"));
if (gpu_count && gpu_devices[0].backend) {
long long start = gpu_milliseconds();
assert(!gpu_start());
do {
gpu_frame frame = gpu_snapshot();
for (i = 0; i < frame.count; i++) if (frame.samples[i].total < 0) break;
if (i == frame.count) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
assert(i == gpu_count);
assert(!gpu_stop());
}
/* No-argument make check retains the worst-width fixture. Explicit off
* must exercise the application's actual zero-GPU/full-width layout.
* Optional count/page/profile arguments are in-memory visual cases only. */
if (!gpu_count && (argc == 1 || argc > 3)) {
gpu_count = argc > 3 ? (size_t)atoi(argv[3]) : 2;
assert(gpu_count >= 1 && gpu_count <= GPU_MAX);
gpu_devices[0].backend = 2;
gpu_devices[1].backend = 1;
strcpy(gpu_devices[0].bus, "0000:01:00.0");
strcpy(gpu_devices[1].bus, "0000:11:00.0");
gpu_publish(0, (gpu_sample){100, LLONG_MAX, LLONG_MAX, -273});
gpu_publish(1, (gpu_sample){100, LLONG_MAX, LLONG_MAX, 1000});
for (i = 2; i < gpu_count; i++) {
gpu_devices[i].backend = 1;
snprintf(gpu_devices[i].bus, 32, "0001:%02x:1f.7", (unsigned)i);
gpu_publish(i, i % 2 ? gpu_unknown : (gpu_sample){100, LLONG_MAX, LLONG_MAX, -273});
}
if (gpu_count > 2) {
/* Exercise every pair at the maximum inventory, including the
* 13-character AMD64 header and an odd final card. */
for (i = 0; i < (gpu_count + 1) / 2; i++) {
gpu_page = i;
gpu_page_due = LLONG_MAX;
g15r_clearScreen(&canvas, G15_COLOR_WHITE);
draw_gpu_column(&canvas);
for (y = 35; y < 43; y++) for (x = 0; x < 160; x++)
assert(!g15r_getPixel(&canvas, x, y));
}
/* Keep the final pair for the exported preview and pixel checks. */
}
}
if (argc > 4) {
gpu_page = (size_t)atoi(argv[4]);
assert(gpu_page < (gpu_count + 1) / 2);
gpu_page_due = LLONG_MAX;
}
if (argc > 5) {
assert(!strcmp(argv[5], "large") || !strcmp(argv[5], "unknown"));
for (i = 0; i < gpu_count; i++)
gpu_publish(i, !strcmp(argv[5], "unknown") ? gpu_unknown :
(gpu_sample){100, 100LL << 30, 192LL << 30, 1000});
}
for (i = 0; i < gpu_count; i++) {
char lines[5][32];
int row;
gpu_lines(i, gpu_cache[i], lines);
for (row = 0; row < 5; row++) {
assert(g15r_testG15FontWidth(font, lines[row]) <= 52);
printf("GPU %zu row %d: %s\n", i + 1, row, lines[row]);
}
}
for (variant = 0; variant < 2; variant++) {
cycle = SCREEN_SUMMARY;
mode[SCREEN_SUMMARY] = variant;
have_temp = have_fan = 0; /* Keep legacy generic hwmon probing out of QA. */
summary_gpu = gpu_count != 0;
draw_cpu_screen_multicore(&canvas, tmp, 0);
before = canvas;
draw_gpu_column(&canvas);
for (y = 0; y < 43; y++)
for (x = 0; x < 160; x++)
if (x < (gpu_count > 1 ? 51 : 105) || y >= 35)
assert(g15r_getPixel(&canvas, x, y) == g15r_getPixel(&before, x, y));
/* Re-render each column on an empty canvas: exact pixels prove neither
* neighboring column nor CPU text overlaps it, including edge glyphs. */
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_page_first(frame.count, gpu_page * 5000ULL);
for (i = first; i < frame.count && i < first + 2; i++) {
g15canvas isolated;
char lines[5][32];
int row, left = frame.count == 1 ? 108 : 54 + (int)(i-first) * 54;
init_canvas(&isolated);
gpu_lines(i, frame.samples[i], lines);
for (row = 0; row < 5; row++)
g15r_renderString(&isolated, (unsigned char *)lines[row], 0, G15_TEXT_SMALL, left, 1+row*7);
for (y = 0; y < 35; y++) for (x = left; x < (left == 54 ? 105 : 160); x++)
assert(g15r_getPixel(&canvas, x, y) == g15r_getPixel(&isolated, x, y));
}
}
summary_gpu = 0;
print_time_info(&canvas, tmp);
assert(g15r_testG15FontWidth(font, tmp) <= 160);
assert_text(&canvas, tmp, G15_TEXT_SMALL,
80 - (int)strlen(tmp) * 4 / 2, INFO_ROW, 160);
if (gpu_count < 2) {
const char *label = "TOTAL";
for (int row = 0; row < 5; row++) {
char letter[2] = {label[row], 0};
assert_text(&canvas, letter, G15_TEXT_MED,
gpu_count ? 98 : TEXT_RIGHT, row * 7, 160);
}
}
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_page_first(frame.count, gpu_page * 5000ULL);
for (i = first; i < frame.count && i < first + 2; i++) {
char lines[5][32];
int left = frame.count == 1 ? 108 : 54 + (int)(i-first) * 54;
gpu_lines(i, frame.samples[i], lines);
for (int row = 0; row < 5; row++)
assert_text(&canvas, lines[row], G15_TEXT_SMALL, left, 1+row*7,
left == 54 ? 105 : 160);
}
}
if (argc > 2) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s-%d.bin", argv[2], variant);
FILE *raw = fopen(path, "wb");
assert(raw);
assert(fwrite(canvas.buffer, 1, 160 * 43 / 8, raw) == 160 * 43 / 8);
assert(!fclose(raw));
snprintf(path, sizeof(path), "%s-%d.pgm", argv[2], variant);
FILE *out = fopen(path, "w");
assert(out);
fprintf(out, "P2\n160 43\n255\n");
for (y = 0; y < 43; y++) {
for (x = 0; x < 160; x++) fprintf(out, "%d ", g15r_getPixel(&canvas, x, y) ? 0 : 255);
fputc('\n', out);
}
assert(!fclose(out));
}
}
if (argc > 2) {
FILE *out = fopen(argv[2], "w");
assert(out);
fprintf(out, "P2\n160 43\n255\n");
for (y = 0; y < 43; y++) {
for (x = 0; x < 160; x++)
fprintf(out, "%d ", g15r_getPixel(&canvas, x, y) ? 0 : 255);
fputc('\n', out);
}
assert(!fclose(out));
}
glibtop_close();
puts("PASS real LCD buffer: initialization, original geometry, both summary variants, independent glyph bits, complete TOTAL/footer, column bounds");
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Full application startup/draw loop, real libgtop and bitmap renderer.
* Only daemon transport is replaced. No TCP, USB, or input-device access.
* Default: GPU off, three frames. Optional: runtime-test auto 21 verifies
* real read-only GPU samples over twenty seconds (not a physical LCD test).
*/
#define main g15stats_application_main
#include "../g15stats.c"
#undef main
#include <assert.h>
static unsigned frames, target = 3;
static int peer = -1;
/* Catch the ABI-unsafe initializer even with a non-TTF renderer in CI.
* Do not stub any bitmap drawing or host statistics functions. */
void g15r_initCanvas(g15canvas *canvas)
{
(void)canvas;
fputs("FAIL: bitmap client called ABI-dependent g15r_initCanvas\n", stderr);
abort();
}
int new_g15_screen(int type)
{
int pair[2];
assert(type == G15_G15RBUF);
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair));
peer = pair[1]; /* No key events: actual keyboard thread blocks in recv. */
return pair[0];
}
int g15_send(int fd, char *buffer, unsigned int length)
{
unsigned ink = 0;
gpu_frame frame = gpu_snapshot();
(void)fd;
assert(length == G15_BUFFER_LEN);
for (unsigned i = 0; i < 160 * 43 / 8; i++) ink += !!buffer[i];
assert(ink);
fprintf(stderr, "runtime frame %u: %u ink bytes, %zu GPUs\n",
++frames, ink, frame.count);
if (frames == target) {
glibtop_cpu cpu;
glibtop_mem mem;
glibtop_get_cpu(&cpu);
glibtop_get_mem(&mem);
assert(cpu.total > 0 && mem.total > 0 && mem.free <= mem.total);
fprintf(stderr, "host stats: CPU ticks=%llu RAM total=%llu free=%llu\n",
(unsigned long long)cpu.total, (unsigned long long)mem.total,
(unsigned long long)mem.free);
for (size_t i = 0; i < frame.count; i++) {
gpu_sample s = frame.samples[i];
fprintf(stderr, "%s: busy=%lld used=%lld total=%lld temp=%lld\n",
gpu_devices[i].bus, s.busy, s.used, s.total, s.temp);
assert(s.busy >= 0 && s.used >= 0 && s.total > 0 && s.temp != LLONG_MIN);
}
assert(!gpu_stop());
/* Full main is intentionally endless. Terminate without closing the
* peer first (which would wake the legacy keyboard loop with EOF). */
fputs("PASS full main, isolated transport, real renderer/collectors\n", stderr);
_Exit(0);
}
return length;
}
int main(int argc, char **argv)
{
char *args[] = {"g15stats", "--gpu", argc > 1 ? argv[1] : "off", NULL};
if (argc > 2) {
long n = gpu_number(argv[2], 3, 30, -1);
assert(n >= 3);
target = (unsigned)n;
}
alarm(target + 10); /* Failure bound, not a passing timeout. */
return g15stats_application_main(3, args);
}