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:
co-authored by
Claude Fable 5.1
parent
41e63f74e9
commit
4280b2226a
@@ -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 <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;
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+8
-5
@@ -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() {
|
||||
<span className="t-core-n">CPU</span>
|
||||
<Meter pct={cpu} grad="cpu" />
|
||||
<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>
|
||||
{cores.map((v, i) => {
|
||||
const t = coreTemp(i)
|
||||
return (
|
||||
<div className="t-core" key={i}>
|
||||
<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>
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
|
||||
+1
-1
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user