disk-load: cpu_top's shares use the unrounded busy time, and key units by cgroup path (issue 741)

This commit is contained in:
jochen
2026-10-15 15:06:06 +02:00
parent ab739d02dd
commit 2b1678f315
3 changed files with 99 additions and 12 deletions
+23 -11
View File
@@ -43,11 +43,15 @@ func (m *Machine) unitCPU() map[string]cgroupCPU {
return out
}
// MachineCPU is the machine's CPU in use over the window, from /proc/stat's cpu line.
// MachineCPU is the machine's CPU in use over the window, from /proc/stat's cpu line. CPUsBusy is
// rounded only for display; raw, unexported, is the same count unrounded, for topCPU's share and
// unaccounted math (review round 1 of novox/hq issue 741: a small busy time that rounds CPUsBusy to
// 0.0 must not zero out every unit's share along with it).
type MachineCPU struct {
CPUs int `json:"cpus"`
BusyPct float64 `json:"busy_pct"` // % of all CPUs together
CPUsBusy float64 `json:"cpus_busy"` // as a count of CPUs, e.g. 7.5 of 12
CPUsBusy float64 `json:"cpus_busy"` // as a count of CPUs, e.g. 7.5 of 12, rounded for display
raw float64
}
// machineCPUOver is the machine's CPU in use between two readings of /proc/stat: everything but idle
@@ -62,13 +66,17 @@ func machineCPUOver(a, b cpuTimes) MachineCPU {
if busy < 0 {
busy = 0
}
pct := round1(busy / total * 100)
return MachineCPU{CPUs: b.cpus, BusyPct: pct, CPUsBusy: round1(pct / 100 * float64(b.cpus))}
raw := busy / total * float64(b.cpus)
return MachineCPU{CPUs: b.cpus, BusyPct: round1(busy / total * 100), CPUsBusy: round1(raw), raw: raw}
}
// CPUUsage is a unit's or a container's share of CPU between two readings of its cgroup's cpu.stat.
// Cgroup is its cgroup's path, relative to /sys/fs/cgroup, said beside Unit always (review round 1 of
// novox/hq issue 741): two cgroups can share a leaf name (an outer "init.scope" and a user session's
// "user.slice/user@1000.service/init.scope"), and the path is how they are told apart.
type CPUUsage struct {
Unit string `json:"unit,omitempty"`
Cgroup string `json:"cgroup,omitempty"`
Container string `json:"container,omitempty"`
CPUPct float64 `json:"cpu_pct"` // % of one CPU: 100 = one CPU busy the whole window
SharePct float64 `json:"share_pct"` // this unit's share of the machine's busy time
@@ -82,8 +90,8 @@ type CPUUsage struct {
func (m *Machine) topCPU(a, b map[string]cgroupCPU, window time.Duration, by string, limit int, cpusBusy float64) ([]CPUUsage, float64) {
windowUsec := window.Seconds() * 1e6
type entry struct {
unit, container string
pct float64
path, unit, container string
pct float64
}
var entries []entry
var names map[string]string
@@ -110,12 +118,16 @@ func (m *Machine) topCPU(a, b map[string]cgroupCPU, window time.Duration, by str
container = n
}
}
entries = append(entries, entry{unit: unit, container: container, pct: pct})
entries = append(entries, entry{path: path, unit: unit, container: container, pct: pct})
}
// Units are keyed by their cgroup's own path, never by its leaf name (filepath.Base): two cgroups can
// share a leaf name (an outer "init.scope" and a user session's nested one), and keying by the name
// alone would merge them into one row (review round 1 of novox/hq issue 741). Containers stay keyed
// by their name, as before.
groups := map[string]*CPUUsage{}
for _, e := range entries {
key := e.unit
key := e.path
if by == "container" {
if e.container == "" {
continue
@@ -124,9 +136,9 @@ func (m *Machine) topCPU(a, b map[string]cgroupCPU, window time.Duration, by str
}
g, ok := groups[key]
if !ok {
g = &CPUUsage{Unit: e.unit, Container: e.container}
g = &CPUUsage{Unit: e.unit, Cgroup: e.path, Container: e.container}
if by == "container" {
g.Unit = ""
g.Unit, g.Cgroup = "", ""
}
groups[key] = g
}
@@ -140,7 +152,7 @@ func (m *Machine) topCPU(a, b map[string]cgroupCPU, window time.Duration, by str
if out[i].raw != out[j].raw {
return out[i].raw > out[j].raw
}
return out[i].Unit+out[i].Container < out[j].Unit+out[j].Container
return out[i].Unit+out[i].Cgroup+out[i].Container < out[j].Unit+out[j].Cgroup+out[j].Container
})
if len(out) > limit {
out = out[:limit]