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
cpu working on mac
  • Loading branch information
johnstcn committed Jun 13, 2023
commit d6029b415cdfbf065d383a78beed970ec30d9df0
118 changes: 48 additions & 70 deletions cli/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@ package cli

import (
"fmt"
"math"
"runtime"
"strconv"
"strings"
"time"

"github.com/elastic/go-sysinfo"
sysinfotypes "github.com/elastic/go-sysinfo/types"

"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
)

func (*RootCmd) stat() *clibase.Cmd {
defaultCols := []string{"host_cpu", "host_memory", "disk"}
if isContainerized() {
// If running in a container, we assume that users want to see these first. Prepend.
defaultCols = append([]string{"container_cpu", "container_memory"}, defaultCols...)
}
var (
sampleInterval time.Duration
formatter = cliui.NewOutputFormatter(
cliui.TextFormat(),
cliui.TableFormat([]statsRow{}, defaultCols),
cliui.JSONFormat(),
)
)
Expand All @@ -35,11 +40,17 @@ func (*RootCmd) stat() *clibase.Cmd {
},
},
Handler: func(inv *clibase.Invocation) error {
stats, err := newStats(sampleInterval)
hi, err := sysinfo.Host()
if err != nil {
return err
}
out, err := formatter.Format(inv.Context(), stats)
sr := statsRow{}
if cs, err := statCPU(hi, sampleInterval); err != nil {
return err
} else {
sr.HostCPU = cs
}
out, err := formatter.Format(inv.Context(), []statsRow{sr})
if err != nil {
return err
}
Expand All @@ -51,96 +62,63 @@ func (*RootCmd) stat() *clibase.Cmd {
return cmd
}

type stats struct {
HostCPU stat `json:"cpu_host"`
HostMemory stat `json:"mem_host"`
Disk stat `json:"disk"`
InContainer bool `json:"in_container,omitempty"`
ContainerCPU stat `json:"cpu_container,omitempty"`
ContainerMemory stat `json:"mem_container,omitempty"`
}

func (s *stats) String() string {
var sb strings.Builder
sb.WriteString(s.HostCPU.String())
sb.WriteString("\n")
sb.WriteString(s.HostMemory.String())
sb.WriteString("\n")
sb.WriteString(s.Disk.String())
sb.WriteString("\n")
if s.InContainer {
sb.WriteString(s.ContainerCPU.String())
sb.WriteString("\n")
sb.WriteString(s.ContainerMemory.String())
sb.WriteString("\n")
}
return sb.String()
}

func newStats(dur time.Duration) (stats, error) {
var s stats
func statCPU(hi sysinfotypes.Host, interval time.Duration) (*stat, error) {
nproc := float64(runtime.NumCPU())
// start := time.Now()
// ticksPerDur := dur / tickInterval
h1, err := sysinfo.Host()
if err != nil {
return s, err
}
<-time.After(dur)
h2, err := sysinfo.Host()
if err != nil {
return s, err
s := &stat{
Unit: "cores",
}
// elapsed := time.Since(start)
// numTicks := elapsed / tickInterval
cts1, err := h1.CPUTime()
c1, err := hi.CPUTime()
if err != nil {
return s, err
return nil, err
}
cts2, err := h2.CPUTime()
<-time.After(interval)
c2, err := hi.CPUTime()
if err != nil {
return s, err
return nil, err
}
// Assuming the total measured should add up to $(nproc) "cores",
// we determine a scaling factor such that scaleFactor * total = nproc.
// We then calculate used as the total time spent idle, and multiply
// that by scaleFactor to give a rough approximation of how busy the
// CPU(s) were.
s.HostCPU.Total = nproc
total := (cts2.Total() - cts1.Total())
idle := (cts2.Idle - cts1.Idle)
s.Total = nproc
total := c2.Total() - c1.Total()
idle := c2.Idle - c1.Idle
used := total - idle
scaleFactor := nproc / total.Seconds()
s.HostCPU.Used = used.Seconds() * scaleFactor
s.HostCPU.Unit = "cores"

s.Used = used.Seconds() * scaleFactor
return s, nil
}

type statsRow struct {
Copy link
Member

Choose a reason for hiding this comment

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

nit: Please consider it to be more like a homework: Network TX, RX :)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see an immediately obvious way to measure this; I'll address in a follow-up PR.

HostCPU *stat `json:"host_cpu" table:"host_cpu,default_sort"`
HostMemory *stat `json:"host_memory" table:"host_memory"`
Disk *stat `json:"disk" table:"disk"`
ContainerCPU *stat `json:"container_cpu" table:"container_cpu"`
ContainerMemory *stat `json:"container_memory" table:"container_memory"`
}

type stat struct {
Used float64 `json:"used"`
Total float64 `json:"total"`
Unit string `json:"unit"`
Used float64 `json:"used"`
}

func (s *stat) String() string {
if s == nil {
return "-"
}
var sb strings.Builder
_, _ = sb.WriteString(strconv.FormatFloat(s.Used, 'f', 1, 64))
_, _ = sb.WriteString("/")
_, _ = sb.WriteString(strconv.FormatFloat(s.Total, 'f', 1, 64))
_, _ = sb.WriteString(" ")
if s.Unit != "" {
_, _ = sb.WriteString(s.Unit)
_, _ = sb.WriteString(" ")
}
_, _ = sb.WriteString("(")
var pct float64
if s.Total == 0 {
pct = math.NaN()
} else {
pct = s.Used / s.Total * 100
}
_, _ = sb.WriteString(strconv.FormatFloat(pct, 'f', 1, 64))
_, _ = sb.WriteString("%)")
return sb.String()
}

func isContainerized() bool {
hi, err := sysinfo.Host()
if err != nil {
// If we can't get the host info, we have other issues.
panic(err)
}
return hi.Info().Containerized != nil && *hi.Info().Containerized
}
6 changes: 3 additions & 3 deletions cli/stat_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ func TestStatString(t *testing.T) {
Stat stat
}{
{
Expected: "1.2/3.4 quatloos (50%)",
Stat: stat{Used: 1.2, Total: 3.4, Unit: "quatloos"},
Expected: "1.2/5.7 quatloos",
Stat: stat{Used: 1.234, Total: 5.678, Unit: "quatloos"},
},
{
Expected: "0/0 HP (NaN%)",
Expected: "0.0/0.0 HP",
Stat: stat{Used: 0, Total: 0, Unit: "HP"},
},
} {
Expand Down
37 changes: 31 additions & 6 deletions cli/stat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cli_test
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -11,8 +13,8 @@ import (
"github.com/coder/coder/testutil"
)

// This just tests that the stat command is recognized and does not output
// an empty string. Actually testing the output of the stat command is
// This just tests that the statRow command is recognized and does not output
// an empty string. Actually testing the output of the stats command is
// fraught with all sorts of fun.
func TestStatCmd(t *testing.T) {
t.Run("JSON", func(t *testing.T) {
Expand All @@ -24,17 +26,40 @@ func TestStatCmd(t *testing.T) {
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.NotEmpty(t, buf.String())
s := buf.String()
require.NotEmpty(t, s)
// Must be valid JSON
tmp := make([]struct{}, 0)
require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&tmp))
})
t.Run("Text", func(t *testing.T) {
t.Run("Table", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
inv, _ := clitest.New(t, "stat", "--output=text")
inv, _ := clitest.New(t, "stat", "--output=table")
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
require.NotEmpty(t, buf.String())
s := buf.String()
require.NotEmpty(t, s)
require.Contains(t, s, "HOST CPU")
require.Contains(t, s, "HOST MEMORY")
require.Contains(t, s, "DISK")
})
t.Run("Default", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
inv, _ := clitest.New(t, "stat", "--output=table")
buf := new(bytes.Buffer)
inv.Stdout = buf
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
s := buf.String()
require.NotEmpty(t, s)
require.Contains(t, s, "HOST CPU")
require.Contains(t, s, "HOST MEMORY")
require.Contains(t, s, "DISK")
})
}