Files
nadir-agent/internal/modules/system/cpustat.go
T
urania d4364a6cb7
build-and-release / release (push) Successful in 2m39s
feat(system): enhance system architecture
2026-06-25 14:44:47 +02:00

138 lines
3.2 KiB
Go

package system
import (
"context"
"math"
"os"
"strconv"
"strings"
"sync"
"time"
)
// Sampler samples /proc/stat periodically and caches per-core CPU usage
// percentages. Create with New, start the background goroutine with Start, and
// read the latest snapshot with Snapshot.
type Sampler struct {
statPath string
interval time.Duration
mu sync.RWMutex
cache []CoreUsage
once sync.Once
}
// NewSampler creates a Sampler that reads statPath and samples every interval.
func NewSampler(statPath string, interval time.Duration) *Sampler {
return &Sampler{statPath: statPath, interval: interval}
}
// Start launches the background sampling goroutine. Only the first call starts
// it; subsequent calls are no-ops. The goroutine exits when ctx is cancelled.
func (s *Sampler) Start(ctx context.Context) {
s.once.Do(func() {
go s.loop(ctx)
})
}
// Snapshot returns a copy of the latest per-core usage snapshot. Returns nil
// before the first sample completes.
func (s *Sampler) Snapshot() []CoreUsage {
s.mu.RLock()
defer s.mu.RUnlock()
if s.cache == nil {
return nil
}
out := make([]CoreUsage, len(s.cache))
copy(out, s.cache)
return out
}
func (s *Sampler) loop(ctx context.Context) {
prev := readProcStat(s.statPath)
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cur := readProcStat(s.statPath)
usage := computeUsage(prev, cur)
s.mu.Lock()
s.cache = usage
s.mu.Unlock()
prev = cur
}
}
}
// cpuCoreTicks holds the cumulative jiffies for one "cpuN" line.
type cpuCoreTicks struct {
core int
total uint64
idle uint64
}
// readProcStat reads /proc/stat and returns per-core tick totals. The
// aggregate "cpu" line (no digit suffix) is skipped.
func readProcStat(path string) []cpuCoreTicks {
data, _ := os.ReadFile(path)
var cores []cpuCoreTicks
for line := range strings.SplitSeq(string(data), "\n") {
if !strings.HasPrefix(line, "cpu") {
continue
}
fields := strings.Fields(line)
if len(fields) < 5 {
continue
}
name := fields[0]
if name == "cpu" {
continue
}
coreIdx, err := strconv.Atoi(strings.TrimPrefix(name, "cpu"))
if err != nil {
continue
}
var total, idle uint64
for _, f := range fields[1:] {
v, _ := strconv.ParseUint(f, 10, 64)
total += v
}
if len(fields) > 5 {
v4, _ := strconv.ParseUint(fields[4], 10, 64)
v5, _ := strconv.ParseUint(fields[5], 10, 64)
idle = v4 + v5
} else {
v4, _ := strconv.ParseUint(fields[4], 10, 64)
idle = v4
}
cores = append(cores, cpuCoreTicks{core: coreIdx, total: total, idle: idle})
}
return cores
}
// computeUsage converts two snapshots into per-core usage percentages.
func computeUsage(prev, cur []cpuCoreTicks) []CoreUsage {
prevMap := make(map[int]cpuCoreTicks, len(prev))
for _, c := range prev {
prevMap[c.core] = c
}
usage := make([]CoreUsage, 0, len(cur))
for _, c := range cur {
p, ok := prevMap[c.core]
if !ok {
continue
}
dTotal := c.total - p.total
dIdle := c.idle - p.idle
var pct float64
if dTotal > 0 {
pct = float64(dTotal-dIdle) / float64(dTotal) * 100
pct = math.Round(pct*10) / 10
}
usage = append(usage, CoreUsage{Core: c.core, UsagePct: pct})
}
return usage
}