Drive tile counts each source device once (a bind mount doubled nexus's NVMe)

fs.* sensors carry a source label; Top.tsx dedupes by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-11-11 00:06:33 -08:00
co-authored by Claude Opus 5.5
parent 574d9855ce
commit a6d7ff436f
5 changed files with 12 additions and 4 deletions
+4 -1
View File
@@ -102,6 +102,9 @@ type FS struct {
// said honestly: a ZFS pool or an md array spreads a filesystem over several drives, and // said honestly: a ZFS pool or an md array spreads a filesystem over several drives, and
// a remote mount is on none of them. // a remote mount is on none of them.
Disk string `json:"disk,omitempty"` Disk string `json:"disk,omitempty"`
// Source is the device the mount is of ("/dev/nvme0n1p1"). A bind mount has the same
// source as the filesystem it shows, so a sum over a drive counts each source once.
Source string `json:"source,omitempty"`
} }
type UPS struct { type UPS struct {
@@ -863,7 +866,7 @@ func filesystems() []FS {
return return
} }
bs := uint64(st.Bsize) bs := uint64(st.Bsize)
done <- FS{Mount: mount, Type: fs[2], Size: st.Blocks * bs, Used: (st.Blocks - st.Bfree) * bs, Remote: remoteFS[fs[2]], Disk: diskOf(fs[0])} done <- FS{Mount: mount, Type: fs[2], Size: st.Blocks * bs, Used: (st.Blocks - st.Bfree) * bs, Remote: remoteFS[fs[2]], Disk: diskOf(fs[0]), Source: fs[0]}
}() }()
select { select {
case r := <-done: case r := <-done:
+3
View File
@@ -137,6 +137,9 @@ func flatten(s Sample, want func(id string) bool) []sensors.Sensor {
if x.Disk != "" { if x.Disk != "" {
lbl = append(lbl, "disk", x.Disk) lbl = append(lbl, "disk", x.Disk)
} }
if x.Source != "" {
lbl = append(lbl, "source", x.Source)
}
f.add(p+".size", x.Mount+" size", "bytes", "B", float64(x.Size), s.fsT, none, lbl...) f.add(p+".size", x.Mount+" size", "bytes", "B", float64(x.Size), s.fsT, none, lbl...)
f.add(p+".used", x.Mount+" used", "bytes", "B", float64(x.Used), s.fsT, upTo(float64(x.Size)), lbl...) f.add(p+".used", x.Mount+" used", "bytes", "B", float64(x.Used), s.fsT, upTo(float64(x.Size)), lbl...)
f.add(p+".used_pct", x.Mount+" used", "usage", "%", ratio(x.Used, x.Size), s.fsT, pct, lbl...) f.add(p+".used_pct", x.Mount+" used", "usage", "%", ratio(x.Used, x.Size), s.fsT, pct, lbl...)
+3 -1
View File
@@ -490,7 +490,9 @@ export default function Top() {
// pool or an md array spans several drives, so nothing is attributed and the row // pool or an md array spans several drives, so nothing is attributed and the row
// shows what is true of the hardware — how busy it is, and how hot. // shows what is true of the hardware — how busy it is, and how hot.
const on = fs.filter((f) => f.disk === d.name) const on = fs.filter((f) => f.disk === d.name)
const used = on.reduce((a, f) => a + f.used, 0) // a bind mount (nexus's /var/log is /data/var-log) is the same filesystem twice:
// count each source device once, or the drive reads double
const used = [...new Map(on.map((f) => [f.source || f.mount, f.used])).values()].reduce((a, b) => a + b, 0)
return <Disk key={d.name} name={d.name} note={d.model} size={d.size} used={on.length ? used : undefined} io={d.util_pct} temp={d.temp} mounts={on.map((f) => f.mount)} pool={pools[d.name]} /> return <Disk key={d.name} name={d.name} note={d.model} size={d.size} used={on.length ? used : undefined} io={d.util_pct} temp={d.temp} mounts={on.map((f) => f.mount)} pool={pools[d.name]} />
})} })}
{(s?.disks ?? []).length === 0 && <div className="t-dim">no drives reported</div>} {(s?.disks ?? []).length === 0 && <div className="t-dim">no drives reported</div>}
+1 -1
View File
@@ -75,7 +75,7 @@ export interface Temp { chip: string; label: string; c: number; max?: number; cr
export interface Fan { chip: string; label: string; rpm: number } export interface Fan { chip: string; label: string; rpm: number }
export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number } export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number }
export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number } export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number }
export interface FS { mount: string; type: string; size: number; used: number; remote?: boolean; disk?: string } export interface FS { mount: string; type: string; size: number; used: number; remote?: boolean; disk?: string; source?: string }
export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string } export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string }
export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; gpu?: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number } export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; gpu?: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number }
export interface MemInfo { total: number; used: number; avail: number; free?: number; cached?: number; buffers?: number } export interface MemInfo { total: number; used: number; avail: number; free?: number; cached?: number; buffers?: number }
+1 -1
View File
@@ -51,7 +51,7 @@ export function sampleOf(v: Values, cat: Catalog): Sample {
fans: hw('fan.').map(({ id, s }) => ({ chip: s.labels!.chip, label: s.labels!.label, rpm: num(v, id) })), fans: hw('fan.').map(({ id, s }) => ({ chip: s.labels!.chip, label: s.labels!.label, rpm: num(v, id) })),
net: [...groups(v, cat, 'net.', 'iface')].map(([iface, p]) => ({ iface, up: v[p + '.up'] === true, rx_bps: num(v, p + '.rx'), tx_bps: num(v, p + '.tx'), rx_total: num(v, p + '.rx_total'), tx_total: num(v, p + '.tx_total') })), net: [...groups(v, cat, 'net.', 'iface')].map(([iface, p]) => ({ iface, up: v[p + '.up'] === true, rx_bps: num(v, p + '.rx'), tx_bps: num(v, p + '.tx'), rx_total: num(v, p + '.rx_total'), tx_total: num(v, p + '.tx_total') })),
disks: [...groups(v, cat, 'disk.', 'device')].map(([name, p]) => ({ name, model: cat.get(p + '.size')?.labels?.model || undefined, size: num(v, p + '.size'), read_bps: num(v, p + '.read'), write_bps: num(v, p + '.write'), util_pct: num(v, p + '.util'), temp: num(v, p + '.temp') || undefined })), disks: [...groups(v, cat, 'disk.', 'device')].map(([name, p]) => ({ name, model: cat.get(p + '.size')?.labels?.model || undefined, size: num(v, p + '.size'), read_bps: num(v, p + '.read'), write_bps: num(v, p + '.write'), util_pct: num(v, p + '.util'), temp: num(v, p + '.temp') || undefined })),
fs: [...groups(v, cat, 'fs.', 'mount')].map(([mount, p]) => ({ mount, type: cat.get(p + '.size')?.labels?.fstype ?? '', size: num(v, p + '.size'), used: num(v, p + '.used'), remote: cat.get(p + '.size')?.labels?.remote === '1', disk: cat.get(p + '.size')?.labels?.disk })), fs: [...groups(v, cat, 'fs.', 'mount')].map(([mount, p]) => ({ mount, type: cat.get(p + '.size')?.labels?.fstype ?? '', size: num(v, p + '.size'), used: num(v, p + '.used'), remote: cat.get(p + '.size')?.labels?.remote === '1', disk: cat.get(p + '.size')?.labels?.disk, source: cat.get(p + '.size')?.labels?.source })),
ups: v['ups.battery'] === undefined ? null : { ups: v['ups.battery'] === undefined ? null : {
driver: cat.get('ups.last_event')?.labels?.driver ?? '', model: cat.get('ups.battery')?.labels?.model ?? '', state: str(v, 'ups.state'), source: str(v, 'ups.source'), driver: cat.get('ups.last_event')?.labels?.driver ?? '', model: cat.get('ups.battery')?.labels?.model ?? '', state: str(v, 'ups.state'), source: str(v, 'ups.source'),
on_battery: v['ups.on_battery'] === true, in_v: num(v, 'ups.in_v'), out_v: num(v, 'ups.out_v'), battery_pct: num(v, 'ups.battery'), runtime_min: num(v, 'ups.runtime'), on_battery: v['ups.on_battery'] === true, in_v: num(v, 'ups.in_v'), out_v: num(v, 'ups.out_v'), battery_pct: num(v, 'ups.battery'), runtime_min: num(v, 'ups.runtime'),