forgejo-runner-optimiser/internal/proc/stat.go

132 lines
3.7 KiB
Go
Raw Normal View History

package proc
import (
"fmt"
"os"
"strconv"
"strings"
)
// ProcStat holds CPU-related information from /proc/[pid]/stat
type ProcStat struct {
PID int
Comm string // Process name (executable filename)
State byte // Process state (R, S, D, Z, T, etc.)
PPID int // Parent PID
UTime uint64 // User mode CPU time (in clock ticks)
STime uint64 // Kernel mode CPU time (in clock ticks)
CUTime int64 // Children user mode CPU time
CSTime int64 // Children system mode CPU time
NumThreads int64 // Number of threads
StartTime uint64 // Time process started after boot (in clock ticks)
}
// ReadStat reads and parses /proc/[pid]/stat for the given PID
func ReadStat(procPath string, pid int) (*ProcStat, error) {
path := fmt.Sprintf("%s/%d/stat", procPath, pid)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading stat file: %w", err)
}
return parseStat(pid, string(data))
}
// parseStat parses the content of /proc/[pid]/stat
// The format is complex because comm (field 2) can contain spaces and parentheses
func parseStat(pid int, data string) (*ProcStat, error) {
// Find the comm field which is enclosed in parentheses
// This handles cases where comm contains spaces or other special characters
start := strings.Index(data, "(")
end := strings.LastIndex(data, ")")
if start == -1 || end == -1 || end <= start {
return nil, fmt.Errorf("invalid stat format: cannot find comm field")
}
comm := data[start+1 : end]
// Fields after the comm field
remainder := strings.TrimSpace(data[end+1:])
fields := strings.Fields(remainder)
// We need at least 20 fields after comm (fields 3-22)
if len(fields) < 20 {
return nil, fmt.Errorf("invalid stat format: expected at least 20 fields after comm, got %d", len(fields))
}
stat := &ProcStat{
PID: pid,
Comm: comm,
}
// Field 3: state (index 0 after comm)
if len(fields[0]) > 0 {
stat.State = fields[0][0]
}
// Field 4: ppid (index 1)
ppid, err := strconv.Atoi(fields[1])
if err != nil {
return nil, fmt.Errorf("parsing ppid: %w", err)
}
stat.PPID = ppid
// Field 14: utime (index 11) - user mode CPU time
utime, err := strconv.ParseUint(fields[11], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing utime: %w", err)
}
stat.UTime = utime
// Field 15: stime (index 12) - kernel mode CPU time
stime, err := strconv.ParseUint(fields[12], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing stime: %w", err)
}
stat.STime = stime
// Field 16: cutime (index 13) - children user mode CPU time
cutime, err := strconv.ParseInt(fields[13], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing cutime: %w", err)
}
stat.CUTime = cutime
// Field 17: cstime (index 14) - children system mode CPU time
cstime, err := strconv.ParseInt(fields[14], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing cstime: %w", err)
}
stat.CSTime = cstime
// Field 20: num_threads (index 17)
numThreads, err := strconv.ParseInt(fields[17], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing num_threads: %w", err)
}
stat.NumThreads = numThreads
// Field 22: starttime (index 19) - time process started after boot
startTime, err := strconv.ParseUint(fields[19], 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing starttime: %w", err)
}
stat.StartTime = startTime
return stat, nil
}
// TotalTime returns the total CPU time (user + system) in clock ticks
func (s *ProcStat) TotalTime() uint64 {
return s.UTime + s.STime
}
// TotalTimeWithChildren returns total CPU time including children
func (s *ProcStat) TotalTimeWithChildren() uint64 {
total := int64(s.UTime) + int64(s.STime) + s.CUTime + s.CSTime
if total < 0 {
return 0
}
return uint64(total)
}