GPU boxes on the Processes tab: busy, engines, temps, and who has the card open
Every DRM device with a real GPU driver gets a box under the cpu box, laid out like it: the busy curve, a meter for the device and one per engine, temperature, clock, power and fan where the card has them, and the processes holding it open with their share. The process table gains a Gpu% column and sort. Device busy is Intel's RC6 idle residency inverted (what intel_gpu_top shows, and readable without root) or amdgpu's gpu_busy_percent. Per-process shares are DRM fdinfo deltas, as nvtop reads them; other users' fdinfo needs root, so a daemon running as a user keeps one root child (hamlab drm-clients) over a pipe rather than paying a sudo a second. Everything is also a gpu.* sensor. What cannot be measured is left out, not shown as zero: an Arc A380 on i915 6.12 has no temperature sensor, an integrated GPU wears the package temperature marked *, and Asahi has neither a busy counter nor fdinfo yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0e82719070
commit
db40220365
+84
-12
@@ -4,7 +4,7 @@
|
||||
// /api/procs for the table — so this works on any machine the console can see, whether or not
|
||||
// btop is installed on it.
|
||||
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { api, ProcTable, Proc, Sample } from './api'
|
||||
import { api, GPU, ProcTable, Proc, Sample } from './api'
|
||||
import { Catalog, MONITOR_SHAPE, Values, mergeValues, sampleOf, stale } from './sampleAdapter'
|
||||
import { fetchCatalog, fetchHistories, useSensorStream } from './sensors'
|
||||
import { fmtMs, nextPoint, REFRESH_OPTIONS, setRefreshMs, strideMs, useHistorySec, useRefreshMs } from './prefs'
|
||||
@@ -186,7 +186,7 @@ function braille(values: number[], cells = 7, max = 100): string {
|
||||
}
|
||||
|
||||
// ---- the curves this page keeps ----
|
||||
const CURVES = 'cpu.usage,cpu.core.*.usage,net.*.rx,net.*.tx,disk.*.util'
|
||||
const CURVES = 'cpu.usage,cpu.core.*.usage,net.*.rx,net.*.tx,disk.*.util,gpu.*.usage'
|
||||
|
||||
function useCurves(live: Values | null, t: number, refreshMs: number, historySec: number, resumed: number) {
|
||||
const cur = useRef<Map<string, number[]>>(new Map())
|
||||
@@ -216,7 +216,7 @@ function useCurves(live: Values | null, t: number, refreshMs: number, historySec
|
||||
const n = binN.current
|
||||
for (const [id, v] of Object.entries(live)) {
|
||||
if (typeof v !== 'number') continue
|
||||
if (!/^cpu\.usage$|^cpu\.core\.\d+\.usage$|^net\..+\.(rx|tx)$|^disk\..+\.util$/.test(id)) continue
|
||||
if (!/^cpu\.usage$|^cpu\.core\.\d+\.usage$|^net\..+\.(rx|tx)$|^disk\..+\.util$|^gpu\..+\.usage$/.test(id)) continue
|
||||
const arr = cur.current.get(id) ?? []
|
||||
if (!cur.current.has(id)) cur.current.set(id, arr)
|
||||
if (fresh || arr.length === 0) { arr.push(v); if (arr.length > MAX) arr.shift() }
|
||||
@@ -280,7 +280,7 @@ export default function Top() {
|
||||
// how many points the history window holds at this rate: the width of every graph
|
||||
const cap = Math.max(2, Math.min(MAX, Math.round((historySec * 1000) / strideMs(historySec, refreshMs))))
|
||||
|
||||
const [sort, setSort] = useState<'cpu' | 'mem' | 'pid' | 'name'>('cpu')
|
||||
const [sort, setSort] = useState<'cpu' | 'gpu' | 'mem' | 'pid' | 'name'>('cpu')
|
||||
const [rev, setRev] = useState(false)
|
||||
const [filter, setFilter] = useState('')
|
||||
const [sel, setSel] = useState<number | null>(null)
|
||||
@@ -303,8 +303,8 @@ export default function Top() {
|
||||
if (!pt) return []
|
||||
const f = filter.trim().toLowerCase()
|
||||
const list = pt.procs.filter((p) => !f || p.name.toLowerCase().includes(f) || (p.cmd ?? '').toLowerCase().includes(f) || p.user.toLowerCase().includes(f) || String(p.pid) === f)
|
||||
const key = (p: Proc) => (sort === 'cpu' ? p.cpu : sort === 'mem' ? p.rss : sort === 'pid' ? p.pid : 0)
|
||||
list.sort((a, b) => (sort === 'name' ? a.name.localeCompare(b.name) : key(b) - key(a)))
|
||||
const key = (p: Proc) => (sort === 'cpu' ? p.cpu : sort === 'gpu' ? (p.gpu ?? 0) : sort === 'mem' ? p.rss : sort === 'pid' ? p.pid : 0)
|
||||
list.sort((a, b) => (sort === 'name' ? a.name.localeCompare(b.name) : key(b) - key(a) || b.cpu - a.cpu))
|
||||
return rev ? list.reverse() : list
|
||||
}, [pt, filter, sort, rev])
|
||||
|
||||
@@ -342,10 +342,16 @@ export default function Top() {
|
||||
const pct = (a: number, b: number) => (b > 0 ? (a / b) * 100 : 0)
|
||||
// the top of a graph's scale: the peak of what it is drawing — the window, not everything kept
|
||||
const peak = (id: string) => Math.max(1, ...(curves.get(id) ?? []).slice(-cap))
|
||||
// the GPUs, with each one's sensor stem: the same rule the server uses, so two identical
|
||||
// cards are gpu.<name> and gpu.<name>_2 here as there
|
||||
const gpus = (pt?.gpus ?? []).map((g, i, all) => {
|
||||
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 (
|
||||
<Pal.Provider value={pal}>
|
||||
<div className="top" style={pal.vars as React.CSSProperties}>
|
||||
<div className={'top' + (gpus.length ? ' has-gpu' : '')} style={pal.vars as React.CSSProperties}>
|
||||
<Box n={1} name="cpu" className="top-cpu" right={<>
|
||||
<span className="t-clock">{clock.toLocaleTimeString([], { hour12: false })}</span>
|
||||
<span className="t-rate">
|
||||
@@ -383,6 +389,16 @@ export default function Top() {
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
{gpus.length > 0 && (
|
||||
<div className="top-gpus">
|
||||
{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>}>
|
||||
<GPUBox g={g} values={curves.get(stem + '.usage') ?? []} tick={tick} advance={advance} cap={cap} />
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Box n={2} name="mem" className="top-mem">
|
||||
<div className="t-row"><b>Total:</b><span className="spacer" />{bytes(mem.total)}</div>
|
||||
{([['Used', mem.used, 'used'], ['Available', mem.avail, 'avail'], ['Cached', cached, 'cached'], ['Free', free, 'free']] as const).map(([k, v, g]) => (
|
||||
@@ -445,17 +461,17 @@ export default function Top() {
|
||||
|
||||
<Box n={5} name="proc" className="top-proc" right={<>
|
||||
<span className="t-sorts">
|
||||
{(['pid', 'name', 'mem', 'cpu'] as const).map((k) => <button key={k} className={sort === k ? 'on' : ''} onClick={() => setSort(k)}>{k}</button>)}
|
||||
{(['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>)}
|
||||
<button className={rev ? 'on' : ''} onClick={() => setRev(!rev)}>reverse</button>
|
||||
</span>
|
||||
</>}>
|
||||
<div className="t-proc">
|
||||
<div className={'t-proc' + (gpus.length ? ' has-gpu' : '')}>
|
||||
<input className="t-filter" value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="filter" spellCheck={false} autoCapitalize="off" autoCorrect="off" />
|
||||
<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-thr">Threads:</span><span className="c-user">User:</span><span className="c-mem">MemB</span><span className="c-g" /><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" />{gpus.length > 0 && <span className="c-gpu">Gpu%</span>}<span className="c-cpu">Cpu%</span>
|
||||
</div>
|
||||
<Rows procs={procs} sel={sel} onSel={setSel} curves={pcurve} empty={err || 'no process matches'} />
|
||||
<Rows procs={procs} sel={sel} onSel={setSel} curves={pcurve} gpu={gpus.length > 0} empty={err || 'no process matches'} />
|
||||
<div className="t-foot">
|
||||
{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" />
|
||||
@@ -473,7 +489,7 @@ export default function Top() {
|
||||
|
||||
// The rows are the heaviest thing on the page — hundreds of them — so they are redrawn when the
|
||||
// table is read again, not on every tick of the curves above them.
|
||||
const Rows = memo(function Rows({ procs, sel, onSel, curves, empty }: { procs: Proc[]; sel: number | null; onSel: (p: number | null) => void; curves: React.RefObject<Map<number, number[]>>; empty: string }) {
|
||||
const Rows = memo(function Rows({ procs, sel, onSel, curves, gpu, empty }: { procs: Proc[]; sel: number | null; onSel: (p: number | null) => void; curves: React.RefObject<Map<number, number[]>>; gpu: boolean; empty: string }) {
|
||||
return (
|
||||
<div className="t-rows">
|
||||
{procs.map((p) => {
|
||||
@@ -487,6 +503,7 @@ const Rows = memo(function Rows({ procs, sel, onSel, curves, empty }: { procs: P
|
||||
<span className="c-user">{p.user}</span>
|
||||
<span className="c-mem">{shortBytes(p.rss)}</span>
|
||||
<span className="c-g">{braille(c, 7, Math.max(5, ...(c.length ? c : [0])))}</span>
|
||||
{gpu && <span className={'c-gpu' + (p.gpu ? ' lit' : '')}>{p.gpu ? p.gpu.toFixed(1) : ''}</span>}
|
||||
<span className="c-cpu">{p.cpu.toFixed(1)}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -496,6 +513,61 @@ const Rows = memo(function Rows({ procs, sel, onSel, curves, empty }: { procs: P
|
||||
)
|
||||
})
|
||||
|
||||
// One GPU, laid out like the cpu box: the busy curve on the left; on the right the device's
|
||||
// meters — busy, each engine, temperature — then clock, power and memory, then the processes
|
||||
// that have it open with their share. What cannot be measured on this card is left out rather
|
||||
// than shown as zero: an Arc on i915 has no temperature sensor, Asahi has no busy counter.
|
||||
const ENGINES: [string, string][] = [['render', 'render'], ['compute', 'compute'], ['video', 'video'], ['video-enhance', 'enhance'], ['copy', 'copy']]
|
||||
function GPUBox({ g, values, tick, advance, cap }: { g: GPU; values: number[]; tick: number; advance: number; cap: number }) {
|
||||
const engines = ENGINES.filter(([k]) => g.engines && g.engines[k] !== undefined)
|
||||
const temp = g.temp !== undefined && g.temp > 0 ? g.temp : undefined
|
||||
const how = g.usage_src === 'idle' ? 'busy, from idle residency' : g.usage_src === 'busy' ? 'busy, as the driver reports it' : g.usage_src === 'clients' ? 'the busiest engine, summed over its clients' : 'this driver exposes no activity counter'
|
||||
return (
|
||||
<div className="t-gpu">
|
||||
<div className="t-gpu-graph">
|
||||
<Graph values={values} tick={tick} advance={advance} cap={cap} max={100} grad="cpu" />
|
||||
<div className="t-up">{g.usage_src ? `${Math.round(g.usage)}% ` : ''}{how}</div>
|
||||
</div>
|
||||
<div className="t-gpu-stats">
|
||||
<div className="t-cores-head"><b>{g.name}</b><span className="spacer" />{g.driver}{g.integrated ? ' · integrated' : ''}</div>
|
||||
<div className="t-core">
|
||||
<span className="t-core-n">GPU</span>
|
||||
<Meter pct={g.usage_src ? g.usage : 0} grad="cpu" />
|
||||
<span className="t-core-p">{g.usage_src ? `${Math.round(g.usage)}%` : '—'}</span>
|
||||
<span className="t-core-t">{temp !== undefined && <Meter pct={Math.min(100, temp)} grad="temp" />}</span>
|
||||
<span className="t-core-c" title={temp !== undefined ? (g.temp_src === 'package' ? 'the CPU package: an integrated GPU shares the die, and this is its only sensor' : "the card's own sensor") : 'no temperature sensor on this device'}>{temp !== undefined ? `${Math.round(temp)}°C${g.temp_src === 'package' ? '*' : ''}` : ''}</span>
|
||||
</div>
|
||||
{engines.map(([k, label]) => (
|
||||
<div className="t-core" key={k}>
|
||||
<span className="t-core-n t-eng">{label}</span>
|
||||
<Meter pct={g.engines![k]} grad="cpu" />
|
||||
<span className="t-core-p">{Math.round(g.engines![k])}%</span>
|
||||
<span className="t-core-t" /><span className="t-core-c" />
|
||||
</div>
|
||||
))}
|
||||
<div className="t-gpu-facts">
|
||||
{(g.mhz !== undefined || g.max_mhz) ? <div className="t-row t-sub"><span>Clock</span><span className="spacer" />
|
||||
<span className="t-fact" title={g.req_mhz ? `${Math.round(g.req_mhz)} MHz requested${g.max_mhz ? `, ${Math.round(g.max_mhz)} MHz max` : ''}` : undefined}>{Math.round(g.mhz ?? 0)}{g.max_mhz ? ` / ${Math.round(g.max_mhz)}` : ''} MHz</span></div> : null}
|
||||
{(g.power_w !== undefined && g.power_w > 0) || g.power_cap_w ? <div className="t-row t-sub"><span>Power</span><span className="spacer" />
|
||||
<span className="t-fact">{(g.power_w ?? 0).toFixed(1)}{g.power_cap_w ? ` / ${Math.round(g.power_cap_w)}` : ''} W</span></div> : null}
|
||||
{g.fan_rpm ? <div className="t-row t-sub"><span>Fan</span><span className="spacer" /><span className="t-fact">{g.fan_rpm} rpm</span></div> : null}
|
||||
{g.mem_total ? <div className="t-row t-sub"><span>Mem</span><span className="t-pct">{Math.round(((g.mem_used ?? 0) / g.mem_total) * 100)}%</span><Meter pct={((g.mem_used ?? 0) / g.mem_total) * 100} grad="used" /><span className="t-amt">{bytes(g.mem_used ?? 0, 1)}</span></div>
|
||||
: g.mem_used ? <div className="t-row t-sub"><span>Mem</span><span className="spacer" /><span className="t-fact" title="resident memory of the processes that have it open">{bytes(g.mem_used, 1)}</span></div> : null}
|
||||
</div>
|
||||
<div className="t-gpu-clients">
|
||||
<div className="t-thead"><span className="c-pid">Pid:</span><span className="c-name">Program:</span><span className="c-mem">MemB</span><span className="c-cpu">Gpu%</span></div>
|
||||
{g.clients.map((c) => (
|
||||
<div className="t-crow" key={c.pid} title={c.engines ? Object.entries(c.engines).map(([k, v]) => `${k} ${v.toFixed(1)}%`).join(' ') : undefined}>
|
||||
<span className="c-pid">{c.pid}</span><span className="c-name">{c.name}</span><span className="c-mem">{c.mem ? shortBytes(c.mem) : ''}</span><span className="c-cpu">{c.usage.toFixed(1)}</span>
|
||||
</div>
|
||||
))}
|
||||
{g.clients.length === 0 && <div className="t-dim t-crow-none">{g.clients_ok ? 'no process has it open' : 'other users’ processes are hidden: the console has no root here'}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 }) {
|
||||
|
||||
Reference in New Issue
Block a user