1
1
package clistat
2
2
3
3
import (
4
+ "bufio"
5
+ "bytes"
6
+ "os"
7
+ "strconv"
4
8
"time"
5
9
6
- "tailscale.com/types/ptr"
7
-
8
10
"golang.org/x/xerrors"
11
+ "tailscale.com/types/ptr"
9
12
)
10
13
11
14
const ()
@@ -15,46 +18,120 @@ const ()
15
18
func (s * Statter ) ContainerCPU () (* Result , error ) {
16
19
// Firstly, check if we are containerized.
17
20
if ok , err := IsContainerized (); err != nil || ! ok {
18
- return nil , nil
21
+ return nil , nil //nolint: nilnil
19
22
}
20
23
21
- used1 , total1 , err := cgroupCPU ()
24
+ used1 , total , err := cgroupCPU ()
22
25
if err != nil {
23
26
return nil , xerrors .Errorf ("get cgroup CPU usage: %w" , err )
24
27
}
25
28
<- time .After (s .sampleInterval )
26
- used2 , total2 , err := cgroupCPU ()
29
+
30
+ // total is unlikely to change. Use the first value.
31
+ used2 , _ , err := cgroupCPU ()
27
32
if err != nil {
28
33
return nil , xerrors .Errorf ("get cgroup CPU usage: %w" , err )
29
34
}
30
35
31
- return & Result {
36
+ r := & Result {
32
37
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
36
42
}
37
43
38
- func cgroupCPU () (used , total float64 , err error ) {
44
+ func cgroupCPU () (used , total time. Duration , err error ) {
39
45
if isCGroupV2 () {
40
- return cGroupv2CPU ()
46
+ return cGroupV2CPU ()
41
47
}
42
48
43
49
// Fall back to CGroupv1
44
- return cGroupv1CPU ()
50
+ return cGroupV1CPU ()
45
51
}
46
52
47
53
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
50
57
}
51
58
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
55
132
}
56
133
57
- func cGroupv1CPU () (float64 , float64 , error ) {
134
+ func cGroupV1CPU () (time. Duration , time. Duration , error ) {
58
135
// TODO: implement
59
136
return 0 , 0 , nil
60
137
}
0 commit comments