138 lines
3.2 KiB
Go
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
|
|
}
|