Two-socket machines: Hardware and Top show each CPU, not one with every core

Hardware counts CPUs, cores and threads per CPU. Top groups threads under a row
per socket, reads each core's temperature from its own socket's coretemp chip,
and the CPU temperature is the hotter package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-11-10 00:04:47 -08:00
co-authored by Claude Opus 5.5
parent 4b238ea93e
commit 1e2dcacb2c
6 changed files with 133 additions and 27 deletions
+16
View File
@@ -0,0 +1,16 @@
package metrics
import "testing"
// a two-socket board: two coretemp chips, and the CPU's temperature is the hotter package
func TestCPUTempHottestPackage(t *testing.T) {
temps := []Temp{
{Chip: "coretemp", Label: "Core 0", C: 31},
{Chip: "coretemp", Label: "Core 0", C: 44, Pkg: 1},
{Chip: "coretemp", Label: "Package id 0", C: 35},
{Chip: "coretemp", Label: "Package id 1", C: 41, Pkg: 1},
}
if got := cpuTemp(temps); got != 41 {
t.Fatalf("cpuTemp = %v, want 41", got)
}
}
+63 -6
View File
@@ -60,6 +60,7 @@ type Temp struct {
C float64 `json:"c"`
Max float64 `json:"max,omitempty"`
Crit float64 `json:"crit,omitempty"`
Pkg int `json:"pkg,omitempty"` // coretemp: the CPU package the chip belongs to — a two-socket board has two chips, both with a "Core 0"
}
type Fan struct {
@@ -544,6 +545,19 @@ func hwmonDir(d string) (chip string, temps []Temp, fans []Fan) {
fans = append(fans, Fan{Chip: chip, Label: label, RPM: int(raw)})
}
}
// coretemp is one chip per CPU package, and only its "Package id N" says which
if chip == "coretemp" {
for _, t := range temps {
if n, ok := strings.CutPrefix(t.Label, "Package id "); ok {
if pkg, err := strconv.Atoi(n); err == nil {
for i := range temps {
temps[i].Pkg = pkg
}
}
break
}
}
}
return
}
@@ -559,7 +573,10 @@ func hwmon() ([]Temp, []Fan) {
if temps[i].Chip != temps[j].Chip {
return temps[i].Chip < temps[j].Chip
}
return temps[i].Label < temps[j].Label
if temps[i].Label != temps[j].Label {
return temps[i].Label < temps[j].Label
}
return temps[i].Pkg < temps[j].Pkg
})
return temps, fans
}
@@ -1112,6 +1129,38 @@ func coreIDs(n int) []int {
return coreIDsVal[:n]
}
// corePkgs maps logical CPUs to the CPU package (socket) each is on, and is nil on a
// one-socket machine: there is nothing to tell apart.
var (
corePkgsOnce sync.Once
corePkgsVal []int
)
func corePkgs(n int) []int {
corePkgsOnce.Do(func() {
seen := map[int]bool{}
var out []int
for i := 0; ; i++ {
b, err := os.ReadFile("/sys/devices/system/cpu/cpu" + strconv.Itoa(i) + "/topology/physical_package_id")
if err != nil {
break
}
id, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
break
}
out, seen[id] = append(out, id), true
}
if len(seen) > 1 {
corePkgsVal = out
}
})
if len(corePkgsVal) < n {
return nil
}
return corePkgsVal[:n]
}
// ProcTable is the Processes tab payload.
type ProcTable struct {
T int64 `json:"t"`
@@ -1123,7 +1172,8 @@ type ProcTable struct {
CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"`
CPUPowerW float64 `json:"cpu_power_w,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)
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)
CorePkgs []int `json:"core_pkgs,omitempty"` // for each logical CPU, the socket it is on; only on a machine with more than one
Load [3]float64 `json:"load"`
Mem Mem `json:"mem"`
Swap Mem `json:"swap"`
@@ -1182,18 +1232,24 @@ func cpuMHz() float64 {
// cpuTemp picks the sensor that best represents the CPU package.
func cpuTemp(temps []Temp) float64 {
best, max := 0.0, 0.0
// the hottest package: a two-socket board has two, and the hotter one is the one to watch
pkg, max := 0.0, 0.0
for _, t := range temps {
l := strings.ToLower(t.Label)
if t.Chip == "coretemp" && strings.HasPrefix(l, "package") || t.Chip == "k10temp" && (l == "tctl" || l == "tdie") ||
t.Chip == "thermal" && strings.Contains(l, "cpu") || t.Chip == "cpu_thermal" || t.Chip == "soc_thermal" {
return t.C
if t.C > pkg {
pkg = t.C
}
}
if (t.Chip == "coretemp" || t.Chip == "k10temp") && t.C > max {
max, best = t.C, t.C
max = t.C
}
}
return best
if pkg > 0 {
return pkg
}
return max
}
var bootTime = func() int64 {
@@ -1323,6 +1379,7 @@ func (c *Collector) Processes() ProcTable {
last := c.ring[n-1]
t.CPU, t.Cores, t.Net, t.CPUTemp, t.CPUPowerW = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps), last.CPUPower
t.CoreIDs = coreIDs(len(last.Cores))
t.CorePkgs = corePkgs(len(last.Cores))
from := n - 90
if from < 0 {
from = 0