Skip to content

Commit a8d3912

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 5777053 commit a8d3912

File tree

9 files changed

+703
-26
lines changed

9 files changed

+703
-26
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/fs.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,33 @@ func (m *Manager) Path(subsys string) string {
181181
}
182182

183183
func (m *Manager) GetStats() (*cgroups.Stats, error) {
184+
return m.Stats(nil)
185+
}
186+
187+
// Stats returns cgroup statistics for the specified controllers.
188+
// If opts is nil or opts.Controllers is zero, statistics for all controllers are returned.
189+
func (m *Manager) Stats(opts *cgroups.StatsOptions) (*cgroups.Stats, error) {
184190
m.mu.Lock()
185191
defer m.mu.Unlock()
192+
193+
// Default: query all controllers
194+
controllers := cgroups.GetAllControllers()
195+
if opts != nil && opts.Controllers != 0 {
196+
controllers = opts.Controllers
197+
}
198+
186199
stats := cgroups.NewStats()
187200
for _, sys := range subsystems {
188201
path := m.paths[sys.Name()]
189202
if path == "" {
190203
continue
191204
}
205+
206+
// Filter based on controller type
207+
if !cgroups.ShouldIncludeSubsystem(sys.Name(), controllers) {
208+
continue
209+
}
210+
192211
if err := sys.GetStats(path, stats); err != nil {
193212
return nil, err
194213
}

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")

fs2/fs2.go

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,50 +105,86 @@ func (m *Manager) GetAllPids() ([]int, error) {
105105
}
106106

107107
func (m *Manager) GetStats() (*cgroups.Stats, error) {
108-
var errs []error
108+
return m.Stats(nil)
109+
}
110+
111+
// Stats returns cgroup statistics for the specified controllers.
112+
// If opts is nil or opts.Controllers is zero, statistics for all controllers are returned.
113+
func (m *Manager) Stats(opts *cgroups.StatsOptions) (*cgroups.Stats, error) {
114+
// Default: query all controllers
115+
controllers := cgroups.GetAllControllers()
116+
if opts != nil && opts.Controllers != 0 {
117+
controllers = opts.Controllers
118+
}
109119

120+
var errs []error
121+
var err error
110122
st := cgroups.NewStats()
111123

112124
// pids (since kernel 4.5)
113-
if err := statPids(m.dirPath, st); err != nil {
114-
errs = append(errs, err)
125+
if controllers&cgroups.Pids != 0 {
126+
if err = statPids(m.dirPath, st); err != nil {
127+
errs = append(errs, err)
128+
}
115129
}
130+
116131
// memory (since kernel 4.5)
117-
if err := statMemory(m.dirPath, st); err != nil && !os.IsNotExist(err) {
118-
errs = append(errs, err)
132+
if controllers&cgroups.Memory != 0 {
133+
if err = statMemory(m.dirPath, st); err != nil && !os.IsNotExist(err) {
134+
errs = append(errs, err)
135+
}
136+
137+
if st.MemoryStats.PSI, err = statPSI(m.dirPath, "memory.pressure"); err != nil {
138+
errs = append(errs, err)
139+
}
119140
}
141+
120142
// io (since kernel 4.5)
121-
if err := statIo(m.dirPath, st); err != nil && !os.IsNotExist(err) {
122-
errs = append(errs, err)
143+
if controllers&cgroups.IO != 0 {
144+
if err = statIo(m.dirPath, st); err != nil && !os.IsNotExist(err) {
145+
errs = append(errs, err)
146+
}
147+
148+
if st.BlkioStats.PSI, err = statPSI(m.dirPath, "io.pressure"); err != nil {
149+
errs = append(errs, err)
150+
}
123151
}
152+
124153
// cpu (since kernel 4.15)
125154
// Note cpu.stat is available even if the controller is not enabled.
126-
if err := statCpu(m.dirPath, st); err != nil && !os.IsNotExist(err) {
127-
errs = append(errs, err)
128-
}
129-
// PSI (since kernel 4.20).
130-
var err error
131-
if st.CpuStats.PSI, err = statPSI(m.dirPath, "cpu.pressure"); err != nil {
132-
errs = append(errs, err)
133-
}
134-
if st.MemoryStats.PSI, err = statPSI(m.dirPath, "memory.pressure"); err != nil {
135-
errs = append(errs, err)
136-
}
137-
if st.BlkioStats.PSI, err = statPSI(m.dirPath, "io.pressure"); err != nil {
138-
errs = append(errs, err)
155+
if controllers&cgroups.CPU != 0 {
156+
if err = statCpu(m.dirPath, st); err != nil && !os.IsNotExist(err) {
157+
errs = append(errs, err)
158+
}
159+
160+
// PSI (since kernel 4.20)
161+
if st.CpuStats.PSI, err = statPSI(m.dirPath, "cpu.pressure"); err != nil {
162+
errs = append(errs, err)
163+
}
164+
139165
}
166+
140167
// hugetlb (since kernel 5.6)
141-
if err := statHugeTlb(m.dirPath, st); err != nil && !os.IsNotExist(err) {
142-
errs = append(errs, err)
168+
if controllers&cgroups.HugeTLB != 0 {
169+
if err := statHugeTlb(m.dirPath, st); err != nil && !os.IsNotExist(err) {
170+
errs = append(errs, err)
171+
}
143172
}
173+
144174
// rdma (since kernel 4.11)
145-
if err := fscommon.RdmaGetStats(m.dirPath, st); err != nil && !os.IsNotExist(err) {
146-
errs = append(errs, err)
175+
if controllers&cgroups.RDMA != 0 {
176+
if err := fscommon.RdmaGetStats(m.dirPath, st); err != nil && !os.IsNotExist(err) {
177+
errs = append(errs, err)
178+
}
147179
}
180+
148181
// misc (since kernel 5.13)
149-
if err := statMisc(m.dirPath, st); err != nil && !os.IsNotExist(err) {
150-
errs = append(errs, err)
182+
if controllers&cgroups.Misc != 0 {
183+
if err := statMisc(m.dirPath, st); err != nil && !os.IsNotExist(err) {
184+
errs = append(errs, err)
185+
}
151186
}
187+
152188
if len(errs) > 0 && !m.config.Rootless {
153189
return st, fmt.Errorf("error while statting cgroup v2: %+v", errs)
154190
}

0 commit comments

Comments
 (0)