github.com/whoyao/protocol@v0.0.0-20230519045905-2d8ace718ca5/utils/cpu_linux.go (about)

     1  //go:build linux
     2  
     3  package utils
     4  
     5  import (
     6  	"errors"
     7  	"os"
     8  	"regexp"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/whoyao/protocol/logger"
    15  )
    16  
    17  var (
    18  	usageRegex = regexp.MustCompile("usage_usec ([0-9]+)")
    19  )
    20  
    21  const (
    22  	cpuStatsPathV1 = "/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage"
    23  	cpuStatsPathV2 = "/sys/fs/cgroup/cpu.stat"
    24  
    25  	numCPUPathV1 = "/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage_percpu"
    26  	numCPUPathV2 = "/sys/fs/cgroup/cpu.max"
    27  )
    28  
    29  type cpuInfoGetter interface {
    30  	getTotalCPUTime() (int64, error)
    31  	numCPU() (int, error)
    32  }
    33  
    34  type cgroupCPUMonitor struct {
    35  	lastSampleTime   int64
    36  	lastTotalCPUTime int64
    37  	nCPU             int
    38  
    39  	cg cpuInfoGetter
    40  }
    41  
    42  func newPlatformCPUMonitor() (platformCPUMonitor, error) {
    43  	// probe for the cgroup version
    44  	var cg cpuInfoGetter
    45  	for k, v := range map[string]func() cpuInfoGetter{
    46  		cpuStatsPathV1: newCpuInfoGetterV1,
    47  		cpuStatsPathV2: newCpuInfoGetterV2,
    48  	} {
    49  		e, err := fileExists(k)
    50  		if err != nil {
    51  			return nil, err
    52  		}
    53  		if e {
    54  			cg = v()
    55  			break
    56  		}
    57  	}
    58  	if cg == nil {
    59  		logger.Infow("failed reading cgroup specific cpu stats, falling back to system wide implementation")
    60  		return newOsstatCPUMonitor()
    61  	}
    62  
    63  	cpu, err := cg.getTotalCPUTime()
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	nCPU, err := cg.numCPU()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	return &cgroupCPUMonitor{
    74  		lastSampleTime:   time.Now().UnixNano(),
    75  		lastTotalCPUTime: cpu,
    76  		nCPU:             nCPU,
    77  		cg:               cg,
    78  	}, nil
    79  }
    80  
    81  func (p *cgroupCPUMonitor) getCPUIdle() (float64, error) {
    82  	next, err := p.cg.getTotalCPUTime()
    83  	if err != nil {
    84  		return 0, err
    85  	}
    86  	t := time.Now().UnixNano()
    87  
    88  	duration := t - p.lastSampleTime
    89  	cpuTime := next - p.lastTotalCPUTime
    90  
    91  	busyRatio := float64(cpuTime) / float64(duration)
    92  	idleRatio := float64(p.nCPU) - busyRatio
    93  
    94  	// Clamp the value as we do not get all the timestamps at the same time
    95  	if idleRatio > float64(p.nCPU) {
    96  		idleRatio = float64(p.nCPU)
    97  	} else if idleRatio < 0 {
    98  		idleRatio = 0
    99  	}
   100  
   101  	p.lastSampleTime = t
   102  	p.lastTotalCPUTime = next
   103  
   104  	return idleRatio, nil
   105  }
   106  
   107  func (p *cgroupCPUMonitor) numCPU() int {
   108  	return p.nCPU
   109  }
   110  
   111  type cpuInfoGetterV1 struct {
   112  }
   113  
   114  func newCpuInfoGetterV1() cpuInfoGetter {
   115  	return &cpuInfoGetterV1{}
   116  }
   117  
   118  func (cg *cpuInfoGetterV1) getTotalCPUTime() (int64, error) {
   119  	b, err := os.ReadFile(cpuStatsPathV1)
   120  	if err != nil {
   121  		return 0, err
   122  	}
   123  
   124  	// Skip the trailing EOL
   125  	i, err := strconv.ParseInt(string(b[:len(b)-1]), 10, 64)
   126  	if err != nil {
   127  		return 0, err
   128  	}
   129  
   130  	return i, nil
   131  }
   132  
   133  func (cg *cpuInfoGetterV1) numCPU() (int, error) {
   134  	b, err := os.ReadFile(numCPUPathV1)
   135  	if err != nil {
   136  		return 0, err
   137  	}
   138  
   139  	// Remove trailing new line if any
   140  	s := strings.TrimSuffix(string(b), "\n")
   141  
   142  	// Remove trailing space if any
   143  	s = strings.TrimSuffix(s, " ")
   144  
   145  	m := strings.Split(s, " ")
   146  	if len(m) == 0 {
   147  		return 0, errors.New("could not parse cpu stats")
   148  	}
   149  
   150  	cpuCount := 0
   151  	for _, v := range m {
   152  		if v != "0" {
   153  			cpuCount++
   154  		}
   155  	}
   156  
   157  	return cpuCount, nil
   158  }
   159  
   160  type cpuInfoGetterV2 struct {
   161  }
   162  
   163  func newCpuInfoGetterV2() cpuInfoGetter {
   164  	return &cpuInfoGetterV2{}
   165  }
   166  
   167  func (cg *cpuInfoGetterV2) getTotalCPUTime() (int64, error) {
   168  	b, err := os.ReadFile(cpuStatsPathV2)
   169  	if err != nil {
   170  		return 0, err
   171  	}
   172  
   173  	m := usageRegex.FindSubmatch(b)
   174  	if len(m) <= 1 {
   175  		return 0, errors.New("could not parse cpu stats")
   176  	}
   177  
   178  	i, err := strconv.ParseInt(string(m[1]), 10, 64)
   179  	if err != nil {
   180  		return 0, err
   181  	}
   182  
   183  	// Caller expexts time in ns
   184  	return i * 1000, nil
   185  }
   186  
   187  func (cg *cpuInfoGetterV2) numCPU() (int, error) {
   188  	b, err := os.ReadFile(numCPUPathV2)
   189  	if err != nil {
   190  		return 0, err
   191  	}
   192  
   193  	s := strings.TrimSuffix(string(b), "\n")
   194  
   195  	m := strings.Split(s, " ")
   196  	if len(m) <= 1 {
   197  		return 0, errors.New("could not parse cpu stats")
   198  	}
   199  
   200  	if m[0] == "max" {
   201  		// No quota
   202  		return runtime.NumCPU(), nil
   203  	} else {
   204  		n, err := strconv.ParseInt(string(m[0]), 10, 64)
   205  		if err != nil {
   206  			return 0, err
   207  		}
   208  
   209  		d, err := strconv.ParseInt(string(m[1]), 10, 64)
   210  		if err != nil {
   211  			return 0, err
   212  		}
   213  
   214  		return int(n / d), nil
   215  	}
   216  }
   217  
   218  func fileExists(path string) (bool, error) {
   219  	_, err := os.Lstat(path)
   220  	switch {
   221  	case err == nil:
   222  		return true, nil
   223  	case errors.Is(err, os.ErrNotExist):
   224  		return false, nil
   225  	default:
   226  		return false, err
   227  	}
   228  }