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 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-07 19:31:25 -07:00
co-authored by Claude Opus 5.5
parent f3ca3d8f73
commit 7d0b87bc45
3 changed files with 42 additions and 10 deletions
+9 -2
View File
@@ -712,8 +712,15 @@ before its own login).
session unused for seven days signs itself out (`auth.SessionIdle`), the Sessions list 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 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. /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 3. ✅ **The Mac's GPU box takes half the Processes page** — built 2026-10-07. A GPU with
GPU with nothing measurable could be one line. 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 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 · 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. `available: false`, as Docker does, would let both pages say so up front.
+26 -7
View File
@@ -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 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}` : '') } 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 ( return (
<Pal.Provider value={pal}> <Pal.Provider value={pal}>
<div className={'top' + (gpus.length ? ' has-gpu' : '')} style={pal.vars as React.CSSProperties}> <div className={'top' + (gpus.length ? ' has-gpu' : '') + (gpus.length && !gpuCol ? ' gpu-slim' : '')} style={pal.vars as React.CSSProperties}>
<Box n={1} name="cpu" className="top-cpu" right={<> <Box n={1} name="cpu" className="top-cpu" right={<>
<span className="t-clock">{clock.toLocaleTimeString([], { hour12: false })}</span> <span className="t-clock">{clock.toLocaleTimeString([], { hour12: false })}</span>
<span className="t-rate"> <span className="t-rate">
@@ -392,8 +395,8 @@ export default function Top() {
{gpus.length > 0 && ( {gpus.length > 0 && (
<div className="top-gpus"> <div className="top-gpus">
{gpus.map(({ g, stem }, i) => ( {gpus.map(({ g, stem }, i) => (
<Box key={stem} name={`gpu${i}`} className="top-gpu" right={<span className="t-gpu-name" title={[g.name, g.driver, g.bus].filter(Boolean).join(' · ')}>{g.name}</span>}> <Box key={stem} name={`gpu${i}`} className={'top-gpu' + (unmeasured(g) ? ' slim' : '')} right={<span className="t-gpu-name" title={[g.name, g.driver, g.bus].filter(Boolean).join(' · ')}>{g.name}</span>}>
<GPUBox g={g} values={curves.get(stem + '.usage') ?? []} tick={tick} advance={advance} cap={cap} /> {unmeasured(g) ? <GPULine g={g} /> : <GPUBox g={g} values={curves.get(stem + '.usage') ?? []} tick={tick} advance={advance} cap={cap} />}
</Box> </Box>
))} ))}
</div> </div>
@@ -461,17 +464,17 @@ export default function Top() {
<Box n={5} name="proc" className="top-proc" right={<> <Box n={5} name="proc" className="top-proc" right={<>
<span className="t-sorts"> <span className="t-sorts">
{(['pid', 'name', 'mem', ...(gpus.length ? ['gpu' as const] : []), 'cpu'] as const).map((k) => <button key={k} className={sort === k ? 'on' : ''} onClick={() => setSort(k)}>{k}</button>)} {(['pid', 'name', 'mem', ...(gpuCol ? ['gpu' as const] : []), 'cpu'] as const).map((k) => <button key={k} className={sort === k ? 'on' : ''} onClick={() => setSort(k)}>{k}</button>)}
<button className={rev ? 'on' : ''} onClick={() => setRev(!rev)}>reverse</button> <button className={rev ? 'on' : ''} onClick={() => setRev(!rev)}>reverse</button>
</span> </span>
</>}> </>}>
<div className={'t-proc' + (gpus.length ? ' has-gpu' : '')}> <div className={'t-proc' + (gpuCol ? ' has-gpu' : '')}>
<input className="t-filter" value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="filter" spellCheck={false} autoCapitalize="off" autoCorrect="off" /> <input className="t-filter" value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="filter" spellCheck={false} autoCapitalize="off" autoCorrect="off" />
<div className="t-thead"> <div className="t-thead">
<span className="c-pid">Pid:</span><span className="c-name">Program:</span><span className="c-cmd">Command:</span> <span className="c-pid">Pid:</span><span className="c-name">Program:</span><span className="c-cmd">Command:</span>
<span className="c-thr">Threads:</span><span className="c-user">User:</span><span className="c-mem">MemB</span><span className="c-g" />{gpus.length > 0 && <span className="c-gpu">Gpu%</span>}<span className="c-cpu">Cpu%</span> <span className="c-thr">Threads:</span><span className="c-user">User:</span><span className="c-mem">MemB</span><span className="c-g" />{gpuCol && <span className="c-gpu">Gpu%</span>}<span className="c-cpu">Cpu%</span>
</div> </div>
<Rows procs={procs} sel={sel} onSel={setSel} curves={pcurve} gpu={gpus.length > 0} empty={err || 'no process matches'} /> <Rows procs={procs} sel={sel} onSel={setSel} curves={pcurve} gpu={gpuCol} empty={err || 'no process matches'} />
<div className="t-foot"> <div className="t-foot">
{sel !== null && procs.find((p) => p.pid === sel) ? (() => { const p = procs.find((x) => x.pid === sel)!; return <> {sel !== null && procs.find((p) => p.pid === sel) ? (() => { const p = procs.find((x) => x.pid === sel)!; return <>
<span className="t-dim">{p.pid} {p.name}</span><span className="spacer" /> <span className="t-dim">{p.pid} {p.name}</span><span className="spacer" />
@@ -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 (
<div className="t-row t-gpu-line">
<span>{g.driver}{g.integrated ? ' · integrated' : ''}</span>
{g.stalled
? <span className="t-dim" title="a sysfs read of this card stopped answering">readings paused until the console restarts</span>
: <span className="t-dim" title="the driver exposes no busy counter, clock, temperature, power or memory reading">nothing measurable</span>}
<span className="t-dim t-gpu-who" title={g.clients_ok ? undefined : 'the console has no root here'}>{g.clients_ok ? 'no process has it open' : 'other users’ processes are hidden'}</span>
</div>
)
}
// One physical drive. `used` is the bytes of the filesystems on it, left out when none can // 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. // 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 }) { 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 }) {
+7 -1
View File
@@ -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-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.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; } .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; } .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 */ /* 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; } .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) { @media (max-width: 800px) {
.top { display: flex; flex-direction: column; font-size: 12px; } .top { display: flex; flex-direction: column; font-size: 12px; }
.top > .tbox { flex: 0 0 auto; } .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-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-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; } .t-net { flex-direction: column; } .t-net-nums { width: auto; } .t-net-graphs { flex: 0 0 130px; }