Skip to content

Commit 1b51f68

Browse files
feat: add ability to get individual controller stats
This adds methods to retrieve statistics for individual cgroup controllers (CPU, memory, pids, IO, hugetlb, rdma, misc) instead of requiring all stats to be fetched at once. This enables tools like cadvisor to collect specific metrics with different housekeeping intervals, reducing computational overhead. Fixes: #44 Signed-off-by: Sambhav Jain <jnsmbhv@gmail.com>
1 parent 496b3eb commit 1b51f68

File tree

22 files changed

+733
-28
lines changed

22 files changed

+733
-28
lines changed

cgroups.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ type Manager interface {
4444
// GetStats returns cgroups statistics.
4545
GetStats() (*Stats, error)
4646

47+
// Stats returns statistics for specified controllers.
48+
// If opts is nil or opts.Controllers is 0, all controllers are queried.
49+
Stats(opts *StatsOptions) (*Stats, error)
50+
4751
// Freeze sets the freezer cgroup to the specified state.
4852
Freeze(state FreezerState) error
4953

fs/blkio.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ func (s *BlkioGroup) Name() string {
1919
return "blkio"
2020
}
2121

22+
// ID returns the controller ID for blkio subsystem.
23+
func (s *BlkioGroup) ID() cgroups.Controller {
24+
return cgroups.IO
25+
}
26+
2227
func (s *BlkioGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
2328
return apply(path, pid)
2429
}

fs/cpu.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ func (s *CpuGroup) Name() string {
1818
return "cpu"
1919
}
2020

21+
// ID returns the controller ID for CPU subsystem.
22+
func (s *CpuGroup) ID() cgroups.Controller {
23+
return cgroups.CPU
24+
}
25+
2126
func (s *CpuGroup) Apply(path string, r *cgroups.Resources, pid int) error {
2227
if err := os.MkdirAll(path, 0o755); err != nil {
2328
return err

fs/cpuacct.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ func (s *CpuacctGroup) Name() string {
2626
return "cpuacct"
2727
}
2828

29+
// ID returns the controller ID for cpuacct subsystem.
30+
func (s *CpuacctGroup) ID() cgroups.Controller {
31+
return cgroups.CPU
32+
}
33+
2934
func (s *CpuacctGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
3035
return apply(path, pid)
3136
}

fs/cpuset.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ func (s *CpusetGroup) Name() string {
5454
return "cpuset"
5555
}
5656

57+
// ID returns the controller ID for cpuset subsystem.
58+
func (s *CpusetGroup) ID() cgroups.Controller {
59+
return cgroups.CPUSet
60+
}
61+
5762
func (s *CpusetGroup) Apply(path string, r *cgroups.Resources, pid int) error {
5863
return s.ApplyDir(path, r, pid)
5964
}

fs/devices.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ func (s *DevicesGroup) Name() string {
1010
return "devices"
1111
}
1212

13+
// ID returns the controller ID for devices subsystem.
14+
// Returns 0 as devices is not a cgroups.Controller.
15+
func (s *DevicesGroup) ID() cgroups.Controller {
16+
return 0
17+
}
18+
1319
func (s *DevicesGroup) Apply(path string, r *cgroups.Resources, pid int) error {
1420
if r.SkipDevices {
1521
return nil

fs/freezer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ func (s *FreezerGroup) Name() string {
1818
return "freezer"
1919
}
2020

21+
// ID returns the controller ID for freezer subsystem.
22+
// Returns 0 as freezer is not a cgroups.Controller.
23+
func (s *FreezerGroup) ID() cgroups.Controller {
24+
return 0
25+
}
26+
2127
func (s *FreezerGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
2228
return apply(path, pid)
2329
}

fs/fs.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var subsystems = []subsystem{
2929
&FreezerGroup{},
3030
&RdmaGroup{},
3131
&NameGroup{GroupName: "name=systemd", Join: true},
32-
&NameGroup{GroupName: "misc", Join: true},
32+
&NameGroup{GroupName: "misc", Join: true, GroupID: cgroups.Misc},
3333
}
3434

3535
var errSubsystemDoesNotExist = errors.New("cgroup: subsystem does not exist")
@@ -45,6 +45,8 @@ func init() {
4545
type subsystem interface {
4646
// Name returns the name of the subsystem.
4747
Name() string
48+
// ID returns the controller ID for filtering.
49+
ID() cgroups.Controller
4850
// GetStats fills in the stats for the subsystem.
4951
GetStats(path string, stats *cgroups.Stats) error
5052
// Apply creates and joins a cgroup, adding pid into it. Some
@@ -181,14 +183,33 @@ func (m *Manager) Path(subsys string) string {
181183
}
182184

183185
func (m *Manager) GetStats() (*cgroups.Stats, error) {
186+
return m.Stats(nil)
187+
}
188+
189+
// Stats returns cgroup statistics for the specified controllers.
190+
// If opts is nil or opts.Controllers is zero, statistics for all controllers are returned.
191+
func (m *Manager) Stats(opts *cgroups.StatsOptions) (*cgroups.Stats, error) {
184192
m.mu.Lock()
185193
defer m.mu.Unlock()
194+
195+
// Default: query all controllers
196+
controllers := cgroups.AllControllers
197+
if opts != nil && opts.Controllers != 0 {
198+
controllers = opts.Controllers
199+
}
200+
186201
stats := cgroups.NewStats()
187202
for _, sys := range subsystems {
188203
path := m.paths[sys.Name()]
189204
if path == "" {
190205
continue
191206
}
207+
208+
// Filter based on controller type
209+
if sys.ID()&controllers == 0 {
210+
continue
211+
}
212+
192213
if err := sys.GetStats(path, stats); err != nil {
193214
return nil, err
194215
}

fs/fs_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,215 @@ import (
66
"github.com/opencontainers/cgroups"
77
)
88

9+
// pointerTo returns a pointer to the given controller value.
10+
func pointerTo(c cgroups.Controller) *cgroups.Controller {
11+
return &c
12+
}
13+
14+
func TestStats(t *testing.T) {
15+
testCases := []struct {
16+
name string
17+
controller *cgroups.Controller
18+
subsystems map[string]map[string]string // subsystem -> file contents
19+
validate func(*testing.T, *cgroups.Stats)
20+
}{
21+
{
22+
name: "CPU stats",
23+
controller: pointerTo(cgroups.CPU),
24+
subsystems: map[string]map[string]string{
25+
"cpu": {
26+
"cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n",
27+
},
28+
"cpuacct": {
29+
"cpuacct.usage": cpuAcctUsageContents,
30+
"cpuacct.usage_percpu": cpuAcctUsagePerCPUContents,
31+
"cpuacct.stat": cpuAcctStatContents,
32+
},
33+
},
34+
validate: func(t *testing.T, stats *cgroups.Stats) {
35+
// Verify throttling data from cpu.stat
36+
expectedThrottling := cgroups.ThrottlingData{
37+
Periods: 2000,
38+
ThrottledPeriods: 200,
39+
ThrottledTime: 18446744073709551615,
40+
}
41+
expectThrottlingDataEquals(t, expectedThrottling, stats.CpuStats.ThrottlingData)
42+
43+
// Verify total usage from cpuacct.usage
44+
if stats.CpuStats.CpuUsage.TotalUsage != 12262454190222160 {
45+
t.Errorf("expected TotalUsage 12262454190222160, got %d", stats.CpuStats.CpuUsage.TotalUsage)
46+
}
47+
},
48+
},
49+
{
50+
name: "Memory stats",
51+
controller: pointerTo(cgroups.Memory),
52+
subsystems: map[string]map[string]string{
53+
"memory": {
54+
"memory.stat": memoryStatContents,
55+
"memory.usage_in_bytes": "2048",
56+
"memory.max_usage_in_bytes": "4096",
57+
"memory.failcnt": "100",
58+
"memory.limit_in_bytes": "8192",
59+
"memory.use_hierarchy": "1",
60+
},
61+
},
62+
validate: func(t *testing.T, stats *cgroups.Stats) {
63+
expected := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}
64+
expectMemoryDataEquals(t, expected, stats.MemoryStats.Usage)
65+
},
66+
},
67+
{
68+
name: "Pids stats",
69+
controller: pointerTo(cgroups.Pids),
70+
subsystems: map[string]map[string]string{
71+
"pids": {
72+
"pids.current": "1337",
73+
"pids.max": "1024",
74+
},
75+
},
76+
validate: func(t *testing.T, stats *cgroups.Stats) {
77+
if stats.PidsStats.Current != 1337 {
78+
t.Errorf("expected Current 1337, got %d", stats.PidsStats.Current)
79+
}
80+
if stats.PidsStats.Limit != 1024 {
81+
t.Errorf("expected Limit 1024, got %d", stats.PidsStats.Limit)
82+
}
83+
},
84+
},
85+
{
86+
name: "IO stats",
87+
controller: pointerTo(cgroups.IO),
88+
subsystems: map[string]map[string]string{
89+
"blkio": blkioBFQStatsTestFiles,
90+
},
91+
validate: func(t *testing.T, stats *cgroups.Stats) {
92+
// Verify we have entries
93+
if len(stats.BlkioStats.IoServiceBytesRecursive) == 0 {
94+
t.Error("expected IoServiceBytesRecursive to have entries")
95+
}
96+
if len(stats.BlkioStats.IoServicedRecursive) == 0 {
97+
t.Error("expected IoServicedRecursive to have entries")
98+
}
99+
},
100+
},
101+
{
102+
name: "Multiple controllers - CPU+Pids",
103+
controller: pointerTo(cgroups.CPU | cgroups.Pids),
104+
subsystems: map[string]map[string]string{
105+
"cpu": {
106+
"cpu.stat": "nr_periods 100\nnr_throttled 10\nthrottled_time 5000\n",
107+
},
108+
"pids": {
109+
"pids.current": "42",
110+
"pids.max": "1000",
111+
},
112+
},
113+
validate: func(t *testing.T, stats *cgroups.Stats) {
114+
// Verify both are populated
115+
if stats.CpuStats.ThrottlingData.Periods != 100 {
116+
t.Errorf("expected Periods 100, got %d", stats.CpuStats.ThrottlingData.Periods)
117+
}
118+
if stats.PidsStats.Current != 42 {
119+
t.Errorf("expected Current 42, got %d", stats.PidsStats.Current)
120+
}
121+
if stats.PidsStats.Limit != 1000 {
122+
t.Errorf("expected Limit 1000, got %d", stats.PidsStats.Limit)
123+
}
124+
},
125+
},
126+
{
127+
name: "All controllers with nil options",
128+
controller: nil, // nil means all controllers (default behavior)
129+
subsystems: map[string]map[string]string{
130+
"cpu": {
131+
"cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n",
132+
},
133+
"cpuacct": {
134+
"cpuacct.usage": cpuAcctUsageContents,
135+
"cpuacct.usage_percpu": cpuAcctUsagePerCPUContents,
136+
"cpuacct.stat": cpuAcctStatContents,
137+
},
138+
"memory": {
139+
"memory.stat": memoryStatContents,
140+
"memory.usage_in_bytes": "2048",
141+
"memory.max_usage_in_bytes": "4096",
142+
"memory.failcnt": "100",
143+
"memory.limit_in_bytes": "8192",
144+
"memory.use_hierarchy": "1",
145+
},
146+
"pids": {
147+
"pids.current": "1337",
148+
"pids.max": "1024",
149+
},
150+
"blkio": blkioBFQStatsTestFiles,
151+
},
152+
validate: func(t *testing.T, stats *cgroups.Stats) {
153+
// Verify CPU stats
154+
expectedThrottling := cgroups.ThrottlingData{
155+
Periods: 2000,
156+
ThrottledPeriods: 200,
157+
ThrottledTime: 18446744073709551615,
158+
}
159+
expectThrottlingDataEquals(t, expectedThrottling, stats.CpuStats.ThrottlingData)
160+
if stats.CpuStats.CpuUsage.TotalUsage != 12262454190222160 {
161+
t.Errorf("expected TotalUsage 12262454190222160, got %d", stats.CpuStats.CpuUsage.TotalUsage)
162+
}
163+
164+
// Verify Memory stats
165+
expectedMemory := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}
166+
expectMemoryDataEquals(t, expectedMemory, stats.MemoryStats.Usage)
167+
168+
// Verify Pids stats
169+
if stats.PidsStats.Current != 1337 {
170+
t.Errorf("expected Current 1337, got %d", stats.PidsStats.Current)
171+
}
172+
if stats.PidsStats.Limit != 1024 {
173+
t.Errorf("expected Limit 1024, got %d", stats.PidsStats.Limit)
174+
}
175+
176+
// Verify IO stats
177+
if len(stats.BlkioStats.IoServiceBytesRecursive) == 0 {
178+
t.Error("expected IoServiceBytesRecursive to have entries")
179+
}
180+
if len(stats.BlkioStats.IoServicedRecursive) == 0 {
181+
t.Error("expected IoServicedRecursive to have entries")
182+
}
183+
},
184+
},
185+
}
186+
187+
for _, tc := range testCases {
188+
t.Run(tc.name, func(t *testing.T) {
189+
// Create temp directories for each subsystem and write files
190+
paths := make(map[string]string)
191+
for subsystem, files := range tc.subsystems {
192+
path := tempDir(t, subsystem)
193+
writeFileContents(t, path, files)
194+
paths[subsystem] = path
195+
}
196+
m := &Manager{
197+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
198+
paths: paths,
199+
}
200+
201+
var stats *cgroups.Stats
202+
var err error
203+
if tc.controller != nil {
204+
stats, err = m.Stats(&cgroups.StatsOptions{Controllers: *tc.controller})
205+
} else {
206+
stats, err = m.Stats(nil)
207+
}
208+
if err != nil {
209+
t.Fatal(err)
210+
}
211+
212+
// Validate the results
213+
tc.validate(t, stats)
214+
})
215+
}
216+
}
217+
9218
func BenchmarkGetStats(b *testing.B) {
10219
if cgroups.IsCgroup2UnifiedMode() {
11220
b.Skip("cgroup v2 is not supported")

fs/hugetlb.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ func (s *HugetlbGroup) Name() string {
1515
return "hugetlb"
1616
}
1717

18+
// ID returns the controller ID for hugetlb subsystem.
19+
func (s *HugetlbGroup) ID() cgroups.Controller {
20+
return cgroups.HugeTLB
21+
}
22+
1823
func (s *HugetlbGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
1924
return apply(path, pid)
2025
}

0 commit comments

Comments
 (0)