owpengram-server/internal/hoststats/cpu_unix.go
2026-08-07 04:26:56 +03:00

79 lines
2 KiB
Go

//go:build !windows
package hoststats
import (
"fmt"
"os"
"strconv"
"strings"
)
// cpuSampler computes CPU busy% from successive cumulative /proc/stat
// snapshots. The kernel's counters are monotonic totals since boot, so a
// single read can't give a percentage -- only the delta between two reads
// separated by real time can. sampleOnce (its only caller) already runs on
// a fixed poll interval, so that interval doubles as the sampling window;
// no internal sleep needed.
type cpuSampler struct {
prevTotal uint64
prevIdle uint64
have bool
}
func (c *cpuSampler) sample() float64 {
total, idle, err := readProcStatCPU()
if err != nil {
return 0
}
if !c.have {
c.prevTotal, c.prevIdle = total, idle
c.have = true
return 0
}
deltaTotal := total - c.prevTotal
deltaIdle := idle - c.prevIdle
c.prevTotal, c.prevIdle = total, idle
if deltaTotal == 0 {
return 0
}
pct := (1 - float64(deltaIdle)/float64(deltaTotal)) * 100
switch {
case pct < 0:
pct = 0
case pct > 100:
pct = 100
}
return pct
}
// readProcStatCPU parses the aggregate "cpu " line: user nice system idle
// iowait irq softirq steal guest guest_nice (guest/guest_nice already
// double-counted inside user/nice on Linux, per `man proc`, so they're not
// added again here). idle = idle + iowait, matching the convention `top`
// and most load calculators use.
func readProcStatCPU() (total, idle uint64, err error) {
data, err := os.ReadFile("/proc/stat")
if err != nil {
return 0, 0, err
}
line, _, _ := strings.Cut(string(data), "\n")
fields := strings.Fields(line)
if len(fields) < 5 || fields[0] != "cpu" {
return 0, 0, fmt.Errorf("hoststats: unexpected /proc/stat format")
}
values := make([]uint64, 0, len(fields)-1)
for _, f := range fields[1:] {
v, err := strconv.ParseUint(f, 10, 64)
if err != nil {
return 0, 0, err
}
values = append(values, v)
total += v
}
idle = values[3]
if len(values) > 4 {
idle += values[4] // iowait
}
return total, idle, nil
}