Skip to content

Commit 7108c6e

Browse files
committed
initial cgroupv2 cpu implementation
1 parent 3528c00 commit 7108c6e

File tree

2 files changed

+98
-21
lines changed

2 files changed

+98
-21
lines changed

cli/clistat/cgroup_linux.go

Lines changed: 95 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package clistat
22

33
import (
4+
"bufio"
5+
"bytes"
6+
"os"
7+
"strconv"
48
"time"
59

6-
"tailscale.com/types/ptr"
7-
810
"golang.org/x/xerrors"
11+
"tailscale.com/types/ptr"
912
)
1013

1114
const ()
@@ -15,46 +18,120 @@ const ()
1518
func (s *Statter) ContainerCPU() (*Result, error) {
1619
// Firstly, check if we are containerized.
1720
if ok, err := IsContainerized(); err != nil || !ok {
18-
return nil, nil
21+
return nil, nil //nolint: nilnil
1922
}
2023

21-
used1, total1, err := cgroupCPU()
24+
used1, total, err := cgroupCPU()
2225
if err != nil {
2326
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
2427
}
2528
<-time.After(s.sampleInterval)
26-
used2, total2, err := cgroupCPU()
29+
30+
// total is unlikely to change. Use the first value.
31+
used2, _, err := cgroupCPU()
2732
if err != nil {
2833
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
2934
}
3035

31-
return &Result{
36+
r := &Result{
3237
Unit: "cores",
33-
Used: used2 - used1,
34-
Total: ptr.To(total2 - total1),
35-
}, nil
38+
Used: (used2 - used1).Seconds(),
39+
Total: ptr.To(total.Seconds()), // close enough to the truth
40+
}
41+
return r, nil
3642
}
3743

38-
func cgroupCPU() (used, total float64, err error) {
44+
func cgroupCPU() (used, total time.Duration, err error) {
3945
if isCGroupV2() {
40-
return cGroupv2CPU()
46+
return cGroupV2CPU()
4147
}
4248

4349
// Fall back to CGroupv1
44-
return cGroupv1CPU()
50+
return cGroupV1CPU()
4551
}
4652

4753
func isCGroupV2() bool {
48-
// TODO implement
49-
return false
54+
// Check for the presence of /sys/fs/cgroup/cpu.max
55+
_, err := os.Stat("/sys/fs/cgroup/cpu.max")
56+
return err == nil
5057
}
5158

52-
func cGroupv2CPU() (float64, float64, error) {
53-
// TODO: implement
54-
return 0, 0, nil
59+
func cGroupV2CPU() (used, total time.Duration, err error) {
60+
total, err = cGroupv2CPUTotal()
61+
if err != nil {
62+
return 0, 0, xerrors.Errorf("get cgroup v2 CPU cores: %w", err)
63+
}
64+
65+
used, err = cGroupv2CPUUsed()
66+
if err != nil {
67+
return 0, 0, xerrors.Errorf("get cgroup v2 CPU used: %w", err)
68+
}
69+
70+
return used, total, nil
71+
}
72+
73+
func cGroupv2CPUUsed() (used time.Duration, err error) {
74+
var data []byte
75+
data, err = os.ReadFile("/sys/fs/cgroup/cpu.stat")
76+
if err != nil {
77+
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.stat: %w", err)
78+
}
79+
80+
s := bufio.NewScanner(bytes.NewReader(data))
81+
for s.Scan() {
82+
line := s.Bytes()
83+
if !bytes.HasPrefix(line, []byte("usage_usec ")) {
84+
continue
85+
}
86+
87+
parts := bytes.Split(line, []byte(" "))
88+
if len(parts) != 2 {
89+
return 0, xerrors.Errorf("unexpected line in /sys/fs/cgroup/cpu.stat: %s", line)
90+
}
91+
92+
iused, err := strconv.Atoi(string(parts[1]))
93+
if err != nil {
94+
return 0, xerrors.Errorf("parse /sys/fs/cgroup/cpu.stat: %w", err)
95+
}
96+
97+
return time.Duration(iused) * time.Microsecond, nil
98+
}
99+
100+
return 0, xerrors.Errorf("did not find expected usage_usec in /sys/fs/cgroup/cpu.stat")
101+
}
102+
103+
func cGroupv2CPUTotal() (total time.Duration, err error) {
104+
var data []byte
105+
var quotaUs int
106+
data, err = os.ReadFile("/sys/fs/cgroup/cpu.max")
107+
if err != nil {
108+
return 0, xerrors.Errorf("read /sys/fs/cgroup/cpu.max: %w", err)
109+
}
110+
111+
lines := bytes.Split(data, []byte("\n"))
112+
if len(lines) < 1 {
113+
return 0, xerrors.Errorf("unexpected empty /sys/fs/cgroup/cpu.max")
114+
}
115+
116+
line := lines[0]
117+
parts := bytes.Split(line, []byte(" "))
118+
if len(parts) != 2 {
119+
return 0, xerrors.Errorf("unexpected line in /sys/fs/cgroup/cpu.max: %s", line)
120+
}
121+
122+
if bytes.Equal(parts[0], []byte("max")) {
123+
quotaUs = nproc * int(time.Second.Microseconds())
124+
} else {
125+
quotaUs, err = strconv.Atoi(string(parts[0]))
126+
if err != nil {
127+
return 0, xerrors.Errorf("parse /sys/fs/cgroup/cpu.max: %w", err)
128+
}
129+
}
130+
131+
return time.Duration(quotaUs) * time.Microsecond, nil
55132
}
56133

57-
func cGroupv1CPU() (float64, float64, error) {
134+
func cGroupV1CPU() (time.Duration, time.Duration, error) {
58135
// TODO: implement
59136
return 0, 0, nil
60137
}

cli/clistat/stat.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
const procOneCgroup = "/proc/1/cgroup"
1717

18-
var nproc = float64(runtime.NumCPU())
18+
var nproc = runtime.NumCPU()
1919

2020
// Result is a generic result type for a statistic.
2121
// Total is the total amount of the resource available.
@@ -87,7 +87,7 @@ func New(opts ...Option) (*Statter, error) {
8787
func (s *Statter) HostCPU() (*Result, error) {
8888
r := &Result{
8989
Unit: "cores",
90-
Total: ptr.To(nproc),
90+
Total: ptr.To(float64(nproc)),
9191
}
9292
c1, err := s.hi.CPUTime()
9393
if err != nil {
@@ -101,7 +101,7 @@ func (s *Statter) HostCPU() (*Result, error) {
101101
total := c2.Total() - c1.Total()
102102
idle := c2.Idle - c1.Idle
103103
used := total - idle
104-
scaleFactor := nproc / total.Seconds()
104+
scaleFactor := float64(nproc) / total.Seconds()
105105
r.Used = used.Seconds() * scaleFactor
106106
return r, nil
107107
}

0 commit comments

Comments
 (0)