fix(cli): stat: explicitly specify resource SI prefix (#8206)

* fix(cli): move to explicitly specifying units

* make gen
This commit is contained in:
Cian Johnston 2023-06-26 18:06:38 +01:00 committed by GitHub
parent 1558ef52f1
commit 80ef147060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 161 additions and 83 deletions

View File

@ -84,9 +84,10 @@ func (s *Statter) ContainerCPU() (*Result, error) {
}
r := &Result{
Unit: "cores",
Used: used2 - used1,
Total: ptr.To(total),
Unit: "cores",
Used: used2 - used1,
Total: ptr.To(total),
Prefix: PrefixDefault,
}
return r, nil
}
@ -184,20 +185,20 @@ func (s *Statter) cGroupV1CPUUsed() (float64, error) {
// ContainerMemory returns the memory usage of the container cgroup.
// If the system is not containerized, this always returns nil.
func (s *Statter) ContainerMemory() (*Result, error) {
func (s *Statter) ContainerMemory(p Prefix) (*Result, error) {
if ok, err := IsContainerized(s.fs); err != nil || !ok {
return nil, nil //nolint:nilnil
}
if s.isCGroupV2() {
return s.cGroupV2Memory()
return s.cGroupV2Memory(p)
}
// Fall back to CGroupv1
return s.cGroupV1Memory()
return s.cGroupV1Memory(p)
}
func (s *Statter) cGroupV2Memory() (*Result, error) {
func (s *Statter) cGroupV2Memory(p Prefix) (*Result, error) {
maxUsageBytes, err := readInt64(s.fs, cgroupV2MemoryMaxBytes)
if err != nil {
return nil, xerrors.Errorf("read memory total: %w", err)
@ -214,13 +215,14 @@ func (s *Statter) cGroupV2Memory() (*Result, error) {
}
return &Result{
Total: ptr.To(float64(maxUsageBytes)),
Used: float64(currUsageBytes - inactiveFileBytes),
Unit: "B",
Total: ptr.To(float64(maxUsageBytes)),
Used: float64(currUsageBytes - inactiveFileBytes),
Unit: "B",
Prefix: p,
}, nil
}
func (s *Statter) cGroupV1Memory() (*Result, error) {
func (s *Statter) cGroupV1Memory(p Prefix) (*Result, error) {
maxUsageBytes, err := readInt64(s.fs, cgroupV1MemoryMaxUsageBytes)
if err != nil {
return nil, xerrors.Errorf("read memory total: %w", err)
@ -239,9 +241,10 @@ func (s *Statter) cGroupV1Memory() (*Result, error) {
// Total memory used is usage - total_inactive_file
return &Result{
Total: ptr.To(float64(maxUsageBytes)),
Used: float64(usageBytes - totalInactiveFileBytes),
Unit: "B",
Total: ptr.To(float64(maxUsageBytes)),
Used: float64(usageBytes - totalInactiveFileBytes),
Unit: "B",
Prefix: p,
}, nil
}

View File

@ -10,7 +10,7 @@ import (
// Disk returns the disk usage of the given path.
// If path is empty, it returns the usage of the root directory.
func (*Statter) Disk(path string) (*Result, error) {
func (*Statter) Disk(p Prefix, path string) (*Result, error) {
if path == "" {
path = "/"
}
@ -22,5 +22,6 @@ func (*Statter) Disk(path string) (*Result, error) {
r.Total = ptr.To(float64(stat.Blocks * uint64(stat.Bsize)))
r.Used = float64(stat.Blocks-stat.Bfree) * float64(stat.Bsize)
r.Unit = "B"
r.Prefix = p
return &r, nil
}

View File

@ -7,7 +7,7 @@ import (
// Disk returns the disk usage of the given path.
// If path is empty, it defaults to C:\
func (*Statter) Disk(path string) (*Result, error) {
func (*Statter) Disk(p Prefix, path string) (*Result, error) {
if path == "" {
path = `C:\`
}
@ -31,5 +31,6 @@ func (*Statter) Disk(path string) (*Result, error) {
r.Total = ptr.To(float64(totalBytes))
r.Used = float64(totalBytes - freeBytes)
r.Unit = "B"
r.Prefix = p
return &r, nil
}

View File

@ -1,14 +1,12 @@
package clistat
import (
"fmt"
"math"
"runtime"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/elastic/go-sysinfo"
"github.com/spf13/afero"
"golang.org/x/xerrors"
@ -17,15 +15,65 @@ import (
sysinfotypes "github.com/elastic/go-sysinfo/types"
)
// Prefix is a scale multiplier for a result.
// Used when creating a human-readable representation.
type Prefix float64
const (
PrefixDefault = 1.0
PrefixKibi = 1024.0
PrefixMebi = PrefixKibi * 1024.0
PrefixGibi = PrefixMebi * 1024.0
PrefixTebi = PrefixGibi * 1024.0
)
var (
PrefixHumanKibi = "Ki"
PrefixHumanMebi = "Mi"
PrefixHumanGibi = "Gi"
PrefixHumanTebi = "Ti"
)
func (s *Prefix) String() string {
switch *s {
case PrefixKibi:
return "Ki"
case PrefixMebi:
return "Mi"
case PrefixGibi:
return "Gi"
case PrefixTebi:
return "Ti"
default:
return ""
}
}
func ParsePrefix(s string) Prefix {
switch s {
case PrefixHumanKibi:
return PrefixKibi
case PrefixHumanMebi:
return PrefixMebi
case PrefixHumanGibi:
return PrefixGibi
case PrefixHumanTebi:
return PrefixTebi
default:
return PrefixDefault
}
}
// Result is a generic result type for a statistic.
// Total is the total amount of the resource available.
// It is nil if the resource is not a finite quantity.
// Unit is the unit of the resource.
// Used is the amount of the resource used.
type Result struct {
Total *float64 `json:"total"`
Unit string `json:"unit"`
Used float64 `json:"used"`
Total *float64 `json:"total"`
Unit string `json:"unit"`
Used float64 `json:"used"`
Prefix Prefix `json:"-"`
}
// String returns a human-readable representation of the result.
@ -34,38 +82,29 @@ func (r *Result) String() string {
return "-"
}
var usedDisplay, totalDisplay string
var usedScaled, totalScaled float64
var usedPrefix, totalPrefix string
usedScaled, usedPrefix = humanize.ComputeSI(r.Used)
usedDisplay = humanizeFloat(usedScaled)
if r.Total != (*float64)(nil) {
totalScaled, totalPrefix = humanize.ComputeSI(*r.Total)
totalDisplay = humanizeFloat(totalScaled)
scale := 1.0
if r.Prefix != 0.0 {
scale = float64(r.Prefix)
}
var sb strings.Builder
_, _ = sb.WriteString(usedDisplay)
// If the unit prefixes of the used and total values are different,
// display the used value's prefix to avoid confusion.
if usedPrefix != totalPrefix || totalDisplay == "" {
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(usedPrefix)
_, _ = sb.WriteString(r.Unit)
}
if totalDisplay != "" {
var usedScaled, totalScaled float64
usedScaled = r.Used / scale
_, _ = sb.WriteString(humanizeFloat(usedScaled))
if r.Total != (*float64)(nil) {
_, _ = sb.WriteString("/")
_, _ = sb.WriteString(totalDisplay)
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(totalPrefix)
_, _ = sb.WriteString(r.Unit)
totalScaled = *r.Total / scale
_, _ = sb.WriteString(humanizeFloat(totalScaled))
}
if r.Total != nil && *r.Total != 0.0 {
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(r.Prefix.String())
_, _ = sb.WriteString(r.Unit)
if r.Total != (*float64)(nil) && *r.Total > 0 {
_, _ = sb.WriteString(" (")
_, _ = sb.WriteString(fmt.Sprintf("%.0f", r.Used/(*r.Total)*100.0))
pct := r.Used / *r.Total * 100.0
_, _ = sb.WriteString(strconv.FormatFloat(pct, 'f', 0, 64))
_, _ = sb.WriteString("%)")
}
@ -153,8 +192,9 @@ func New(opts ...Option) (*Statter, error) {
// Units are in "cores".
func (s *Statter) HostCPU() (*Result, error) {
r := &Result{
Unit: "cores",
Total: ptr.To(float64(s.nproc)),
Unit: "cores",
Total: ptr.To(float64(s.nproc)),
Prefix: PrefixDefault,
}
c1, err := s.hi.CPUTime()
if err != nil {
@ -177,9 +217,10 @@ func (s *Statter) HostCPU() (*Result, error) {
}
// HostMemory returns the memory usage of the host, in gigabytes.
func (s *Statter) HostMemory() (*Result, error) {
func (s *Statter) HostMemory(p Prefix) (*Result, error) {
r := &Result{
Unit: "B",
Unit: "B",
Prefix: p,
}
hm, err := s.hi.Memory()
if err != nil {

View File

@ -33,19 +33,19 @@ func TestResultString(t *testing.T) {
Result: Result{Used: 12.34, Total: nil, Unit: ""},
},
{
Expected: "1.54 kB",
Result: Result{Used: 1536, Total: nil, Unit: "B"},
Expected: "1.5 KiB",
Result: Result{Used: 1536, Total: nil, Unit: "B", Prefix: PrefixKibi},
},
{
Expected: "1.23 things",
Result: Result{Used: 1.234, Total: nil, Unit: "things"},
},
{
Expected: "1 B/100 TB (0%)",
Result: Result{Used: 1, Total: ptr.To(1000 * 1000 * 1000 * 1000 * 100.0), Unit: "B"},
Expected: "0/100 TiB (0%)",
Result: Result{Used: 1, Total: ptr.To(100.0 * float64(PrefixTebi)), Unit: "B", Prefix: PrefixTebi},
},
{
Expected: "500 mcores/8 cores (6%)",
Expected: "0.5/8 cores (6%)",
Result: Result{Used: 0.5, Total: ptr.To(8.0), Unit: "cores"},
},
} {
@ -76,7 +76,7 @@ func TestStatter(t *testing.T) {
t.Run("HostMemory", func(t *testing.T) {
t.Parallel()
mem, err := s.HostMemory()
mem, err := s.HostMemory(PrefixDefault)
require.NoError(t, err)
assert.NotZero(t, mem.Used)
assert.NotZero(t, mem.Total)
@ -85,7 +85,7 @@ func TestStatter(t *testing.T) {
t.Run("HostDisk", func(t *testing.T) {
t.Parallel()
disk, err := s.Disk("") // default to home dir
disk, err := s.Disk(PrefixDefault, "") // default to home dir
require.NoError(t, err)
assert.NotZero(t, disk.Used)
assert.NotZero(t, disk.Total)
@ -159,7 +159,7 @@ func TestStatter(t *testing.T) {
fs := initFS(t, fsContainerCgroupV1)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory()
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)
@ -211,7 +211,7 @@ func TestStatter(t *testing.T) {
fs := initFS(t, fsContainerCgroupV2)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory()
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)

View File

@ -75,7 +75,7 @@ func (r *RootCmd) stat() *clibase.Cmd {
}
// Host-level stats
ms, err := st.HostMemory()
ms, err := st.HostMemory(clistat.PrefixGibi)
if err != nil {
return err
}
@ -85,7 +85,7 @@ func (r *RootCmd) stat() *clibase.Cmd {
if err != nil {
return err
}
ds, err := st.Disk(home)
ds, err := st.Disk(clistat.PrefixGibi, home)
if err != nil {
return err
}
@ -99,7 +99,7 @@ func (r *RootCmd) stat() *clibase.Cmd {
}
sr.ContainerCPU = cs
ms, err := st.ContainerMemory()
ms, err := st.ContainerMemory(clistat.PrefixGibi)
if err != nil {
return err
}
@ -120,7 +120,6 @@ func (r *RootCmd) stat() *clibase.Cmd {
func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
var hostArg bool
var prefixArg string
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
cmd := &clibase.Cmd{
Use: "cpu",
@ -131,12 +130,6 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
Value: clibase.BoolOf(&hostArg),
Description: "Force host CPU measurement.",
},
{
Flag: "prefix",
Value: clibase.StringOf(&prefixArg),
Description: "Unit prefix.",
Default: "",
},
},
Handler: func(inv *clibase.Invocation) error {
var cs *clistat.Result
@ -164,6 +157,7 @@ func (*RootCmd) statCPU(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
var hostArg bool
var prefixArg string
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
cmd := &clibase.Cmd{
Use: "mem",
@ -174,14 +168,26 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
Value: clibase.BoolOf(&hostArg),
Description: "Force host memory measurement.",
},
{
Description: "SI Prefix for memory measurement.",
Default: clistat.PrefixHumanGibi,
Flag: "prefix",
Value: clibase.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
clistat.PrefixHumanTebi,
),
},
},
Handler: func(inv *clibase.Invocation) error {
pfx := clistat.ParsePrefix(prefixArg)
var ms *clistat.Result
var err error
if ok, _ := clistat.IsContainerized(fs); ok && !hostArg {
ms, err = s.ContainerMemory()
ms, err = s.ContainerMemory(pfx)
} else {
ms, err = s.HostMemory()
ms, err = s.HostMemory(pfx)
}
if err != nil {
return err
@ -201,6 +207,7 @@ func (*RootCmd) statMem(s *clistat.Statter, fs afero.Fs) *clibase.Cmd {
func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd {
var pathArg string
var prefixArg string
formatter := cliui.NewOutputFormatter(cliui.TextFormat(), cliui.JSONFormat())
cmd := &clibase.Cmd{
Use: "disk",
@ -212,9 +219,21 @@ func (*RootCmd) statDisk(s *clistat.Statter) *clibase.Cmd {
Description: "Path for which to check disk usage.",
Default: "/",
},
{
Flag: "prefix",
Default: clistat.PrefixHumanGibi,
Description: "SI Prefix for disk measurement.",
Value: clibase.EnumOf(&prefixArg,
clistat.PrefixHumanKibi,
clistat.PrefixHumanMebi,
clistat.PrefixHumanGibi,
clistat.PrefixHumanTebi,
),
},
},
Handler: func(inv *clibase.Invocation) error {
ds, err := s.Disk(pathArg)
pfx := clistat.ParsePrefix(prefixArg)
ds, err := s.Disk(pfx, pathArg)
if err != nil {
return err
}

View File

@ -9,8 +9,5 @@ Show CPU usage, in cores.
-o, --output string (default: text)
Output format. Available formats: text, json.
--prefix string
Unit prefix.
---
Run `coder --help` for a list of global options.

View File

@ -9,5 +9,8 @@ Show disk usage, in gigabytes.
--path string (default: /)
Path for which to check disk usage.
--prefix Ki|Mi|Gi|Ti (default: Gi)
SI Prefix for disk measurement.
---
Run `coder --help` for a list of global options.

View File

@ -9,5 +9,8 @@ Show memory usage, in gigabytes.
-o, --output string (default: text)
Output format. Available formats: text, json.
--prefix Ki|Mi|Gi|Ti (default: Gi)
SI Prefix for memory measurement.
---
Run `coder --help` for a list of global options.

View File

@ -28,11 +28,3 @@ Force host CPU measurement.
| Default | <code>text</code> |
Output format. Available formats: text, json.
### --prefix
| | |
| ---- | ------------------- |
| Type | <code>string</code> |
Unit prefix.

View File

@ -29,3 +29,12 @@ Output format. Available formats: text, json.
| Default | <code>/</code> |
Path for which to check disk usage.
### --prefix
| | |
| ------- | --------------- | --- | --- | ---------- |
| Type | <code>enum[Ki | Mi | Gi | Ti]</code> |
| Default | <code>Gi</code> |
SI Prefix for disk measurement.

View File

@ -28,3 +28,12 @@ Force host memory measurement.
| Default | <code>text</code> |
Output format. Available formats: text, json.
### --prefix
| | |
| ------- | --------------- | --- | --- | ---------- |
| Type | <code>enum[Ki | Mi | Gi | Ti]</code> |
| Default | <code>Gi</code> |
SI Prefix for memory measurement.