Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cgroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ type Manager interface {
// GetStats returns cgroups statistics.
GetStats() (*Stats, error)

// Stats returns statistics for specified controllers.
// If opts is nil or opts.Controllers is 0, all controllers are queried.
Stats(opts *StatsOptions) (*Stats, error)

// Freeze sets the freezer cgroup to the specified state.
Freeze(state FreezerState) error

Expand Down
5 changes: 5 additions & 0 deletions fs/blkio.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func (s *BlkioGroup) Name() string {
return "blkio"
}

// ID returns the controller ID for blkio subsystem.
func (s *BlkioGroup) ID() cgroups.Controller {
return cgroups.IO
}

func (s *BlkioGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
return apply(path, pid)
}
Expand Down
5 changes: 5 additions & 0 deletions fs/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ func (s *CpuGroup) Name() string {
return "cpu"
}

// ID returns the controller ID for CPU subsystem.
func (s *CpuGroup) ID() cgroups.Controller {
return cgroups.CPU
}

func (s *CpuGroup) Apply(path string, r *cgroups.Resources, pid int) error {
if err := os.MkdirAll(path, 0o755); err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions fs/cpuacct.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ func (s *CpuacctGroup) Name() string {
return "cpuacct"
}

// ID returns the controller ID for cpuacct subsystem.
func (s *CpuacctGroup) ID() cgroups.Controller {
return cgroups.CPU
}

func (s *CpuacctGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
return apply(path, pid)
}
Expand Down
5 changes: 5 additions & 0 deletions fs/cpuset.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func (s *CpusetGroup) Name() string {
return "cpuset"
}

// ID returns the controller ID for cpuset subsystem.
func (s *CpusetGroup) ID() cgroups.Controller {
return cgroups.CPUSet
}

func (s *CpusetGroup) Apply(path string, r *cgroups.Resources, pid int) error {
return s.ApplyDir(path, r, pid)
}
Expand Down
6 changes: 6 additions & 0 deletions fs/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ func (s *DevicesGroup) Name() string {
return "devices"
}

// ID returns the controller ID for devices subsystem.
// Returns 0 as devices is not a cgroups.Controller.
func (s *DevicesGroup) ID() cgroups.Controller {
return 0
}

func (s *DevicesGroup) Apply(path string, r *cgroups.Resources, pid int) error {
if r.SkipDevices {
return nil
Expand Down
6 changes: 6 additions & 0 deletions fs/freezer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ func (s *FreezerGroup) Name() string {
return "freezer"
}

// ID returns the controller ID for freezer subsystem.
// Returns 0 as freezer is not a cgroups.Controller.
func (s *FreezerGroup) ID() cgroups.Controller {
return 0
}

func (s *FreezerGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
return apply(path, pid)
}
Expand Down
23 changes: 22 additions & 1 deletion fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var subsystems = []subsystem{
&FreezerGroup{},
&RdmaGroup{},
&NameGroup{GroupName: "name=systemd", Join: true},
&NameGroup{GroupName: "misc", Join: true},
&NameGroup{GroupName: "misc", Join: true, GroupID: cgroups.Misc},
}

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

func (m *Manager) GetStats() (*cgroups.Stats, error) {
return m.Stats(nil)
}

// Stats returns cgroup statistics for the specified controllers.
// If opts is nil or opts.Controllers is zero, statistics for all controllers are returned.
func (m *Manager) Stats(opts *cgroups.StatsOptions) (*cgroups.Stats, error) {
m.mu.Lock()
defer m.mu.Unlock()

// Default: query all controllers
controllers := cgroups.AllControllers
if opts != nil && opts.Controllers != 0 {
controllers = opts.Controllers
}

stats := cgroups.NewStats()
for _, sys := range subsystems {
path := m.paths[sys.Name()]
if path == "" {
continue
}

// Filter based on controller type
if sys.ID()&controllers == 0 {
continue
}

if err := sys.GetStats(path, stats); err != nil {
return nil, err
}
Expand Down
209 changes: 209 additions & 0 deletions fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,215 @@ import (
"github.com/opencontainers/cgroups"
)

// pointerTo returns a pointer to the given controller value.
func pointerTo(c cgroups.Controller) *cgroups.Controller {
return &c
}

func TestStats(t *testing.T) {
testCases := []struct {
name string
controller *cgroups.Controller
subsystems map[string]map[string]string // subsystem -> file contents
validate func(*testing.T, *cgroups.Stats)
}{
{
name: "CPU stats",
controller: pointerTo(cgroups.CPU),
subsystems: map[string]map[string]string{
"cpu": {
"cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n",
},
"cpuacct": {
"cpuacct.usage": cpuAcctUsageContents,
"cpuacct.usage_percpu": cpuAcctUsagePerCPUContents,
"cpuacct.stat": cpuAcctStatContents,
},
},
validate: func(t *testing.T, stats *cgroups.Stats) {
// 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)
}
},
},
{
name: "Memory stats",
controller: pointerTo(cgroups.Memory),
subsystems: map[string]map[string]string{
"memory": {
"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",
},
},
validate: func(t *testing.T, stats *cgroups.Stats) {
expected := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}
expectMemoryDataEquals(t, expected, stats.MemoryStats.Usage)
},
},
{
name: "Pids stats",
controller: pointerTo(cgroups.Pids),
subsystems: map[string]map[string]string{
"pids": {
"pids.current": "1337",
"pids.max": "1024",
},
},
validate: func(t *testing.T, stats *cgroups.Stats) {
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)
}
},
},
{
name: "IO stats",
controller: pointerTo(cgroups.IO),
subsystems: map[string]map[string]string{
"blkio": blkioBFQStatsTestFiles,
},
validate: func(t *testing.T, stats *cgroups.Stats) {
// 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")
}
},
},
{
name: "Multiple controllers - CPU+Pids",
controller: pointerTo(cgroups.CPU | cgroups.Pids),
subsystems: map[string]map[string]string{
"cpu": {
"cpu.stat": "nr_periods 100\nnr_throttled 10\nthrottled_time 5000\n",
},
"pids": {
"pids.current": "42",
"pids.max": "1000",
},
},
validate: func(t *testing.T, stats *cgroups.Stats) {
// 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)
}
},
},
{
name: "All controllers with nil options",
controller: nil, // nil means all controllers (default behavior)
subsystems: map[string]map[string]string{
"cpu": {
"cpu.stat": "nr_periods 2000\nnr_throttled 200\nthrottled_time 18446744073709551615\n",
},
"cpuacct": {
"cpuacct.usage": cpuAcctUsageContents,
"cpuacct.usage_percpu": cpuAcctUsagePerCPUContents,
"cpuacct.stat": cpuAcctStatContents,
},
"memory": {
"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",
},
"pids": {
"pids.current": "1337",
"pids.max": "1024",
},
"blkio": blkioBFQStatsTestFiles,
},
validate: func(t *testing.T, stats *cgroups.Stats) {
// Verify CPU stats
expectedThrottling := cgroups.ThrottlingData{
Periods: 2000,
ThrottledPeriods: 200,
ThrottledTime: 18446744073709551615,
}
expectThrottlingDataEquals(t, expectedThrottling, stats.CpuStats.ThrottlingData)
if stats.CpuStats.CpuUsage.TotalUsage != 12262454190222160 {
t.Errorf("expected TotalUsage 12262454190222160, got %d", stats.CpuStats.CpuUsage.TotalUsage)
}

// Verify Memory stats
expectedMemory := cgroups.MemoryData{Usage: 2048, MaxUsage: 4096, Failcnt: 100, Limit: 8192}
expectMemoryDataEquals(t, expectedMemory, stats.MemoryStats.Usage)

// Verify Pids stats
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)
}

// Verify IO stats
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")
}
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create temp directories for each subsystem and write files
paths := make(map[string]string)
for subsystem, files := range tc.subsystems {
path := tempDir(t, subsystem)
writeFileContents(t, path, files)
paths[subsystem] = path
}
m := &Manager{
cgroups: &cgroups.Cgroup{Resources: &cgroups.Resources{}},
paths: paths,
}

var stats *cgroups.Stats
var err error
if tc.controller != nil {
stats, err = m.Stats(&cgroups.StatsOptions{Controllers: *tc.controller})
} else {
stats, err = m.Stats(nil)
}
if err != nil {
t.Fatal(err)
}

// Validate the results
tc.validate(t, stats)
})
}
}

func BenchmarkGetStats(b *testing.B) {
if cgroups.IsCgroup2UnifiedMode() {
b.Skip("cgroup v2 is not supported")
Expand Down
5 changes: 5 additions & 0 deletions fs/hugetlb.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ func (s *HugetlbGroup) Name() string {
return "hugetlb"
}

// ID returns the controller ID for hugetlb subsystem.
func (s *HugetlbGroup) ID() cgroups.Controller {
return cgroups.HugeTLB
}

func (s *HugetlbGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
return apply(path, pid)
}
Expand Down
5 changes: 5 additions & 0 deletions fs/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func (s *MemoryGroup) Name() string {
return "memory"
}

// ID returns the controller ID for memory subsystem.
func (s *MemoryGroup) ID() cgroups.Controller {
return cgroups.Memory
}

func (s *MemoryGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
return apply(path, pid)
}
Expand Down
6 changes: 6 additions & 0 deletions fs/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import (
type NameGroup struct {
GroupName string
Join bool
GroupID cgroups.Controller
}

func (s *NameGroup) Name() string {
return s.GroupName
}

// ID returns the controller ID for named subsystem.
func (s *NameGroup) ID() cgroups.Controller {
return s.GroupID
}

func (s *NameGroup) Apply(path string, _ *cgroups.Resources, pid int) error {
if s.Join {
// Ignore errors if the named cgroup does not exist.
Expand Down
Loading
Loading