package main import ( "fmt" "io/fs" "path/filepath" "sort" "strconv" "strings" "time" ) // clockTicks is USER_HZ, the unit of utime and stime in /proc//stat: 100 on every Linux the mesh runs. const clockTicks = 100.0 // ProcessCPU is one process of a unit and its share of CPU over the window (cpu_top with unit set). type ProcessCPU struct { PID int `json:"pid"` Command string `json:"command"` // comm, then the first 120 characters of the command line CPUPct float64 `json:"cpu_pct"` // % of one CPU: 100 = one CPU busy the whole window SharePct float64 `json:"share_pct"` // this process's share of the unit's busy time Threads int `json:"threads"` raw float64 } // procTicks is a process's utime+stime ticks and thread count from its /proc//stat, whose comm // field may hold spaces and parentheses, so the fields are counted from the last ")". func parseProcStat(s string) (comm string, ticks uint64, threads int, start string, ok bool) { l, r := strings.Index(s, "("), strings.LastIndex(s, ")") if l < 0 || r < l { return "", 0, 0, "", false } f := strings.Fields(s[r+1:]) if len(f) < 20 { return "", 0, 0, "", false } ut, e1 := strconv.ParseUint(f[11], 10, 64) st, e2 := strconv.ParseUint(f[12], 10, 64) th, e3 := strconv.Atoi(f[17]) if e1 != nil || e2 != nil || e3 != nil { return "", 0, 0, "", false } return s[l+1 : r], ut + st, th, f[19], true } // cgroupOfUnit is the cgroup path (relative to /sys/fs/cgroup) a unit name or a path names. func (m *Machine) cgroupOfUnit(unit string) (string, error) { base := m.path("/sys/fs/cgroup") if strings.Contains(unit, "/") { rel := strings.Trim(strings.TrimPrefix(unit, "/sys/fs/cgroup"), "/") if strings.Contains(rel, "..") || filepath.Clean("/"+rel) != "/"+rel { return "", fmt.Errorf("cgroup %q is not a path under /sys/fs/cgroup", unit) } if rel == "" || !m.exists("/sys/fs/cgroup/"+rel) { return "", fmt.Errorf("no cgroup %q", unit) } return rel, nil } var found []string filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error { if err == nil && d.IsDir() && d.Name() == unit { rel, _ := filepath.Rel(base, p) found = append(found, filepath.ToSlash(rel)) } return nil }) switch len(found) { case 0: return "", fmt.Errorf("no unit %q in the cgroup tree", unit) case 1: return found[0], nil } return "", fmt.Errorf("unit %q is %d cgroups (%s): give the cgroup path", unit, len(found), strings.Join(found, ", ")) } type procReading struct { ticks uint64 comm string threads int pid int start string // starttime, to tell a reused pid } // unitProcs reads every process in a cgroup and its child cgroups: its ticks, name and threads. func (m *Machine) unitProcs(cgroup string) map[int]procReading { out := map[int]procReading{} base := m.path("/sys/fs/cgroup/" + cgroup) filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() || d.Name() != "cgroup.procs" { return nil } for _, f := range strings.Fields(m.read(strings.TrimPrefix(p, filepath.Clean(m.Root)))) { pid, err := strconv.Atoi(f) if err != nil { continue } comm, ticks, th, start, ok := parseProcStat(m.read("/proc/" + f + "/stat")) if ok { out[pid] = procReading{ticks: ticks, comm: comm, threads: th, pid: pid, start: start} } } return nil }) return out } // commandLine is a process's program path (argv[0], cut to 120 characters), never its arguments: // they can carry a password. func (m *Machine) commandLine(pid int) string { s, _, _ := strings.Cut(m.read("/proc/"+strconv.Itoa(pid)+"/cmdline"), "\x00") s, _, _ = strings.Cut(s, " ") // a process that rewrites its title puts its whole command line here s, _, _ = strings.Cut(s, "\t") s = strings.TrimSpace(s) if r := []rune(s); len(r) > 120 { s = string(r[:120]) } return s } // topProcessCPU ranks a unit's processes by CPU between two readings; a process missing from either // reading (started or ended in the window) is skipped. func (m *Machine) topProcessCPU(a, b map[int]procReading, window time.Duration, limit int) []ProcessCPU { var out []ProcessCPU var sum float64 for pid, end := range b { start, ok := a[pid] if !ok || end.start != start.start || end.ticks <= start.ticks { continue } pct := float64(end.ticks-start.ticks) / clockTicks / window.Seconds() * 100 sum += pct out = append(out, ProcessCPU{PID: pid, Command: end.comm, Threads: end.threads, raw: pct}) } 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].PID < out[j].PID }) if len(out) > limit { out = out[:limit] } for i := range out { out[i].CPUPct = round1(out[i].raw) out[i].SharePct = round1(out[i].raw / sum * 100) if cl := m.commandLine(out[i].PID); cl != "" { out[i].Command += ": " + cl } } return out }