Files
mesh-catalog/modules/disk-load/cmd/disk-load/cpu.go
T

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
}