From 7d0b87bc45577c52ed3712182eed723818894dc9 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Wed, 7 Oct 2026 19:31:25 -0700 Subject: [PATCH] A GPU with nothing measurable is one line, not half the Processes page The Mac's Asahi GPU exposes no busy counter, clock, sensor or memory reading, so its box was a graph that could never draw beside a column of dashes. A GPU with nothing to read now gets one line (driver, "nothing measurable" or the stall, and whether a process has it open), its row shrinks to fit, and the Gpu% column and gpu sort are dropped when no GPU on the machine can light them. QoL #3 from bug bash 6 (BUGS.md). Co-Authored-By: Claude Opus 5.5 --- BUGS.md | 11 +++++++++-- web/src/Top.tsx | 33 ++++++++++++++++++++++++++------- web/src/styles.css | 8 +++++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/BUGS.md b/BUGS.md index 91c818c..7299899 100644 --- a/BUGS.md +++ b/BUGS.md @@ -712,8 +712,15 @@ before its own login). session unused for seven days signs itself out (`auth.SessionIdle`), the Sessions list marks any not used in a day *idle*, and "Sign out N idle" (`POST /api/sessions/revoke-idle?hours=24`) drops those while leaving every device used since. -3. **The Mac's GPU box takes half the Processes page** to say Asahi exposes no counters; a - GPU with nothing measurable could be one line. +3. ✅ **The Mac's GPU box takes half the Processes page** — built 2026-10-07. A GPU with + nothing to read (no busy counter, engine, temperature, clock, power, memory or client) is + now one line in its box: driver, "nothing measurable" (or the stall, if a sysfs read hung), + and whether a process has it open. Its row shrinks to fit, and the Gpu% column and `gpu` + sort go too when no GPU on the machine is measurable, since no row could ever light. A + client opening the device brings the full box back. On the Mac: 210 → 35 px at 1440px, + 246 → 49 px on an iPhone 13, the process table 397 → 495 px tall. Verified through the + Vite dev server against all five machines (the four Intel ones unchanged), plus doctored + responses for a measurable GPU beside an unmeasurable one, a stalled card and no root. 4. **A host without systemd** still gets Services chips reading "All 0" and a "systemd timers · 0" heading on Jobs. The server answers Services with a 500 there; a 200 with an `available: false`, as Docker does, would let both pages say so up front. diff --git a/web/src/Top.tsx b/web/src/Top.tsx index 9e115ec..2866688 100644 --- a/web/src/Top.tsx +++ b/web/src/Top.tsx @@ -348,10 +348,13 @@ export default function Top() { const seg = segOf(g.name), n = all.slice(0, i).filter((o) => segOf(o.name) === seg).length return { g, stem: 'gpu.' + seg + (n ? `_${n + 1}` : '') } }) + // a GPU with nothing to read can't light a process's Gpu% either: the column, and its sort, + // are for a machine where at least one GPU says something + const gpuCol = gpus.some(({ g }) => !unmeasured(g)) return ( -
+
{clock.toLocaleTimeString([], { hour12: false })} @@ -392,8 +395,8 @@ export default function Top() { {gpus.length > 0 && (
{gpus.map(({ g, stem }, i) => ( - {g.name}}> - + {g.name}}> + {unmeasured(g) ? : } ))}
@@ -461,17 +464,17 @@ export default function Top() { - {(['pid', 'name', 'mem', ...(gpus.length ? ['gpu' as const] : []), 'cpu'] as const).map((k) => )} + {(['pid', 'name', 'mem', ...(gpuCol ? ['gpu' as const] : []), 'cpu'] as const).map((k) => )} }> -
+
setFilter(e.target.value)} placeholder="filter" spellCheck={false} autoCapitalize="off" autoCorrect="off" />
Pid:Program:Command: - Threads:User:MemB{gpus.length > 0 && Gpu%}Cpu% + Threads:User:MemB{gpuCol && Gpu%}Cpu%
- 0} empty={err || 'no process matches'} /> +
{sel !== null && procs.find((p) => p.pid === sel) ? (() => { const p = procs.find((x) => x.pid === sel)!; return <> {p.pid} {p.name} @@ -569,6 +572,22 @@ function GPUBox({ g, values, tick, advance, cap }: { g: GPU; values: number[]; t ) } +// Nothing on this GPU can be read — no busy counter, engine, temperature, clock, power, memory, +// nor a process with it open: the Mac's Asahi, today. Its box would be a graph that can never +// draw beside a column of dashes, so it is one line instead. A client opening it brings the box back. +const unmeasured = (g: GPU) => !g.usage_src && !g.engines && !(g.temp && g.temp > 0) && !g.mhz && !g.max_mhz && !g.power_w && !g.power_cap_w && !g.fan_rpm && !g.mem_total && !g.mem_used && g.clients.length === 0 +function GPULine({ g }: { g: GPU }) { + return ( +
+ {g.driver}{g.integrated ? ' · integrated' : ''} + {g.stalled + ? readings paused until the console restarts + : nothing measurable} + {g.clients_ok ? 'no process has it open' : 'other users’ processes are hidden'} +
+ ) +} + // One physical drive. `used` is the bytes of the filesystems on it, left out when none can // be attributed to it — a pool member's share of a pool is not a number worth inventing. function Disk({ name, note, size, used, io, temp, mounts, pool }: { name: string; note?: string; size: number; used?: number; io?: number; temp?: number; mounts?: string[]; pool?: string }) { diff --git a/web/src/styles.css b/web/src/styles.css index 007cec5..d363abb 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -666,6 +666,12 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, .top-gpus { grid-column: 1 / -1; grid-row: 2; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px; min-width: 0; } .top.has-gpu .top-mem, .top.has-gpu .top-disks { grid-row: 3; } .top.has-gpu .top-net { grid-row: 4; } .top.has-gpu .top-proc { grid-row: 3 / 5; } .top-gpu { min-height: 210px; } +/* a GPU with nothing to measure is one line, and on a machine with no other its row is only as + tall as that line. Same weight as .top.has-gpu and after it, so the narrower layouts' own + .top.has-gpu, further down, still wins there (their gpu row is already auto). */ +.top.gpu-slim { grid-template-rows: minmax(200px, 1fr) auto auto minmax(170px, 1fr); } +.top-gpu.slim { align-self: start; } /* beside a full gpu box it keeps its one line, not that box's height */ +.t-row.t-gpu-line { flex-wrap: wrap; gap: 0 14px; } .t-gpu-line > span { white-space: nowrap; } .t-gpu-who { margin-left: auto; } /* on a phone it wraps to its own line, still on the right */ .t-gpu { display: flex; gap: 8px; min-width: 0; flex: 1; min-height: 0; } /* the graph and its caption share this box: tall enough for both, so the caption never lands on the stats */ .t-gpu-graph { flex: 1; min-width: 0; min-height: 130px; display: flex; flex-direction: column; } @@ -782,7 +788,7 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, @media (max-width: 800px) { .top { display: flex; flex-direction: column; font-size: 12px; } .top > .tbox { flex: 0 0 auto; } - .top-gpus { display: flex; flex-direction: column; flex: 0 0 auto; } + .top-gpus { display: flex; flex-direction: column; flex: 0 0 auto; } .top-gpu.slim { align-self: stretch; } .t-gpu { flex-direction: column; } .t-gpu-graph { height: 130px; flex: 0 0 130px; } .t-gpu-stats { width: auto; max-height: none; } .t-cpu { flex-direction: column; } .t-cpu-graph { height: 130px; flex: 0 0 130px; } .t-cores { width: auto; max-height: 40vh; } .t-net { flex-direction: column; } .t-net-nums { width: auto; } .t-net-graphs { flex: 0 0 130px; }