diff --git a/PLAN.md b/PLAN.md index cbdacb0..e8b9923 100644 --- a/PLAN.md +++ b/PLAN.md @@ -331,7 +331,7 @@ something long in one, switch away and back, refresh — it's all still there. the top, not a partition: the scan covers every filesystem the disk backs — its partitions' mountpoints, and for a disk in a ZFS pool every mounted dataset of the pool (any of `tank`'s eight disks shows `tank`). The walk runs as a helper process, `hamlab space-walk `, - as root through `priv` when the host allows (so `/var/lib/docker` and `/root` count), 16 + as root through `priv` when the host allows (so `/var/lib/docker` and `/root` count), 32 workers reading directories depth-first, never crossing a filesystem boundary by itself; each root is grafted into one tree at its path. Files under 128 KiB are not kept one by one but summed per folder as "N smaller files" — what keeps a scan of millions of files to tens diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 18cdf0b..4901996 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -1011,6 +1011,34 @@ func uidName(uid string) string { return n } +// coreIDs maps logical CPUs to physical cores from sysfs topology: on a chip with +// hyper-threading, CPUs 8–15 are the second threads of cores 0–7, and coretemp names its +// sensors by core. Read once; the topology does not change while we run. +var ( + coreIDsOnce sync.Once + coreIDsVal []int +) + +func coreIDs(n int) []int { + coreIDsOnce.Do(func() { + for i := 0; ; i++ { + b, err := os.ReadFile("/sys/devices/system/cpu/cpu" + strconv.Itoa(i) + "/topology/core_id") + if err != nil { + break + } + id, err := strconv.Atoi(strings.TrimSpace(string(b))) + if err != nil { + break + } + coreIDsVal = append(coreIDsVal, id) + } + }) + if len(coreIDsVal) < n { + return nil + } + return coreIDsVal[:n] +} + // ProcTable is the Processes tab payload. type ProcTable struct { T int64 `json:"t"` @@ -1021,6 +1049,7 @@ type ProcTable struct { CPUMHz float64 `json:"cpu_mhz,omitempty"` CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"` CPUTemp float64 `json:"cpu_temp,omitempty"` + CoreIDs []int `json:"core_ids,omitempty"` // for each logical CPU, the physical core it is a thread of (what the temperature sensors are named by) Load [3]float64 `json:"load"` Mem Mem `json:"mem"` Swap Mem `json:"swap"` @@ -1204,6 +1233,7 @@ func (c *Collector) Processes() ProcTable { if n := len(c.ring); n > 0 { last := c.ring[n-1] t.CPU, t.Cores, t.Net, t.CPUTemp = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps) + t.CoreIDs = coreIDs(len(last.Cores)) from := n - 90 if from < 0 { from = 0 diff --git a/internal/space/walk.go b/internal/space/walk.go index e1f3541..43e1e57 100644 --- a/internal/space/walk.go +++ b/internal/space/walk.go @@ -100,7 +100,7 @@ func Walk(ctx context.Context, roots []string, progress func(Progress)) (*Node, if len(trees) == 0 { return root, os.ErrNotExist } - workers := 16 + workers := 32 var wg sync.WaitGroup for i := 0; i < workers; i++ { wg.Add(1) diff --git a/web/src/Top.tsx b/web/src/Top.tsx index 5117967..859353d 100644 --- a/web/src/Top.tsx +++ b/web/src/Top.tsx @@ -309,9 +309,12 @@ export default function Top() { // "11th Gen Intel(R) Core(TM) i7-11700T @ 1.40GHz" is a whole line of border to itself; the // clock is already beside it, and the generation is in the model number const model = (pt?.cpu_model ?? '').replace(/\(R\)|\(TM\)|CPU|Processor/g, '').replace(/@.*$/, '').replace(/^\d+th Gen\s*/i, '').replace(/\s+/g, ' ').trim() - // the hottest thing that looks like a CPU sensor, and one per core where the chip names them - const coreTemp = (i: number) => s?.temps.find((t) => new RegExp(`core ${i}\\b`, 'i').test(t.label))?.c + // the hottest thing that looks like a CPU sensor, and one per core where the chip names them; + // a second thread of a core (CPU 8 on an 8-core chip with hyper-threading) wears its core's + const coreTemp = (i: number) => { const id = pt?.core_ids?.[i] ?? i; return s?.temps.find((t) => new RegExp(`core ${id}\\b`, 'i').test(t.label))?.c } const cpuTemp = pt?.cpu_temp ?? s?.temps.find((t) => /package|tctl|tdie|cpu/i.test(t.label))?.c ?? coreTemp(0) + // the temperature columns are there for every row once any row has one, so the % column lines up + const anyTemp = cpuTemp !== undefined || cores.some((_, i) => coreTemp(i) !== undefined) const cached = (mem as { cached?: number }).cached ?? 0 const free = (mem as { free?: number }).free ?? Math.max(0, mem.total - mem.used - cached) const swap = s?.swap ?? pt?.swap @@ -344,16 +347,16 @@ export default function Top() { CPU {Math.round(cpu)}% - {cpuTemp !== undefined && <>{Math.round(cpuTemp)}°C} + {anyTemp && <>{cpuTemp !== undefined && }{cpuTemp !== undefined ? `${Math.round(cpuTemp)}°C` : ''}} {cores.map((v, i) => { const t = coreTemp(i) return (
C{i} - {braille(curves.get(`cpu.core.${i}.usage`) ?? [], 9)} + {braille(curves.get(`cpu.core.${i}.usage`) ?? [], 9)} {Math.round(v)}% - {t !== undefined && <>{Math.round(t)}°C} + {anyTemp && <>{t !== undefined && }{t !== undefined ? `${Math.round(t)}°C` : ''}}
) })} diff --git a/web/src/api.ts b/web/src/api.ts index 961de1b..b3565a1 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -73,7 +73,7 @@ export interface FS { mount: string; type: string; size: number; used: number } 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; 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 ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_temp?: number; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; uptime: number; procs: Proc[]; total: number; running: number } +export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_temp?: number; core_ids?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; uptime: number; procs: Proc[]; total: number; running: number } export interface Sample { t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number] mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }