Top: a hyper-thread wears its core's temperature, and the columns line up

The process table carries each logical CPU's core id from sysfs topology; a row with
no reading still has the temperature cells, so the % column is one column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-22 20:22:54 -07:00
co-authored by Claude Fable 5.1
parent 41e63f74e9
commit 4280b2226a
5 changed files with 41 additions and 8 deletions
+1 -1
View File
@@ -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' 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 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 <roots>`, eight disks shows `tank`). The walk runs as a helper process, `hamlab space-walk <roots>`,
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; 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 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 but summed per folder as "N smaller files" — what keeps a scan of millions of files to tens
+30
View File
@@ -1011,6 +1011,34 @@ func uidName(uid string) string {
return n 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. // ProcTable is the Processes tab payload.
type ProcTable struct { type ProcTable struct {
T int64 `json:"t"` T int64 `json:"t"`
@@ -1021,6 +1049,7 @@ type ProcTable struct {
CPUMHz float64 `json:"cpu_mhz,omitempty"` CPUMHz float64 `json:"cpu_mhz,omitempty"`
CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"` CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"`
CPUTemp float64 `json:"cpu_temp,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"` Load [3]float64 `json:"load"`
Mem Mem `json:"mem"` Mem Mem `json:"mem"`
Swap Mem `json:"swap"` Swap Mem `json:"swap"`
@@ -1204,6 +1233,7 @@ func (c *Collector) Processes() ProcTable {
if n := len(c.ring); n > 0 { if n := len(c.ring); n > 0 {
last := c.ring[n-1] last := c.ring[n-1]
t.CPU, t.Cores, t.Net, t.CPUTemp = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps) 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 from := n - 90
if from < 0 { if from < 0 {
from = 0 from = 0
+1 -1
View File
@@ -100,7 +100,7 @@ func Walk(ctx context.Context, roots []string, progress func(Progress)) (*Node,
if len(trees) == 0 { if len(trees) == 0 {
return root, os.ErrNotExist return root, os.ErrNotExist
} }
workers := 16 workers := 32
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < workers; i++ { for i := 0; i < workers; i++ {
wg.Add(1) wg.Add(1)
+8 -5
View File
@@ -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 // "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 // 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() 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 // 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 // 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) 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 cached = (mem as { cached?: number }).cached ?? 0
const free = (mem as { free?: number }).free ?? Math.max(0, mem.total - mem.used - cached) const free = (mem as { free?: number }).free ?? Math.max(0, mem.total - mem.used - cached)
const swap = s?.swap ?? pt?.swap const swap = s?.swap ?? pt?.swap
@@ -344,16 +347,16 @@ export default function Top() {
<span className="t-core-n">CPU</span> <span className="t-core-n">CPU</span>
<Meter pct={cpu} grad="cpu" /> <Meter pct={cpu} grad="cpu" />
<span className="t-core-p">{Math.round(cpu)}%</span> <span className="t-core-p">{Math.round(cpu)}%</span>
{cpuTemp !== undefined && <><span className="t-core-t"><Meter pct={Math.min(100, (cpuTemp / 100) * 100)} grad="temp" /></span><span className="t-core-c">{Math.round(cpuTemp)}°C</span></>} {anyTemp && <><span className="t-core-t">{cpuTemp !== undefined && <Meter pct={Math.min(100, (cpuTemp / 100) * 100)} grad="temp" />}</span><span className="t-core-c">{cpuTemp !== undefined ? `${Math.round(cpuTemp)}°C` : ''}</span></>}
</div> </div>
{cores.map((v, i) => { {cores.map((v, i) => {
const t = coreTemp(i) const t = coreTemp(i)
return ( return (
<div className="t-core" key={i}> <div className="t-core" key={i}>
<span className="t-core-n">C{i}</span> <span className="t-core-n">C{i}</span>
<span className="t-core-g" title={`core ${i}`}>{braille(curves.get(`cpu.core.${i}.usage`) ?? [], 9)}</span> <span className="t-core-g" title={pt?.core_ids && pt.core_ids[i] !== i ? `cpu ${i} · a thread of core ${pt.core_ids[i]}` : `core ${i}`}>{braille(curves.get(`cpu.core.${i}.usage`) ?? [], 9)}</span>
<span className="t-core-p">{Math.round(v)}%</span> <span className="t-core-p">{Math.round(v)}%</span>
{t !== undefined && <><span className="t-core-t"><Meter pct={Math.min(100, t)} grad="temp" /></span><span className="t-core-c">{Math.round(t)}°C</span></>} {anyTemp && <><span className="t-core-t">{t !== undefined && <Meter pct={Math.min(100, t)} grad="temp" />}</span><span className="t-core-c">{t !== undefined ? `${Math.round(t)}°C` : ''}</span></>}
</div> </div>
) )
})} })}
+1 -1
View File
@@ -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 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 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 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 { export interface Sample {
t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number] 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 } mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }