diff --git a/cgroups.go b/cgroups.go index 5a97bd3..4d4b29b 100644 --- a/cgroups.go +++ b/cgroups.go @@ -44,6 +44,27 @@ type Manager interface { // GetStats returns cgroups statistics. GetStats() (*Stats, error) + // AddCpuStats adds cpu statistics to the provided stats object. + AddCpuStats(stats *Stats) error + + // AddMemoryStats adds memory statistics to the provided stats object. + AddMemoryStats(stats *Stats) error + + // AddPidsStats adds pids statistics to the provided stats object. + AddPidsStats(stats *Stats) error + + // AddIoStats adds io statistics to the provided stats object. + AddIoStats(stats *Stats) error + + // AddHugetlbStats adds hugetlb statistics to the provided stats object. + AddHugetlbStats(stats *Stats) error + + // AddRdmaStats adds rdma statistics to the provided stats object. + AddRdmaStats(stats *Stats) error + + // AddMiscStats adds misc statistics to the provided stats object. + AddMiscStats(stats *Stats) error + // Freeze sets the freezer cgroup to the specified state. Freeze(state FreezerState) error diff --git a/fs/fs.go b/fs/fs.go index 6259311..2b872f5 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -196,6 +196,104 @@ func (m *Manager) GetStats() (*cgroups.Stats, error) { return stats, nil } +func (m *Manager) AddCpuStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + cpuGroup := &CpuGroup{} + if path := m.paths["cpu"]; path != "" { + if err := cpuGroup.GetStats(path, stats); err != nil { + return err + } + } + + cpuacctGroup := &CpuacctGroup{} + if path := m.paths["cpuacct"]; path != "" { + if err := cpuacctGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddMemoryStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + memoryGroup := &MemoryGroup{} + if path := m.paths["memory"]; path != "" { + if err := memoryGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddPidsStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + pidsGroup := &PidsGroup{} + if path := m.paths["pids"]; path != "" { + if err := pidsGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddIoStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + blkioGroup := &BlkioGroup{} + if path := m.paths["blkio"]; path != "" { + if err := blkioGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddHugetlbStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + hugetlbGroup := &HugetlbGroup{} + if path := m.paths["hugetlb"]; path != "" { + if err := hugetlbGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddRdmaStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + rdmaGroup := &RdmaGroup{} + if path := m.paths["rdma"]; path != "" { + if err := rdmaGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *Manager) AddMiscStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + return nil +} + func (m *Manager) Set(r *cgroups.Resources) error { if r == nil { return nil diff --git a/fs/fs_test.go b/fs/fs_test.go index f9a0935..4b2f8ca 100644 --- a/fs/fs_test.go +++ b/fs/fs_test.go @@ -47,3 +47,188 @@ func BenchmarkGetStats(b *testing.B) { b.Fatalf("stats: %+v", st) } } + +func TestAddCpuStats(t *testing.T) { + cpuPath := tempDir(t, "cpu") + cpuacctPath := tempDir(t, "cpuacct") + + writeFileContents(t, cpuPath, map[string]string{ + "cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n", + }) + writeFileContents(t, cpuacctPath, map[string]string{ + "cpuacct.usage": cpuAcctUsageContents, + "cpuacct.usage_percpu": cpuAcctUsagePerCPUContents, + "cpuacct.stat": cpuAcctStatContents, + }) + + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: map[string]string{"cpu": cpuPath, "cpuacct": cpuacctPath}, + } + + stats := cgroups.NewStats() + if err := m.AddCpuStats(stats); err != nil { + t.Fatal(err) + } + + // Verify throttling data from cpu.stat + expectedThrottling := cgroups.ThrottlingData{ + Periods: 2000, + ThrottledPeriods: 200, + ThrottledTime: 18446744073709551615, + } + expectThrottlingDataEquals(t, expectedThrottling, stats.CpuStats.ThrottlingData) + + // Verify total usage from cpuacct.usage + if stats.CpuStats.CpuUsage.TotalUsage != 12262454190222160 { + t.Errorf("expected TotalUsage 12262454190222160, got %d", stats.CpuStats.CpuUsage.TotalUsage) + } +} + +func TestAddPidsStats(t *testing.T) { + path := tempDir(t, "pids") + writeFileContents(t, path, map[string]string{ + "pids.current": "1337", + "pids.max": "1024", + }) + + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: map[string]string{"pids": path}, + } + + stats := cgroups.NewStats() + if err := m.AddPidsStats(stats); err != nil { + t.Fatal(err) + } + + if stats.PidsStats.Current != 1337 { + t.Errorf("expected Current 1337, got %d", stats.PidsStats.Current) + } + if stats.PidsStats.Limit != 1024 { + t.Errorf("expected Limit 1024, got %d", stats.PidsStats.Limit) + } +} + +func TestAddMemoryStats(t *testing.T) { + path := tempDir(t, "memory") + writeFileContents(t, path, map[string]string{ + "memory.stat": memoryStatContents, + "memory.usage_in_bytes": "2048", + "memory.max_usage_in_bytes": "4096", + "memory.failcnt": "100", + "memory.limit_in_bytes": "8192", + "memory.use_hierarchy": "1", + }) + + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: map[string]string{"memory": path}, + } + + stats := cgroups.NewStats() + if err := m.AddMemoryStats(stats); err != nil { + t.Fatal(err) + } + + expected := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192} + expectMemoryDataEquals(t, expected, stats.MemoryStats.Usage) +} + +func TestAddIoStats(t *testing.T) { + path := tempDir(t, "blkio") + // Use blkioBFQStatsTestFiles from blkio_test.go for proper file format + writeFileContents(t, path, blkioBFQStatsTestFiles) + + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: map[string]string{"blkio": path}, + } + + stats := cgroups.NewStats() + if err := m.AddIoStats(stats); err != nil { + t.Fatal(err) + } + + // Verify we have entries + if len(stats.BlkioStats.IoServiceBytesRecursive) == 0 { + t.Error("expected IoServiceBytesRecursive to have entries") + } + if len(stats.BlkioStats.IoServicedRecursive) == 0 { + t.Error("expected IoServicedRecursive to have entries") + } +} + +func TestAddStatsIterative(t *testing.T) { + // Set up both cpu and pids directories + cpuPath := tempDir(t, "cpu") + pidsPath := tempDir(t, "pids") + + writeFileContents(t, cpuPath, map[string]string{ + "cpu.stat": "nr_periods 100\nnr_throttled 10\nthrottled_time 5000\n", + }) + writeFileContents(t, pidsPath, map[string]string{ + "pids.current": "42", + "pids.max": "1000", + }) + + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: map[string]string{"cpu": cpuPath, "pids": pidsPath}, + } + + stats := cgroups.NewStats() + + // Call both methods on same stats object + if err := m.AddCpuStats(stats); err != nil { + t.Fatal(err) + } + if err := m.AddPidsStats(stats); err != nil { + t.Fatal(err) + } + + // Verify both are populated + if stats.CpuStats.ThrottlingData.Periods != 100 { + t.Errorf("expected Periods 100, got %d", stats.CpuStats.ThrottlingData.Periods) + } + if stats.PidsStats.Current != 42 { + t.Errorf("expected Current 42, got %d", stats.PidsStats.Current) + } + if stats.PidsStats.Limit != 1000 { + t.Errorf("expected Limit 1000, got %d", stats.PidsStats.Limit) + } +} + +// TestAddStatsWithEmptyPaths tests that Add*Stats methods work correctly +// when the corresponding controller paths are empty (controller not available). +func TestAddStatsWithEmptyPaths(t *testing.T) { + m := &Manager{ + cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}}, + paths: make(map[string]string), + } + + stats := cgroups.NewStats() + + // All Add*Stats methods should succeed with empty paths (no-op) + if err := m.AddCpuStats(stats); err != nil { + t.Errorf("AddCpuStats failed with empty paths: %v", err) + } + if err := m.AddMemoryStats(stats); err != nil { + t.Errorf("AddMemoryStats failed with empty paths: %v", err) + } + if err := m.AddPidsStats(stats); err != nil { + t.Errorf("AddPidsStats failed with empty paths: %v", err) + } + if err := m.AddIoStats(stats); err != nil { + t.Errorf("AddIoStats failed with empty paths: %v", err) + } + if err := m.AddHugetlbStats(stats); err != nil { + t.Errorf("AddHugetlbStats failed with empty paths: %v", err) + } + if err := m.AddRdmaStats(stats); err != nil { + t.Errorf("AddRdmaStats failed with empty paths: %v", err) + } + if err := m.AddMiscStats(stats); err != nil { + t.Errorf("AddMiscStats failed with empty paths: %v", err) + } +} diff --git a/fs2/fs2.go b/fs2/fs2.go index 356d087..533340b 100644 --- a/fs2/fs2.go +++ b/fs2/fs2.go @@ -155,6 +155,73 @@ func (m *Manager) GetStats() (*cgroups.Stats, error) { return st, nil } +func (m *Manager) AddCpuStats(stats *cgroups.Stats) error { + if err := statCpu(m.dirPath, stats); err != nil && !os.IsNotExist(err) { + return err + } + + var err error + if stats.CpuStats.PSI, err = statPSI(m.dirPath, "cpu.pressure"); err != nil { + return err + } + + return nil +} + +func (m *Manager) AddMemoryStats(stats *cgroups.Stats) error { + if err := statMemory(m.dirPath, stats); err != nil && !os.IsNotExist(err) { + return err + } + + var err error + if stats.MemoryStats.PSI, err = statPSI(m.dirPath, "memory.pressure"); err != nil { + return err + } + + return nil +} + +func (m *Manager) AddPidsStats(stats *cgroups.Stats) error { + return statPids(m.dirPath, stats) +} + +func (m *Manager) AddIoStats(stats *cgroups.Stats) error { + if err := statIo(m.dirPath, stats); err != nil && !os.IsNotExist(err) { + return err + } + + var err error + if stats.BlkioStats.PSI, err = statPSI(m.dirPath, "io.pressure"); err != nil { + return err + } + + return nil +} + +func (m *Manager) AddHugetlbStats(stats *cgroups.Stats) error { + err := statHugeTlb(m.dirPath, stats) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func (m *Manager) AddRdmaStats(stats *cgroups.Stats) error { + err := fscommon.RdmaGetStats(m.dirPath, stats) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func (m *Manager) AddMiscStats(stats *cgroups.Stats) error { + err := statMisc(m.dirPath, stats) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + func (m *Manager) Freeze(state cgroups.FreezerState) error { if m.config.Resources == nil { return errors.New("cannot toggle freezer: cgroups not configured for container") diff --git a/fs2/fs2_test.go b/fs2/fs2_test.go new file mode 100644 index 0000000..f6513b6 --- /dev/null +++ b/fs2/fs2_test.go @@ -0,0 +1,407 @@ +package fs2 + +import ( + "os" + "path/filepath" + "testing" + + "github.com/opencontainers/cgroups" +) + +const exampleCpuStatData = `usage_usec 1000000 +user_usec 600000 +system_usec 400000 +nr_periods 100 +nr_throttled 10 +throttled_usec 50000 +nr_bursts 5 +burst_usec 10000` + +const exampleCpuStatDataShort = `usage_usec 1000000 +user_usec 600000 +system_usec 400000` + +const exampleMemoryCurrent = "4194304" +const exampleMemoryMax = "max" + +const examplePSIData = `some avg10=1.00 avg60=2.00 avg300=3.00 total=100000 +full avg10=0.50 avg60=1.00 avg300=1.50 total=50000` + +const exampleRdmaCurrent = `mlx5_0 hca_handle=10 hca_object=20` + + +func TestAddCpuStats(t *testing.T) { + // We're using a fake cgroupfs. + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "cpu.stat"), []byte(exampleCpuStatData), 0o644); err != nil { + t.Fatal(err) + } + + // Create manager + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + // Create stats and call AddCpuStats + stats := cgroups.NewStats() + if err := m.AddCpuStats(stats); err != nil { + t.Fatal(err) + } + + // Verify CPU stats populated correctly (values are converted from usec to nsec) + if stats.CpuStats.CpuUsage.TotalUsage != 1000000000 { + t.Errorf("expected TotalUsage 1000000000, got %d", stats.CpuStats.CpuUsage.TotalUsage) + } + if stats.CpuStats.CpuUsage.UsageInUsermode != 600000000 { + t.Errorf("expected UsageInUsermode 600000000, got %d", stats.CpuStats.CpuUsage.UsageInUsermode) + } + if stats.CpuStats.CpuUsage.UsageInKernelmode != 400000000 { + t.Errorf("expected UsageInKernelmode 400000000, got %d", stats.CpuStats.CpuUsage.UsageInKernelmode) + } + if stats.CpuStats.ThrottlingData.Periods != 100 { + t.Errorf("expected Periods 100, got %d", stats.CpuStats.ThrottlingData.Periods) + } + if stats.CpuStats.ThrottlingData.ThrottledPeriods != 10 { + t.Errorf("expected ThrottledPeriods 10, got %d", stats.CpuStats.ThrottlingData.ThrottledPeriods) + } +} + +func TestAddMemoryStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + // Use exampleMemoryStatData from memory_test.go (file = 6502666240) + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.stat"), []byte(exampleMemoryStatData), 0o644); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.current"), []byte(exampleMemoryCurrent), 0o644); err != nil { + t.Fatal(err) + } + + // memory.max is required by getMemoryDataV2 + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.max"), []byte(exampleMemoryMax), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddMemoryStats(stats); err != nil { + t.Fatal(err) + } + + // Verify memory stats + if stats.MemoryStats.Usage.Usage != 4194304 { + t.Errorf("expected Usage 4194304, got %d", stats.MemoryStats.Usage.Usage) + } + // Cache comes from "file" field in memory.stat (6502666240 from exampleMemoryStatData) + if stats.MemoryStats.Cache != 6502666240 { + t.Errorf("expected Cache 6502666240, got %d", stats.MemoryStats.Cache) + } +} + +func TestAddPidsStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "pids.current"), []byte("42\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "pids.max"), []byte("1000\n"), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddPidsStats(stats); err != nil { + t.Fatal(err) + } + + if stats.PidsStats.Current != 42 { + t.Errorf("expected Current 42, got %d", stats.PidsStats.Current) + } + if stats.PidsStats.Limit != 1000 { + t.Errorf("expected Limit 1000, got %d", stats.PidsStats.Limit) + } +} + +func TestAddIoStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + // Use exampleIoStatData from io_test.go + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "io.stat"), []byte(exampleIoStatData), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddIoStats(stats); err != nil { + t.Fatal(err) + } + + // Verify IO stats - check that we have entries + if len(stats.BlkioStats.IoServiceBytesRecursive) == 0 { + t.Error("expected IoServiceBytesRecursive to have entries") + } + if len(stats.BlkioStats.IoServicedRecursive) == 0 { + t.Error("expected IoServicedRecursive to have entries") + } +} + +func TestAddMiscStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + // Use exampleMiscCurrentData and exampleMiscEventsData from misc_test.go + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "misc.current"), []byte(exampleMiscCurrentData), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "misc.events"), []byte(exampleMiscEventsData), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddMiscStats(stats); err != nil { + t.Fatal(err) + } + + // Verify misc stats - exampleMiscCurrentData has res_a, res_b, res_c + if _, ok := stats.MiscStats["res_a"]; !ok { + t.Error("expected MiscStats to have 'res_a' entry") + } + if _, ok := stats.MiscStats["res_b"]; !ok { + t.Error("expected MiscStats to have 'res_b' entry") + } + if _, ok := stats.MiscStats["res_c"]; !ok { + t.Error("expected MiscStats to have 'res_c' entry") + } +} + +func TestAddStatsIterative(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "cpu.stat"), []byte(exampleCpuStatDataShort), 0o644); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "pids.current"), []byte("42\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "pids.max"), []byte("1000\n"), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + // Test iterative population - call multiple Add*Stats on the same Stats object + stats := cgroups.NewStats() + + if err := m.AddCpuStats(stats); err != nil { + t.Fatal(err) + } + if err := m.AddPidsStats(stats); err != nil { + t.Fatal(err) + } + + // Verify both stats are populated in the same object + if stats.CpuStats.CpuUsage.TotalUsage != 1000000000 { + t.Errorf("expected TotalUsage 1000000000, got %d", stats.CpuStats.CpuUsage.TotalUsage) + } + if stats.PidsStats.Current != 42 { + t.Errorf("expected Current 42, got %d", stats.PidsStats.Current) + } + if stats.PidsStats.Limit != 1000 { + t.Errorf("expected Limit 1000, got %d", stats.PidsStats.Limit) + } +} + +func TestAddCpuStatsWithPSI(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "cpu.stat"), []byte(exampleCpuStatData), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "cpu.pressure"), []byte(examplePSIData), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddCpuStats(stats); err != nil { + t.Fatal(err) + } + + // Verify PSI data is populated + if stats.CpuStats.PSI == nil { + t.Fatal("expected PSI to be populated") + } + if stats.CpuStats.PSI.Some.Avg10 != 1.00 { + t.Errorf("expected PSI.Some.Avg10 1.00, got %f", stats.CpuStats.PSI.Some.Avg10) + } + if stats.CpuStats.PSI.Full.Total != 50000 { + t.Errorf("expected PSI.Full.Total 50000, got %d", stats.CpuStats.PSI.Full.Total) + } +} + +func TestAddMemoryStatsWithPSI(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.stat"), []byte(exampleMemoryStatData), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.current"), []byte(exampleMemoryCurrent), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.max"), []byte(exampleMemoryMax), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "memory.pressure"), []byte(examplePSIData), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddMemoryStats(stats); err != nil { + t.Fatal(err) + } + + // Verify PSI data is populated + if stats.MemoryStats.PSI == nil { + t.Fatal("expected PSI to be populated") + } + if stats.MemoryStats.PSI.Some.Avg60 != 2.00 { + t.Errorf("expected PSI.Some.Avg60 2.00, got %f", stats.MemoryStats.PSI.Some.Avg60) + } +} + +func TestAddIoStatsWithPSI(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "io.stat"), []byte(exampleIoStatData), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "io.pressure"), []byte(examplePSIData), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddIoStats(stats); err != nil { + t.Fatal(err) + } + + // Verify PSI data is populated + if stats.BlkioStats.PSI == nil { + t.Fatal("expected PSI to be populated") + } + if stats.BlkioStats.PSI.Full.Avg300 != 1.50 { + t.Errorf("expected PSI.Full.Avg300 1.50, got %f", stats.BlkioStats.PSI.Full.Avg300) + } +} + +func TestAddRdmaStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "rdma.current"), []byte(exampleRdmaCurrent), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(fakeCgroupDir, "rdma.max"), []byte("mlx5_0 hca_handle=max hca_object=max"), 0o644); err != nil { + t.Fatal(err) + } + + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + if err := m.AddRdmaStats(stats); err != nil { + t.Fatal(err) + } + + // Verify RDMA stats are populated + if len(stats.RdmaStats.RdmaCurrent) == 0 { + t.Error("expected RdmaStats.RdmaCurrent to have entries") + } +} + +func TestAddHugetlbStats(t *testing.T) { + cgroups.TestMode = true + + fakeCgroupDir := t.TempDir() + + // HugePageSizes() returns available page sizes from the system + // We can only test if files don't exist (should not error) + config := &cgroups.Cgroup{} + m, err := NewManager(config, fakeCgroupDir) + if err != nil { + t.Fatal(err) + } + + stats := cgroups.NewStats() + // Should not error even when files don't exist + if err := m.AddHugetlbStats(stats); err != nil { + t.Fatal(err) + } +} diff --git a/systemd/v1.go b/systemd/v1.go index 96e69bb..50890c3 100644 --- a/systemd/v1.go +++ b/systemd/v1.go @@ -355,6 +355,104 @@ func (m *LegacyManager) GetStats() (*cgroups.Stats, error) { return stats, nil } +func (m *LegacyManager) AddCpuStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + cpuGroup := &fs.CpuGroup{} + if path := m.paths["cpu"]; path != "" { + if err := cpuGroup.GetStats(path, stats); err != nil { + return err + } + } + + cpuacctGroup := &fs.CpuacctGroup{} + if path := m.paths["cpuacct"]; path != "" { + if err := cpuacctGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddMemoryStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + memoryGroup := &fs.MemoryGroup{} + if path := m.paths["memory"]; path != "" { + if err := memoryGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddPidsStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + pidsGroup := &fs.PidsGroup{} + if path := m.paths["pids"]; path != "" { + if err := pidsGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddIoStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + blkioGroup := &fs.BlkioGroup{} + if path := m.paths["blkio"]; path != "" { + if err := blkioGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddHugetlbStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + hugetlbGroup := &fs.HugetlbGroup{} + if path := m.paths["hugetlb"]; path != "" { + if err := hugetlbGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddRdmaStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + rdmaGroup := &fs.RdmaGroup{} + if path := m.paths["rdma"]; path != "" { + if err := rdmaGroup.GetStats(path, stats); err != nil { + return err + } + } + + return nil +} + +func (m *LegacyManager) AddMiscStats(stats *cgroups.Stats) error { + m.mu.Lock() + defer m.mu.Unlock() + + return nil +} + func (m *LegacyManager) Set(r *cgroups.Resources) error { if r == nil { return nil diff --git a/systemd/v2.go b/systemd/v2.go index f76c93e..0f0b598 100644 --- a/systemd/v2.go +++ b/systemd/v2.go @@ -497,6 +497,34 @@ func (m *UnifiedManager) GetStats() (*cgroups.Stats, error) { return m.fsMgr.GetStats() } +func (m *UnifiedManager) AddCpuStats(stats *cgroups.Stats) error { + return m.fsMgr.AddCpuStats(stats) +} + +func (m *UnifiedManager) AddMemoryStats(stats *cgroups.Stats) error { + return m.fsMgr.AddMemoryStats(stats) +} + +func (m *UnifiedManager) AddPidsStats(stats *cgroups.Stats) error { + return m.fsMgr.AddPidsStats(stats) +} + +func (m *UnifiedManager) AddIoStats(stats *cgroups.Stats) error { + return m.fsMgr.AddIoStats(stats) +} + +func (m *UnifiedManager) AddHugetlbStats(stats *cgroups.Stats) error { + return m.fsMgr.AddHugetlbStats(stats) +} + +func (m *UnifiedManager) AddRdmaStats(stats *cgroups.Stats) error { + return m.fsMgr.AddRdmaStats(stats) +} + +func (m *UnifiedManager) AddMiscStats(stats *cgroups.Stats) error { + return m.fsMgr.AddMiscStats(stats) +} + func (m *UnifiedManager) Set(r *cgroups.Resources) error { if r == nil { return nil