mesh/delivery ready: it is merged on the Approve
mesh/check pass dd4c53eefb23 on 0b2d393b1100 | unit 714 run, 4022 kept, 13 skipped | integration 758 run, 4038 kept, 22 skipped | over bound: unit, in…
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// clockTicks is USER_HZ, the unit of utime and stime in /proc/<pid>/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/<pid>/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.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
|
|
}
|