177 lines
5.5 KiB
Go
177 lines
5.5 KiB
Go
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/<pid>/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
|
|
}
|