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 }