Skip to content

Commit b0140c4

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 e0c56cb commit b0140c4

File tree

7 files changed

+904
-0
lines changed

7 files changed

+904
-0
lines changed

cgroups.go

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

47+
// AddCpuStats adds cpu statistics to the provided stats object.
48+
AddCpuStats(stats *Stats) error
49+
50+
// AddMemoryStats adds memory statistics to the provided stats object.
51+
AddMemoryStats(stats *Stats) error
52+
53+
// AddPidsStats adds pids statistics to the provided stats object.
54+
AddPidsStats(stats *Stats) error
55+
56+
// AddIoStats adds io statistics to the provided stats object.
57+
AddIoStats(stats *Stats) error
58+
59+
// AddHugetlbStats adds hugetlb statistics to the provided stats object.
60+
AddHugetlbStats(stats *Stats) error
61+
62+
// AddRdmaStats adds rdma statistics to the provided stats object.
63+
AddRdmaStats(stats *Stats) error
64+
65+
// AddMiscStats adds misc statistics to the provided stats object.
66+
AddMiscStats(stats *Stats) error
67+
4768
// Freeze sets the freezer cgroup to the specified state.
4869
Freeze(state FreezerState) error
4970

fs/fs.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,104 @@ func (m *Manager) GetStats() (*cgroups.Stats, error) {
196196
return stats, nil
197197
}
198198

199+
func (m *Manager) AddCpuStats(stats *cgroups.Stats) error {
200+
m.mu.Lock()
201+
defer m.mu.Unlock()
202+
203+
cpuGroup := &CpuGroup{}
204+
if path := m.paths["cpu"]; path != "" {
205+
if err := cpuGroup.GetStats(path, stats); err != nil {
206+
return err
207+
}
208+
}
209+
210+
cpuacctGroup := &CpuacctGroup{}
211+
if path := m.paths["cpuacct"]; path != "" {
212+
if err := cpuacctGroup.GetStats(path, stats); err != nil {
213+
return err
214+
}
215+
}
216+
217+
return nil
218+
}
219+
220+
func (m *Manager) AddMemoryStats(stats *cgroups.Stats) error {
221+
m.mu.Lock()
222+
defer m.mu.Unlock()
223+
224+
memoryGroup := &MemoryGroup{}
225+
if path := m.paths["memory"]; path != "" {
226+
if err := memoryGroup.GetStats(path, stats); err != nil {
227+
return err
228+
}
229+
}
230+
231+
return nil
232+
}
233+
234+
func (m *Manager) AddPidsStats(stats *cgroups.Stats) error {
235+
m.mu.Lock()
236+
defer m.mu.Unlock()
237+
238+
pidsGroup := &PidsGroup{}
239+
if path := m.paths["pids"]; path != "" {
240+
if err := pidsGroup.GetStats(path, stats); err != nil {
241+
return err
242+
}
243+
}
244+
245+
return nil
246+
}
247+
248+
func (m *Manager) AddIoStats(stats *cgroups.Stats) error {
249+
m.mu.Lock()
250+
defer m.mu.Unlock()
251+
252+
blkioGroup := &BlkioGroup{}
253+
if path := m.paths["blkio"]; path != "" {
254+
if err := blkioGroup.GetStats(path, stats); err != nil {
255+
return err
256+
}
257+
}
258+
259+
return nil
260+
}
261+
262+
func (m *Manager) AddHugetlbStats(stats *cgroups.Stats) error {
263+
m.mu.Lock()
264+
defer m.mu.Unlock()
265+
266+
hugetlbGroup := &HugetlbGroup{}
267+
if path := m.paths["hugetlb"]; path != "" {
268+
if err := hugetlbGroup.GetStats(path, stats); err != nil {
269+
return err
270+
}
271+
}
272+
273+
return nil
274+
}
275+
276+
func (m *Manager) AddRdmaStats(stats *cgroups.Stats) error {
277+
m.mu.Lock()
278+
defer m.mu.Unlock()
279+
280+
rdmaGroup := &RdmaGroup{}
281+
if path := m.paths["rdma"]; path != "" {
282+
if err := rdmaGroup.GetStats(path, stats); err != nil {
283+
return err
284+
}
285+
}
286+
287+
return nil
288+
}
289+
290+
func (m *Manager) AddMiscStats(stats *cgroups.Stats) error {
291+
m.mu.Lock()
292+
defer m.mu.Unlock()
293+
294+
return nil
295+
}
296+
199297
func (m *Manager) Set(r *cgroups.Resources) error {
200298
if r == nil {
201299
return nil

fs/fs_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,188 @@ func BenchmarkGetStats(b *testing.B) {
4747
b.Fatalf("stats: %+v", st)
4848
}
4949
}
50+
51+
func TestAddCpuStats(t *testing.T) {
52+
cpuPath := tempDir(t, "cpu")
53+
cpuacctPath := tempDir(t, "cpuacct")
54+
55+
writeFileContents(t, cpuPath, map[string]string{
56+
"cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n",
57+
})
58+
writeFileContents(t, cpuacctPath, map[string]string{
59+
"cpuacct.usage": cpuAcctUsageContents,
60+
"cpuacct.usage_percpu": cpuAcctUsagePerCPUContents,
61+
"cpuacct.stat": cpuAcctStatContents,
62+
})
63+
64+
m := &Manager{
65+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
66+
paths: map[string]string{"cpu": cpuPath, "cpuacct": cpuacctPath},
67+
}
68+
69+
stats := cgroups.NewStats()
70+
if err := m.AddCpuStats(stats); err != nil {
71+
t.Fatal(err)
72+
}
73+
74+
// Verify throttling data from cpu.stat
75+
expectedThrottling := cgroups.ThrottlingData{
76+
Periods: 2000,
77+
ThrottledPeriods: 200,
78+
ThrottledTime: 18446744073709551615,
79+
}
80+
expectThrottlingDataEquals(t, expectedThrottling, stats.CpuStats.ThrottlingData)
81+
82+
// Verify total usage from cpuacct.usage
83+
if stats.CpuStats.CpuUsage.TotalUsage != 12262454190222160 {
84+
t.Errorf("expected TotalUsage 12262454190222160, got %d", stats.CpuStats.CpuUsage.TotalUsage)
85+
}
86+
}
87+
88+
func TestAddPidsStats(t *testing.T) {
89+
path := tempDir(t, "pids")
90+
writeFileContents(t, path, map[string]string{
91+
"pids.current": "1337",
92+
"pids.max": "1024",
93+
})
94+
95+
m := &Manager{
96+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
97+
paths: map[string]string{"pids": path},
98+
}
99+
100+
stats := cgroups.NewStats()
101+
if err := m.AddPidsStats(stats); err != nil {
102+
t.Fatal(err)
103+
}
104+
105+
if stats.PidsStats.Current != 1337 {
106+
t.Errorf("expected Current 1337, got %d", stats.PidsStats.Current)
107+
}
108+
if stats.PidsStats.Limit != 1024 {
109+
t.Errorf("expected Limit 1024, got %d", stats.PidsStats.Limit)
110+
}
111+
}
112+
113+
func TestAddMemoryStats(t *testing.T) {
114+
path := tempDir(t, "memory")
115+
writeFileContents(t, path, map[string]string{
116+
"memory.stat": memoryStatContents,
117+
"memory.usage_in_bytes": "2048",
118+
"memory.max_usage_in_bytes": "4096",
119+
"memory.failcnt": "100",
120+
"memory.limit_in_bytes": "8192",
121+
"memory.use_hierarchy": "1",
122+
})
123+
124+
m := &Manager{
125+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
126+
paths: map[string]string{"memory": path},
127+
}
128+
129+
stats := cgroups.NewStats()
130+
if err := m.AddMemoryStats(stats); err != nil {
131+
t.Fatal(err)
132+
}
133+
134+
expected := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}
135+
expectMemoryDataEquals(t, expected, stats.MemoryStats.Usage)
136+
}
137+
138+
func TestAddIoStats(t *testing.T) {
139+
path := tempDir(t, "blkio")
140+
// Use blkioBFQStatsTestFiles from blkio_test.go for proper file format
141+
writeFileContents(t, path, blkioBFQStatsTestFiles)
142+
143+
m := &Manager{
144+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
145+
paths: map[string]string{"blkio": path},
146+
}
147+
148+
stats := cgroups.NewStats()
149+
if err := m.AddIoStats(stats); err != nil {
150+
t.Fatal(err)
151+
}
152+
153+
// Verify we have entries
154+
if len(stats.BlkioStats.IoServiceBytesRecursive) == 0 {
155+
t.Error("expected IoServiceBytesRecursive to have entries")
156+
}
157+
if len(stats.BlkioStats.IoServicedRecursive) == 0 {
158+
t.Error("expected IoServicedRecursive to have entries")
159+
}
160+
}
161+
162+
func TestAddStatsIterative(t *testing.T) {
163+
// Set up both cpu and pids directories
164+
cpuPath := tempDir(t, "cpu")
165+
pidsPath := tempDir(t, "pids")
166+
167+
writeFileContents(t, cpuPath, map[string]string{
168+
"cpu.stat": "nr_periods 100\nnr_throttled 10\nthrottled_time 5000\n",
169+
})
170+
writeFileContents(t, pidsPath, map[string]string{
171+
"pids.current": "42",
172+
"pids.max": "1000",
173+
})
174+
175+
m := &Manager{
176+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
177+
paths: map[string]string{"cpu": cpuPath, "pids": pidsPath},
178+
}
179+
180+
stats := cgroups.NewStats()
181+
182+
// Call both methods on same stats object
183+
if err := m.AddCpuStats(stats); err != nil {
184+
t.Fatal(err)
185+
}
186+
if err := m.AddPidsStats(stats); err != nil {
187+
t.Fatal(err)
188+
}
189+
190+
// Verify both are populated
191+
if stats.CpuStats.ThrottlingData.Periods != 100 {
192+
t.Errorf("expected Periods 100, got %d", stats.CpuStats.ThrottlingData.Periods)
193+
}
194+
if stats.PidsStats.Current != 42 {
195+
t.Errorf("expected Current 42, got %d", stats.PidsStats.Current)
196+
}
197+
if stats.PidsStats.Limit != 1000 {
198+
t.Errorf("expected Limit 1000, got %d", stats.PidsStats.Limit)
199+
}
200+
}
201+
202+
// TestAddStatsWithEmptyPaths tests that Add*Stats methods work correctly
203+
// when the corresponding controller paths are empty (controller not available).
204+
func TestAddStatsWithEmptyPaths(t *testing.T) {
205+
m := &Manager{
206+
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
207+
paths: make(map[string]string),
208+
}
209+
210+
stats := cgroups.NewStats()
211+
212+
// All Add*Stats methods should succeed with empty paths (no-op)
213+
if err := m.AddCpuStats(stats); err != nil {
214+
t.Errorf("AddCpuStats failed with empty paths: %v", err)
215+
}
216+
if err := m.AddMemoryStats(stats); err != nil {
217+
t.Errorf("AddMemoryStats failed with empty paths: %v", err)
218+
}
219+
if err := m.AddPidsStats(stats); err != nil {
220+
t.Errorf("AddPidsStats failed with empty paths: %v", err)
221+
}
222+
if err := m.AddIoStats(stats); err != nil {
223+
t.Errorf("AddIoStats failed with empty paths: %v", err)
224+
}
225+
if err := m.AddHugetlbStats(stats); err != nil {
226+
t.Errorf("AddHugetlbStats failed with empty paths: %v", err)
227+
}
228+
if err := m.AddRdmaStats(stats); err != nil {
229+
t.Errorf("AddRdmaStats failed with empty paths: %v", err)
230+
}
231+
if err := m.AddMiscStats(stats); err != nil {
232+
t.Errorf("AddMiscStats failed with empty paths: %v", err)
233+
}
234+
}

fs2/fs2.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,73 @@ func (m *Manager) GetStats() (*cgroups.Stats, error) {
155155
return st, nil
156156
}
157157

158+
func (m *Manager) AddCpuStats(stats *cgroups.Stats) error {
159+
if err := statCpu(m.dirPath, stats); err != nil && !os.IsNotExist(err) {
160+
return err
161+
}
162+
163+
var err error
164+
if stats.CpuStats.PSI, err = statPSI(m.dirPath, "cpu.pressure"); err != nil {
165+
return err
166+
}
167+
168+
return nil
169+
}
170+
171+
func (m *Manager) AddMemoryStats(stats *cgroups.Stats) error {
172+
if err := statMemory(m.dirPath, stats); err != nil && !os.IsNotExist(err) {
173+
return err
174+
}
175+
176+
var err error
177+
if stats.MemoryStats.PSI, err = statPSI(m.dirPath, "memory.pressure"); err != nil {
178+
return err
179+
}
180+
181+
return nil
182+
}
183+
184+
func (m *Manager) AddPidsStats(stats *cgroups.Stats) error {
185+
return statPids(m.dirPath, stats)
186+
}
187+
188+
func (m *Manager) AddIoStats(stats *cgroups.Stats) error {
189+
if err := statIo(m.dirPath, stats); err != nil && !os.IsNotExist(err) {
190+
return err
191+
}
192+
193+
var err error
194+
if stats.BlkioStats.PSI, err = statPSI(m.dirPath, "io.pressure"); err != nil {
195+
return err
196+
}
197+
198+
return nil
199+
}
200+
201+
func (m *Manager) AddHugetlbStats(stats *cgroups.Stats) error {
202+
err := statHugeTlb(m.dirPath, stats)
203+
if err != nil && !os.IsNotExist(err) {
204+
return err
205+
}
206+
return nil
207+
}
208+
209+
func (m *Manager) AddRdmaStats(stats *cgroups.Stats) error {
210+
err := fscommon.RdmaGetStats(m.dirPath, stats)
211+
if err != nil && !os.IsNotExist(err) {
212+
return err
213+
}
214+
return nil
215+
}
216+
217+
func (m *Manager) AddMiscStats(stats *cgroups.Stats) error {
218+
err := statMisc(m.dirPath, stats)
219+
if err != nil && !os.IsNotExist(err) {
220+
return err
221+
}
222+
return nil
223+
}
224+
158225
func (m *Manager) Freeze(state cgroups.FreezerState) error {
159226
if m.config.Resources == nil {
160227
return errors.New("cannot toggle freezer: cgroups not configured for container")

0 commit comments

Comments
 (0)