Skip to content

feat(cli): add coder stat command #8005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 48 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5db9006
add stat command
johnstcn May 29, 2023
d6029b4
cpu working on mac
johnstcn Jun 7, 2023
18f4942
add stat memory
johnstcn Jun 7, 2023
251fdda
support values with no total
johnstcn Jun 7, 2023
4c081dc
move clistats to its own package
johnstcn Jun 8, 2023
2ba7392
fix container detection to work with sysbox containers
johnstcn Jun 8, 2023
0e1c96a
add cross-platform declaration for IsContainerized()
johnstcn Jun 8, 2023
0f9859e
add a sync.Once to IsContainerized()
johnstcn Jun 8, 2023
a220c7f
make uptime minutes
johnstcn Jun 8, 2023
89f7e8d
lint
johnstcn Jun 8, 2023
c51e245
extract nproc to variable
johnstcn Jun 8, 2023
3528c00
add skeleton of cgroup stuff
johnstcn Jun 8, 2023
7108c6e
initial cgroupv2 cpu implementation
johnstcn Jun 8, 2023
4ef5f24
fix disk_windows
johnstcn Jun 8, 2023
f0f7b6a
add tests for clistats
johnstcn Jun 8, 2023
6a878b9
improve testing
johnstcn Jun 9, 2023
be7ba72
remove unnecessary os-specific implementations now that we have abstr…
johnstcn Jun 12, 2023
3643407
remove uptime stat as it is trivial to implement in bash
johnstcn Jun 12, 2023
1c8943e
implement cgroupv1 cpu
johnstcn Jun 12, 2023
95b8d1f
unskip container memory tests
johnstcn Jun 12, 2023
495b5b0
flesh out tests
johnstcn Jun 13, 2023
fa0c4c6
cgroupv1 memory
johnstcn Jun 13, 2023
70ef79b
improve tests to allow testing cpu used
johnstcn Jun 13, 2023
7eeefc1
refactor cpu usage calc
johnstcn Jun 13, 2023
305675f
fix tests
johnstcn Jun 13, 2023
d1bb322
fix off-by-10 error
johnstcn Jun 13, 2023
eb2bcf6
remove --sample-interval and collect CPU stats in parallel
johnstcn Jun 13, 2023
44edcf3
fmt; gen
johnstcn Jun 13, 2023
0f3254a
make default_cols consistent to avoid ci surprises
johnstcn Jun 13, 2023
edd99f4
fix race condition
johnstcn Jun 13, 2023
49b6861
remove UPTIME from test
johnstcn Jun 13, 2023
69b1904
update golden files
johnstcn Jun 13, 2023
7eb526d
add stat subcommands
johnstcn Jun 14, 2023
665bf7f
allow modifying unit prefixes
johnstcn Jun 14, 2023
6b11a5c
update docs and examples
johnstcn Jun 14, 2023
c1467f0
fix NaN issue for HostCPU
johnstcn Jun 14, 2023
789c6de
avoid blocking on err chan
johnstcn Jun 14, 2023
482db10
add percentages
johnstcn Jun 15, 2023
0775082
remove outdated comments
johnstcn Jun 15, 2023
73debf8
handle counter reset
johnstcn Jun 15, 2023
d0c992a
add test for large difference between used and total
johnstcn Jun 15, 2023
ef7460a
auto-scale precision, limiting to 3 digits
johnstcn Jun 15, 2023
bec527f
automatically scale precision, remove --prefix arg
johnstcn Jun 15, 2023
08adba7
make gen
johnstcn Jun 15, 2023
78f76e7
improve cli tests
johnstcn Jun 15, 2023
9a82882
update go.mod
johnstcn Jun 15, 2023
19c8a80
Merge remote-tracking branch 'origin/main' into cj/coder-stat
johnstcn Jun 15, 2023
eab2530
update go.sum
johnstcn Jun 15, 2023
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
Prev Previous commit
Next Next commit
refactor cpu usage calc
  • Loading branch information
johnstcn committed Jun 13, 2023
commit 7eeefc195493ce566157a3c7cbbfa007247adf08
90 changes: 42 additions & 48 deletions cli/clistat/cgroup.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy that you split a lot of this logic into its own package.

Original file line number Diff line number Diff line change
Expand Up @@ -53,33 +53,45 @@ func (s *Statter) ContainerCPU() (*Result, error) {
return nil, nil //nolint: nilnil
}

used1, total, err := s.cgroupCPU()
total, err := s.cGroupCPUTotal()
if err != nil {
return nil, xerrors.Errorf("get total cpu: %w", err)
}

used1, err := s.cGroupCPUUsed()
if err != nil {
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
}
s.wait(s.sampleInterval)

// total is unlikely to change. Use the first value.
used2, _, err := s.cgroupCPU()
used2, err := s.cGroupCPUUsed()
if err != nil {
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
}

r := &Result{
Unit: "cores",
Used: (used2 - used1).Seconds(),
Total: ptr.To(total.Seconds()), // close enough to the truth
Used: (used2 - used1),
Total: ptr.To(total),
}
return r, nil
}

func (s *Statter) cgroupCPU() (used, total time.Duration, err error) {
func (s *Statter) cGroupCPUTotal() (used float64, err error) {
if s.isCGroupV2() {
return s.cGroupV2CPU()
return s.cGroupV2CPUTotal()
}

// Fall back to CGroupv1
return s.cGroupV1CPU()
return s.cGroupV1CPUTotal()
}

func (s *Statter) cGroupCPUUsed() (used float64, err error) {
if s.isCGroupV2() {
return s.cGroupV2CPUUsed()
}

return s.cGroupV1CPUUsed()
}

func (s *Statter) isCGroupV2() bool {
Expand All @@ -88,68 +100,50 @@ func (s *Statter) isCGroupV2() bool {
return err == nil
}

func (s *Statter) cGroupV2CPU() (used, total time.Duration, err error) {
total, err = s.cGroupv2CPUTotal()
if err != nil {
return 0, 0, xerrors.Errorf("get cpu total: %w", err)
}

used, err = s.cGroupv2CPUUsed()
func (s *Statter) cGroupV2CPUUsed() (used float64, err error) {
usageUs, err := readInt64Prefix(s.fs, cgroupV2CPUStat, "usage_usec")
if err != nil {
return 0, 0, xerrors.Errorf("get cpu used: %w", err)
return 0, xerrors.Errorf("get cgroupv2 cpu used: %w", err)
}

return used, total, nil
return (time.Duration(usageUs) * time.Microsecond).Seconds(), nil
}

func (s *Statter) cGroupv2CPUUsed() (used time.Duration, err error) {
iused, err := readInt64Prefix(s.fs, cgroupV2CPUStat, "usage_usec")
func (s *Statter) cGroupV2CPUTotal() (total float64, err error) {
var quotaUs, periodUs int64
periodUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 1)
if err != nil {
return 0, xerrors.Errorf("get cgroupv2 cpu used: %w", err)
return 0, xerrors.Errorf("get cpu period: %w", err)
}
return time.Duration(iused) * time.Microsecond, nil
}

func (s *Statter) cGroupv2CPUTotal() (total time.Duration, err error) {
var quotaUs int64
quotaUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 0)
if err != nil {
// Fall back to number of cores
quotaUs = int64(s.nproc) * time.Second.Microseconds()
quotaUs = int64(s.nproc) * periodUs
}

return time.Duration(quotaUs) * time.Microsecond, nil
return float64(quotaUs) / float64(periodUs), nil
}

func (s *Statter) cGroupV1CPU() (used, total time.Duration, err error) {
total, err = s.cGroupV1CPUTotal()
func (s *Statter) cGroupV1CPUTotal() (float64, error) {
periodUs, err := readInt64(s.fs, cgroupV1CFSPeriodUs)
if err != nil {
return 0, 0, xerrors.Errorf("get cpu total: %w", err)
return 0, xerrors.Errorf("read cpu period: %w", err)
}

used, err = s.cgroupV1CPUUsed()
if err != nil {
return 0, 0, xerrors.Errorf("get cpu used: %w", err)
}

return used, total, nil
}

func (s *Statter) cGroupV1CPUTotal() (time.Duration, error) {
quotaUs, err := readInt64(s.fs, cgroupV1CFSQuotaUs)
if err != nil {
return 0, xerrors.Errorf("read cpu quota: %w", err)
}

if quotaUs < 0 {
// Fall back to the number of cores
quotaUs = int64(s.nproc) * time.Second.Microseconds()
quotaUs = int64(s.nproc) * periodUs
}

return time.Duration(quotaUs) * time.Microsecond, nil
return float64(quotaUs) / float64(periodUs), nil
}

func (s *Statter) cgroupV1CPUUsed() (time.Duration, error) {
func (s *Statter) cGroupV1CPUUsed() (float64, error) {
usageNs, err := readInt64(s.fs, cgroupV1CPUAcctUsage)
if err != nil {
// try alternate path
Expand All @@ -159,7 +153,7 @@ func (s *Statter) cgroupV1CPUUsed() (time.Duration, error) {
}
}

return time.Duration(usageNs), nil
return time.Duration(usageNs).Seconds(), nil
}

// ContainerMemory returns the memory usage of the container cgroup.
Expand All @@ -170,14 +164,14 @@ func (s *Statter) ContainerMemory() (*Result, error) {
}

if s.isCGroupV2() {
return s.cGroupv2Memory()
return s.cGroupV2Memory()
}

// Fall back to CGroupv1
return s.cGroupv1Memory()
return s.cGroupV1Memory()
}

func (s *Statter) cGroupv2Memory() (*Result, error) {
func (s *Statter) cGroupV2Memory() (*Result, error) {
maxUsageBytes, err := readInt64(s.fs, cgroupV2MemoryMaxBytes)
if err != nil {
return nil, xerrors.Errorf("read memory total: %w", err)
Expand All @@ -200,7 +194,7 @@ func (s *Statter) cGroupv2Memory() (*Result, error) {
}, nil
}

func (s *Statter) cGroupv1Memory() (*Result, error) {
func (s *Statter) cGroupV1Memory() (*Result, error) {
maxUsageBytes, err := readInt64(s.fs, cgroupV1MemoryMaxUsageBytes)
if err != nil {
return nil, xerrors.Errorf("read memory total: %w", err)
Expand Down Expand Up @@ -252,7 +246,7 @@ func readInt64SepIdx(fs afero.Fs, path, sep string, idx int) (int64, error) {
return 0, xerrors.Errorf("expected line %q to have at least %d parts", string(data), idx+1)
}

val, err := strconv.ParseInt(parts[idx], 10, 64)
val, err := strconv.ParseInt(strings.TrimSpace(parts[idx]), 10, 64)
if err != nil {
return 0, xerrors.Errorf("parse %s: %w", path, err)
}
Expand Down
47 changes: 30 additions & 17 deletions cli/clistat/stat_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,16 @@ func TestStatter(t *testing.T) {

t.Run("Limit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "1000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)

t.Run("ContainerCPU", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "1000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
Expand All @@ -122,6 +122,13 @@ func TestStatter(t *testing.T) {

t.Run("ContainerMemory", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "1000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
mem, err := s.ContainerMemory()
require.NoError(t, err)
require.NotNil(t, mem)
Expand Down Expand Up @@ -154,26 +161,33 @@ func TestStatter(t *testing.T) {
t.Parallel()
t.Run("Limit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "10000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
t.Run("ContainerCPU", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "10000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Zero(t, cpu.Used)
assert.Equal(t, 1.0, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 2.5, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})

t.Run("ContainerMemory", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "10000000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
mem, err := s.ContainerMemory()
require.NoError(t, err)
require.NotNil(t, mem)
Expand All @@ -196,8 +210,7 @@ func TestStatter(t *testing.T) {
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
// This value does not change in between tests so it is zero.
assert.Zero(t, cpu.Used)
assert.Equal(t, 1.0, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 2.0, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
Expand Down