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
44 changes: 44 additions & 0 deletions cgroupv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,33 @@ func getFromParentOrFallback[T any](
return fallback()
}

// cpuUsed reads usage from the cgroup that owns the effective cpu.max so
// that used and total describe the same domain. cpu.stat includes all
// descendants, so this also counts sibling cgroups under the limit owner
// (for example /docker/<id> next to /init.scope in docker-in-docker).
func (s cgroupV2Statter) cpuUsed() (used float64, err error) {
owner, err := s.cpuQuotaOwner()
if err != nil {
return 0, xerrors.Errorf("get cpu quota owner: %w", err)
}

if owner != nil && owner.path != s.path {
if used, err := owner.cpuUsedOwn(); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return 0, xerrors.Errorf("read limit owner cpu: %w", err)
}
// The limit owner does not expose cpu.stat, so fall back to
// the process's own cgroup below.
} else {
return used, nil
}
}

return s.cpuUsedOwn()
}

// cpuUsedOwn reads usage from this cgroup's cpu.stat.
func (s cgroupV2Statter) cpuUsedOwn() (used float64, err error) {
cpuStatPath := filepath.Join(s.path, cgroupV2CPUStat)

usageUs, err := readInt64Prefix(s.fs, cpuStatPath, "usage_usec")
Expand Down Expand Up @@ -122,6 +148,24 @@ func (s cgroupV2Statter) cpuQuota() (float64, error) {
return float64(quotaUs), nil
}

// cpuQuotaOwner returns the nearest cgroup, starting at s and walking up,
// whose cpu.max sets a quota. It returns nil when no cgroup sets one.
func (s *cgroupV2Statter) cpuQuotaOwner() (*cgroupV2Statter, error) {
cpuMaxPath := filepath.Join(s.path, cgroupV2CPUMax)

if _, err := readInt64SepIdx(s.fs, cpuMaxPath, " ", 0); err != nil {
if !errors.Is(err, strconv.ErrSyntax) && !errors.Is(err, fs.ErrNotExist) {
return nil, xerrors.Errorf("get cpu quota: %w", err)
}

return getFromParentOrFallback(s, (*cgroupV2Statter).cpuQuotaOwner, func() (*cgroupV2Statter, error) {
return nil, nil
})
}

return s, nil
}

func (s cgroupV2Statter) cpuPeriod() (float64, error) {
cpuMaxPath := filepath.Join(s.path, cgroupV2CPUMax)

Expand Down
44 changes: 44 additions & 0 deletions stat_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,30 @@ func TestStatter(t *testing.T) {
assert.Equal(t, "cores", cpu.Unit)
})

t.Run("CPU/UsageFromLimitOwner", func(t *testing.T) {
t.Parallel()

fs := initFS(t, fsContainerCgroupV2DockerInDockerCPU)
fakeWait := func(time.Duration) {
// The nested container in /docker/<id> uses 4 cores'
// worth of CPU time, which is rolled up into the root.
mungeFS(t, fs, filepath.Join(cgroupRootPath, cgroupV2CPUStat), "usage_usec 401000")
mungeFS(t, fs, filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat), "usage_usec 1000")
mungeFS(t, fs, filepath.Join(cgroupRootPath, cgroupV2Path, cgroupV2CPUStat), "usage_usec 400000")
}
s, err := New(WithFS(fs), withWait(fakeWait), withIsCgroupV2(true))
require.NoError(t, err)

cpu, err := s.ContainerCPU()
require.NoError(t, err)

require.NotNil(t, cpu)
assert.Equal(t, 4.01, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 12.0, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})

t.Run("CPU/InitScopeDefaultPeriod", func(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -835,6 +859,26 @@ sysboxfs /proc/sys sysboxfs rw,nosuid,nodev,noexec,relatime 0 0`,
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryStat): "inactive_file 268435456",
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryUsageBytes): "536870912",
}
// fsContainerCgroupV2DockerInDockerCPU simulates a docker-in-docker workspace.
// The agent runs in /init.scope and a nested container runs in the
// sibling /docker/<id>. Only the container root sets cpu.max, so usage
// must be read from the root to include both children.
fsContainerCgroupV2DockerInDockerCPU = map[string]string{
procOneCgroup: "0::/init.scope",
procSelfCgroup: "0::/init.scope",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
sysCgroupType: "domain",

filepath.Join(cgroupRootPath, cgroupV2CPUMax): "1200000 100000",
filepath.Join(cgroupRootPath, cgroupV2CPUStat): "usage_usec 0",

filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUMax): "max 100000",
filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat): "usage_usec 0",

filepath.Join(cgroupRootPath, cgroupV2Path, cgroupV2CPUMax): "max 100000",
filepath.Join(cgroupRootPath, cgroupV2Path, cgroupV2CPUStat): "usage_usec 0",
}
// Variant where child has memory.stat but NOT memory.current (should inherit from root)
fsContainerCgroupV2KubernetesMissingMemoryCurrent = map[string]string{
procOneCgroup: "0::/",
Expand Down
Loading