package main import ( "path/filepath" "sort" "strconv" "strings" "time" ) // cgroupCPU is a cgroup's processor time, from its cpu.stat. type cgroupCPU struct { usageUsec uint64 } // parseCPUStat reads a cgroup's cpu.stat ("usage_usec 123\nuser_usec …\nsystem_usec …"), readable by // every account, unlike /proc//stat for another account's process. func parseCPUStat(s string) cgroupCPU { var c cgroupCPU for _, line := range strings.Split(s, "\n") { k, v, ok := strings.Cut(line, " ") if !ok { continue } if k == "usage_usec" { c.usageUsec, _ = strconv.ParseUint(strings.TrimSpace(v), 10, 64) } } return c } // unitCPU is every unit's processor time, by its cgroup's path. A cgroup whose cpu.stat cannot be // read is left out, never an error for the whole answer. func (m *Machine) unitCPU() map[string]cgroupCPU { out := map[string]cgroupCPU{} for _, u := range m.unitPaths() { text := m.read("/sys/fs/cgroup/" + u + "/cpu.stat") if text == "" { continue } out[u] = parseCPUStat(text) } return out } // 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, rounded for display raw float64 } // machineCPUOver is the machine's CPU in use between two readings of /proc/stat: everything but idle // and iowait, counted against the window's total ticks over every CPU. func machineCPUOver(a, b cpuTimes) MachineCPU { total := float64(b.total() - a.total()) if total <= 0 { return MachineCPU{CPUs: b.cpus} } idle := sub(b.idle, a.idle) + sub(b.iowait, a.iowait) busy := total - idle if busy < 0 { busy = 0 } 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 raw float64 } // topCPU ranks the units, or the containers, with the most CPU time between two readings of // cpu.stat, busiest first. unaccountedPct is the machine's busy time no unit accounts for, as a // share of it (never below 0): the sum is always taken over every unit, whichever by ranks by, so a // limit on the list never changes it. 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 { path, unit, container string pct float64 } var entries []entry var names map[string]string var sumPct float64 for path, end := range b { start, ok := a[path] if !ok { continue } d := sub(end.usageUsec, start.usageUsec) if d == 0 { continue } pct := d / windowUsec * 100 sumPct += pct unit := filepath.Base(path) var container string if id := containerID.FindStringSubmatch("/" + path); id != nil { if names == nil { names = m.containerNames() } container = id[1][:12] if n, ok := names[id[1]]; ok { container = n } } 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.path if by == "container" { if e.container == "" { continue } key = e.container } g, ok := groups[key] if !ok { g = &CPUUsage{Unit: e.unit, Cgroup: e.path, Container: e.container} if by == "container" { g.Unit, g.Cgroup = "", "" } groups[key] = g } g.raw += e.pct } out := make([]CPUUsage, 0, len(groups)) for _, g := range groups { out = append(out, *g) } sort.Slice(out, func(i, j int) bool { if out[i].raw != out[j].raw { return out[i].raw > out[j].raw } 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] } for i := range out { out[i].CPUPct = round1(out[i].raw) if cpusBusy > 0 { out[i].SharePct = round1(out[i].raw / (cpusBusy * 100) * 100) } } unaccounted := cpusBusy*100 - sumPct if unaccounted < 0 { unaccounted = 0 } unaccountedPct := 0.0 if cpusBusy > 0 { unaccountedPct = round1(unaccounted / (cpusBusy * 100) * 100) } return out, unaccountedPct }