Top: each CPU's own wattage on a two-socket machine, its threads folded under it
Per-package RAPL watts in the sample, the process table and as cpu.package.N.power sensors. The socket rows collapse with an arrow; the fold is remembered per browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1e2dcacb2c
commit
e9a87af5e8
@@ -95,30 +95,41 @@ func readRAPL(root string, dirs []string) ([]raplCount, bool) {
|
||||
// is carried across its range. One that went backwards with no known range, or a set of
|
||||
// counters that changed between readings, gives no answer rather than a wrong one.
|
||||
func raplWatts(prev, cur []raplCount, max map[string]uint64, dt float64) (float64, bool) {
|
||||
if dt <= 0 || len(prev) == 0 || len(prev) != len(cur) {
|
||||
return 0, false
|
||||
each, ok := raplWattsEach(prev, cur, max, dt)
|
||||
var w float64
|
||||
for _, x := range each {
|
||||
w += x
|
||||
}
|
||||
var uj float64
|
||||
return w, ok
|
||||
}
|
||||
|
||||
// raplWattsEach is raplWatts per counter: one package each.
|
||||
func raplWattsEach(prev, cur []raplCount, max map[string]uint64, dt float64) ([]float64, bool) {
|
||||
if dt <= 0 || len(prev) == 0 || len(prev) != len(cur) {
|
||||
return nil, false
|
||||
}
|
||||
out := make([]float64, len(cur))
|
||||
for i := range cur {
|
||||
if cur[i].Dir != prev[i].Dir {
|
||||
return 0, false
|
||||
return nil, false
|
||||
}
|
||||
if cur[i].UJ >= prev[i].UJ {
|
||||
uj += float64(cur[i].UJ - prev[i].UJ)
|
||||
out[i] = float64(cur[i].UJ-prev[i].UJ) / 1e6 / dt
|
||||
continue
|
||||
}
|
||||
m := max[cur[i].Dir]
|
||||
if m == 0 || prev[i].UJ > m {
|
||||
return 0, false
|
||||
return nil, false
|
||||
}
|
||||
uj += float64(m - prev[i].UJ + cur[i].UJ)
|
||||
out[i] = float64(m-prev[i].UJ+cur[i].UJ) / 1e6 / dt
|
||||
}
|
||||
return uj / 1e6 / dt, true
|
||||
return out, true
|
||||
}
|
||||
|
||||
// cpuPower is the CPU packages' power since the previous sample, in watts; 0 when this
|
||||
// machine has no counters or no way to read them.
|
||||
func (c *Collector) cpuPower(now time.Time) float64 {
|
||||
// machine has no counters or no way to read them. On a machine with more than one package
|
||||
// it also gives each one's, indexed by package id (the socket).
|
||||
func (c *Collector) cpuPower(now time.Time) (float64, []float64) {
|
||||
p := &c.power
|
||||
if !p.probed {
|
||||
p.probed = true
|
||||
@@ -131,7 +142,7 @@ func (c *Collector) cpuPower(now time.Time) float64 {
|
||||
}
|
||||
}
|
||||
if len(p.dirs) == 0 {
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
var cur []raplCount
|
||||
ok := false
|
||||
@@ -148,14 +159,40 @@ func (c *Collector) cpuPower(now time.Time) float64 {
|
||||
}
|
||||
if !ok {
|
||||
p.prev = nil
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
w, good := raplWatts(p.prev, cur, p.max, now.Sub(p.prevAt).Seconds())
|
||||
each, good := raplWattsEach(p.prev, cur, p.max, now.Sub(p.prevAt).Seconds())
|
||||
p.prev, p.prevAt = cur, now
|
||||
if !good {
|
||||
return 0
|
||||
return 0, nil
|
||||
}
|
||||
return round1(w)
|
||||
var w float64
|
||||
for _, x := range each {
|
||||
w += x
|
||||
}
|
||||
return round1(w), perPackage(cur, each)
|
||||
}
|
||||
|
||||
// perPackage puts each counter's watts at its package's id — intel-rapl:1 is "package-1" —
|
||||
// and is nil for a single package, whose figure is the total.
|
||||
func perPackage(cur []raplCount, each []float64) []float64 {
|
||||
if len(cur) < 2 {
|
||||
return nil
|
||||
}
|
||||
var out []float64
|
||||
for i, c := range cur {
|
||||
id := i
|
||||
if n, ok := strings.CutPrefix(strings.TrimSpace(readStr(filepath.Join(raplRoot, c.Dir, "name"))), "package-"); ok {
|
||||
if v, err := strconv.Atoi(n); err == nil && v >= 0 && v < 64 {
|
||||
id = v
|
||||
}
|
||||
}
|
||||
for len(out) <= id {
|
||||
out = append(out, 0)
|
||||
}
|
||||
out[id] = round1(each[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// energyHelper is the root child that answers each newline with the package counters, one
|
||||
|
||||
@@ -43,6 +43,21 @@ func fold(b []Sample) Sample {
|
||||
if pn > 0 {
|
||||
out.CPUPower = round1(pw / pn)
|
||||
}
|
||||
// and each package's, the same way
|
||||
out.CPUPowerPkgs = nil
|
||||
for _, s := range b {
|
||||
for i, w := range s.CPUPowerPkgs {
|
||||
for len(out.CPUPowerPkgs) <= i {
|
||||
out.CPUPowerPkgs = append(out.CPUPowerPkgs, 0)
|
||||
}
|
||||
out.CPUPowerPkgs[i] += w
|
||||
}
|
||||
}
|
||||
if pn > 0 {
|
||||
for i := range out.CPUPowerPkgs {
|
||||
out.CPUPowerPkgs[i] = round1(out.CPUPowerPkgs[i] / pn)
|
||||
}
|
||||
}
|
||||
out.Load[0] = sum(func(s Sample) float64 { return s.Load[0] })
|
||||
out.Load[1] = sum(func(s Sample) float64 { return s.Load[1] })
|
||||
out.Load[2] = sum(func(s Sample) float64 { return s.Load[2] })
|
||||
|
||||
+42
-40
@@ -136,24 +136,25 @@ type Proc struct {
|
||||
}
|
||||
|
||||
type Sample struct {
|
||||
T int64 `json:"t"` // unix ms
|
||||
Interval int64 `json:"interval_ms"` // sampling interval in effect
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
CPUPower float64 `json:"cpu_power_w,omitempty"` // the CPU packages' own energy counters; 0 = no reading
|
||||
Load [3]float64 `json:"load"`
|
||||
Mem Mem `json:"mem"`
|
||||
Swap Mem `json:"swap"`
|
||||
Temps []Temp `json:"temps"`
|
||||
Fans []Fan `json:"fans"`
|
||||
Net []Net `json:"net"`
|
||||
Disks []Disk `json:"disks"`
|
||||
FS []FS `json:"fs"`
|
||||
UPS *UPS `json:"ups"`
|
||||
GPUs []GPU `json:"gpus"`
|
||||
ARC *ARC `json:"arc"` // the ZFS read cache; nil without the module
|
||||
ZFSWrite *ZFSWrites `json:"zfs_writes"` // the ZFS write path; nil without the module
|
||||
Uptime float64 `json:"uptime"`
|
||||
T int64 `json:"t"` // unix ms
|
||||
Interval int64 `json:"interval_ms"` // sampling interval in effect
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
CPUPower float64 `json:"cpu_power_w,omitempty"` // the CPU packages' own energy counters; 0 = no reading
|
||||
CPUPowerPkgs []float64 `json:"cpu_power_pkgs_w,omitempty"` // each package's share, by socket; only with more than one
|
||||
Load [3]float64 `json:"load"`
|
||||
Mem Mem `json:"mem"`
|
||||
Swap Mem `json:"swap"`
|
||||
Temps []Temp `json:"temps"`
|
||||
Fans []Fan `json:"fans"`
|
||||
Net []Net `json:"net"`
|
||||
Disks []Disk `json:"disks"`
|
||||
FS []FS `json:"fs"`
|
||||
UPS *UPS `json:"ups"`
|
||||
GPUs []GPU `json:"gpus"`
|
||||
ARC *ARC `json:"arc"` // the ZFS read cache; nil without the module
|
||||
ZFSWrite *ZFSWrites `json:"zfs_writes"` // the ZFS write path; nil without the module
|
||||
Uptime float64 `json:"uptime"`
|
||||
|
||||
fsT, upsT int64 // when the slow sections were really read; sensors report these, not T
|
||||
}
|
||||
@@ -350,7 +351,7 @@ func (c *Collector) sample() {
|
||||
|
||||
s := Sample{T: now.UnixMilli(), Interval: c.Interval().Milliseconds(), Temps: []Temp{}, Fans: []Fan{}, Net: []Net{}, Disks: []Disk{}}
|
||||
s.CPU, s.Cores = c.cpu()
|
||||
s.CPUPower = c.cpuPower(now)
|
||||
s.CPUPower, s.CPUPowerPkgs = c.cpuPower(now)
|
||||
s.Load = loadavg()
|
||||
s.Mem, s.Swap = meminfo()
|
||||
s.Uptime = uptime()
|
||||
@@ -1163,26 +1164,27 @@ func corePkgs(n int) []int {
|
||||
|
||||
// ProcTable is the Processes tab payload.
|
||||
type ProcTable struct {
|
||||
T int64 `json:"t"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
CPUHist []float64 `json:"cpu_hist"` // recent total-CPU samples, oldest first
|
||||
CPUModel string `json:"cpu_model,omitempty"`
|
||||
CPUMHz float64 `json:"cpu_mhz,omitempty"`
|
||||
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)
|
||||
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"`
|
||||
Net []Net `json:"net"`
|
||||
GPUs []GPU `json:"gpus"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
Procs []Proc `json:"procs"`
|
||||
Total int `json:"total"`
|
||||
Running int `json:"running"`
|
||||
T int64 `json:"t"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Cores []float64 `json:"cores"`
|
||||
CPUHist []float64 `json:"cpu_hist"` // recent total-CPU samples, oldest first
|
||||
CPUModel string `json:"cpu_model,omitempty"`
|
||||
CPUMHz float64 `json:"cpu_mhz,omitempty"`
|
||||
CPUMaxMHz float64 `json:"cpu_max_mhz,omitempty"`
|
||||
CPUPowerW float64 `json:"cpu_power_w,omitempty"`
|
||||
CPUPowerPkgsW []float64 `json:"cpu_power_pkgs_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)
|
||||
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"`
|
||||
Net []Net `json:"net"`
|
||||
GPUs []GPU `json:"gpus"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
Procs []Proc `json:"procs"`
|
||||
Total int `json:"total"`
|
||||
Running int `json:"running"`
|
||||
}
|
||||
|
||||
var cpuModel = func() string {
|
||||
@@ -1377,7 +1379,7 @@ func (c *Collector) Processes() ProcTable {
|
||||
t.Mem, t.Swap = meminfo()
|
||||
if n := len(c.ring); n > 0 {
|
||||
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.CPU, t.Cores, t.Net, t.CPUTemp, t.CPUPowerW, t.CPUPowerPkgsW = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps), last.CPUPower, last.CPUPowerPkgs
|
||||
t.CoreIDs = coreIDs(len(last.Cores))
|
||||
t.CorePkgs = corePkgs(len(last.Cores))
|
||||
from := n - 90
|
||||
|
||||
@@ -65,6 +65,10 @@ func flatten(s Sample, want func(id string) bool) []sensors.Sensor {
|
||||
if s.CPUPower > 0 {
|
||||
f.add("cpu.power", "CPU power", "power", "W", s.CPUPower, t, none)
|
||||
}
|
||||
for i, w := range s.CPUPowerPkgs {
|
||||
n := strconv.Itoa(i)
|
||||
f.add("cpu.package."+n+".power", "CPU "+strconv.Itoa(i+1)+" power", "power", "W", w, t, none, "package", n)
|
||||
}
|
||||
for i, w := range []string{"1", "5", "15"} {
|
||||
f.add("load."+w, "Load "+w+" min", "load", "", s.Load[i], t, none)
|
||||
}
|
||||
|
||||
+12
-4
@@ -294,6 +294,13 @@ export default function Top() {
|
||||
// the process table, and the CPU model, come from the same place the Processes page reads,
|
||||
// on the sample's beat (above)
|
||||
const [pickedIf, setPickedIf] = useState<string | null>(null) // which NIC the net box draws
|
||||
// which sockets' threads are folded away on a two-socket machine; remembered in this browser
|
||||
const [folded, setFolded] = useState<number[]>(() => { try { return JSON.parse(localStorage.getItem('top.folded-cpus') ?? '[]') } catch { return [] } })
|
||||
const fold = (pk: number) => setFolded((f) => {
|
||||
const n = f.includes(pk) ? f.filter((x) => x !== pk) : [...f, pk]
|
||||
try { localStorage.setItem('top.folded-cpus', JSON.stringify(n)) } catch { /* private window */ }
|
||||
return n
|
||||
})
|
||||
// Which pool each drive belongs to, asked once: a drive in a ZFS pool has no filesystem of
|
||||
// its own, and "nothing is on it" would be a lie about eight drives holding a NAS.
|
||||
const [pools, setPools] = useState<Record<string, string>>({})
|
||||
@@ -421,10 +428,11 @@ export default function Top() {
|
||||
{(sockets.length > 1 ? sockets : [undefined]).map((pk) => (
|
||||
<Fragment key={pk ?? 'all'}>
|
||||
{pk !== undefined && (() => {
|
||||
const u = sockUse(pk), t = sockTemp(pk)
|
||||
const u = sockUse(pk), t = sockTemp(pk), w = pt?.cpu_power_pkgs_w?.[pk], open = !folded.includes(pk)
|
||||
return (
|
||||
<div className="t-core t-sock">
|
||||
<span className="t-core-n">CPU{pk + 1}</span>
|
||||
<div className="t-core t-sock" role="button" aria-expanded={open} title={open ? 'hide its threads' : 'show its threads'} onClick={() => fold(pk)}>
|
||||
<span className="t-core-n"><span className="t-fold">{open ? '▾' : '▸'}</span>CPU{pk + 1}</span>
|
||||
<span className="t-core-w" title="this CPU's own power reading: its cores, cache and memory controller">{w ? `${w < 10 ? w.toFixed(1) : Math.round(w)} W` : ''}</span>
|
||||
<Meter pct={u} grad="cpu" />
|
||||
<span className="t-core-p">{Math.round(u)}%</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></>}
|
||||
@@ -432,7 +440,7 @@ export default function Top() {
|
||||
)
|
||||
})()}
|
||||
{cores.map((v, i) => {
|
||||
if (pk !== undefined && pkgs?.[i] !== pk) return null
|
||||
if (pk !== undefined && (pkgs?.[i] !== pk || folded.includes(pk))) return null
|
||||
const t = coreTemp(i)
|
||||
return (
|
||||
<div className="t-core" key={i}>
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export interface GPU {
|
||||
mhz?: number; req_mhz?: number; max_mhz?: number; mem_used?: number; mem_total?: number
|
||||
clients: GPUClient[]; clients_ok: boolean; stalled?: boolean
|
||||
}
|
||||
export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_max_mhz?: number; cpu_power_w?: number; cpu_temp?: number; core_ids?: number[]; core_pkgs?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; gpus?: GPU[]; 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_power_w?: number; cpu_power_pkgs_w?: number[]; cpu_temp?: number; core_ids?: number[]; core_pkgs?: number[]; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; gpus?: GPU[]; 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 }
|
||||
|
||||
+5
-2
@@ -794,8 +794,11 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%,
|
||||
.t-cores-head b { color: var(--fg); font-weight: 400; overflow: hidden; text-overflow: ellipsis; }
|
||||
.t-core { display: flex; align-items: center; gap: 6px; }
|
||||
.t-core-n { width: 30px; } .t-core-p { width: 34px; text-align: right; }
|
||||
.t-cores.multi .t-core-n { width: 38px; } /* "CPU2" — a two-socket board has a row per socket above its threads */
|
||||
.t-sock { color: var(--fg); margin-top: 2px; }
|
||||
/* a two-socket board: a row per socket — arrow, name, watts — with its threads indented under it */
|
||||
.t-cores.multi .t-core-n { width: 50px; } .t-cores.multi .t-core:not(.t-sock) .t-core-n { padding-left: 14px; box-sizing: border-box; }
|
||||
.t-sock { color: var(--fg); margin-top: 2px; cursor: pointer; user-select: none; }
|
||||
.t-fold { display: inline-block; width: 12px; color: var(--muted); }
|
||||
.t-core-w { width: 40px; flex-shrink: 0; text-align: right; color: var(--muted); }
|
||||
.t-core-g { flex: 1; color: var(--ok); letter-spacing: 0; overflow: hidden; white-space: nowrap; }
|
||||
.t-core .tm { flex: 1; } .t-core-t { flex: 0 0 62px; display: flex; } .t-core-c { width: 42px; text-align: right; color: var(--t-blue); }
|
||||
.t-load { display: flex; margin-top: 2px; }
|
||||
|
||||
Reference in New Issue
Block a user