//go:build linux package utils import ( "errors" "os" "regexp" "runtime" "strconv" "time" ) var ( usageRegex = regexp.MustCompile("usage_usec ([0-9]+)") ) const ( cpuStatsPathV1 = "/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage" cpuStatsPathV2 = "/sys/fs/cgroup/cpu.stat" ) type platformCPUMonitor struct { lastSampleTime int64 lastTotalCPUTime int64 cpuTimeFunc func() (int64, error) } func newPlatformCPUMonitor() (*platformCPUMonitor, error) { // probe for the cgroup version var cpuTimeFunc func() (int64, error) for k, v := range map[string]func() (int64, error){ cpuStatsPathV1: getTotalCPUTimeV1, cpuStatsPathV2: getTotalCPUTimeV2, } { e, err := fileExists(k) if err != nil { return nil, err } if e { cpuTimeFunc = v break } } if cpuTimeFunc == nil { return nil, errors.New("failed reading cpu stats file") } cpu, err := cpuTimeFunc() if err != nil { return nil, err } return &platformCPUMonitor{ lastSampleTime: time.Now().UnixNano(), lastTotalCPUTime: cpu, cpuTimeFunc: cpuTimeFunc, }, nil } func (p *platformCPUMonitor) getCPUIdle() (float64, error) { next, err := p.cpuTimeFunc() if err != nil { return 0, err } t := time.Now().UnixNano() duration := t - p.lastSampleTime cpuTime := next - p.lastTotalCPUTime busyRatio := float64(cpuTime) / float64(duration) idleRatio := float64(runtime.NumCPU()) - busyRatio // Clamp the value as we do not get all the timestamps at the same time if idleRatio > float64(runtime.NumCPU()) { idleRatio = float64(runtime.NumCPU()) } else if idleRatio < 0 { idleRatio = 0 } p.lastSampleTime = t p.lastTotalCPUTime = next return idleRatio, nil } func getTotalCPUTimeV1() (int64, error) { b, err := os.ReadFile(cpuStatsPathV1) if err != nil { return 0, err } // Skip the trailing EOL i, err := strconv.ParseInt(string(b[:len(b)-1]), 10, 64) if err != nil { return 0, err } return i, nil } func getTotalCPUTimeV2() (int64, error) { b, err := os.ReadFile(cpuStatsPathV2) if err != nil { return 0, err } m := usageRegex.FindSubmatch(b) if len(m) <= 1 { return 0, errors.New("could not parse cpu stats") } i, err := strconv.ParseInt(string(m[1]), 10, 64) if err != nil { return 0, err } // Caller expexts time in ns return i * 1000, nil } func fileExists(path string) (bool, error) { _, err := os.Lstat(path) switch { case err == nil: return true, nil case errors.Is(err, os.ErrNotExist): return false, nil default: return false, err } }