Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/google/go-containerregistry v0.20.7
github.com/gorilla/websocket v1.5.3
github.com/itchyny/json2yaml v0.1.4
github.com/kernel/hypeman-go v0.9.3
github.com/kernel/hypeman-go v0.9.6
github.com/muesli/reflow v0.3.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
github.com/kernel/hypeman-go v0.9.3 h1:UtlynELXJJ9Znnuq5mLWXCc1ymPh7PPYamEd6fb3UXM=
github.com/kernel/hypeman-go v0.9.3/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/kernel/hypeman-go v0.9.6 h1:gkKbUiTYPWVDa9GwX/xaf9+z+eiTYIj6oglPlir4Xbo=
github.com/kernel/hypeman-go v0.9.6/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
37 changes: 34 additions & 3 deletions pkg/cmd/resourcecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func showResourcesTable(data []byte) error {
printResourceRow("cpu", obj.Get("cpu"), "cores")
printResourceRow("memory", obj.Get("memory"), "bytes")
printResourceRow("disk", obj.Get("disk"), "bytes")
printResourceRow("disk_io", obj.Get("disk_io"), "disk_bps")
printResourceRow("network", obj.Get("network"), "bps")

// Print GPU information if available
Expand Down Expand Up @@ -104,8 +105,8 @@ func showResourcesTable(data []byte) error {
if allocations.Exists() && allocations.IsArray() && len(allocations.Array()) > 0 {
fmt.Println()
fmt.Println("ALLOCATIONS:")
fmt.Println("INSTANCE CPU MEMORY DISK NET DOWN NET UP")
fmt.Println(strings.Repeat("-", 80))
fmt.Println("INSTANCE CPU MEMORY DISK DISK I/O NET DOWN NET UP")
fmt.Println(strings.Repeat("-", 95))
allocations.ForEach(func(key, value gjson.Result) bool {
name := value.Get("instance_name").String()
if len(name) > 28 {
Expand All @@ -114,9 +115,10 @@ func showResourcesTable(data []byte) error {
cpu := value.Get("cpu").Int()
mem := formatBytes(value.Get("memory_bytes").Int())
disk := formatBytes(value.Get("disk_bytes").Int())
diskIO := formatDiskBps(value.Get("disk_io_bps").Int())
netDown := formatBps(value.Get("network_download_bps").Int())
netUp := formatBps(value.Get("network_upload_bps").Int())
fmt.Printf("%-28s %3d %-9s %-9s %-10s %s\n", name, cpu, mem, disk, netDown, netUp)
fmt.Printf("%-28s %3d %-9s %-9s %-10s %-10s %s\n", name, cpu, mem, disk, diskIO, netDown, netUp)
return true
})
}
Expand Down Expand Up @@ -148,6 +150,11 @@ func printResourceRow(name string, res gjson.Result, unit string) {
effStr = formatBps(effective)
allocStr = formatBps(allocated)
availStr = formatBps(available)
case "disk_bps":
capStr = formatDiskBps(capacity)
effStr = formatDiskBps(effective)
allocStr = formatDiskBps(allocated)
availStr = formatDiskBps(available)
default:
capStr = fmt.Sprintf("%d", capacity)
effStr = fmt.Sprintf("%d", effective)
Expand Down Expand Up @@ -257,3 +264,27 @@ func formatBps(bytesPerSec int64) string {
return fmt.Sprintf("%d bps", bps)
}
}

// formatDiskBps formats disk I/O bandwidth in bytes per second to a human-readable
// string (KB/s, MB/s, GB/s). Unlike network bandwidth which uses bits, disk I/O
// is conventionally displayed in bytes per second.
func formatDiskBps(bytesPerSec int64) string {
const (
KBps = 1000
MBps = KBps * 1000
GBps = MBps * 1000
)

switch {
case bytesPerSec >= GBps:
return fmt.Sprintf("%.1f GB/s", float64(bytesPerSec)/GBps)
case bytesPerSec >= MBps:
return fmt.Sprintf("%.0f MB/s", float64(bytesPerSec)/MBps)
case bytesPerSec >= KBps:
return fmt.Sprintf("%.0f KB/s", float64(bytesPerSec)/KBps)
case bytesPerSec == 0:
return "-"
default:
return fmt.Sprintf("%d B/s", bytesPerSec)
}
}
38 changes: 35 additions & 3 deletions pkg/cmd/resourcecmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ func TestFormatBps(t *testing.T) {
// The CLI should convert to bits and display as Mbps/Gbps
// Formula: bytes/sec * 8 = bits/sec
tests := []struct {
name string
bytesPerSec int64
expected string
name string
bytesPerSec int64
expected string
}{
// 30 Mbps = 30,000,000 bits/sec = 3,750,000 bytes/sec
// This is the user's reported bug: they set 30Mbps, API stores 3750000 bytes/sec,
Expand Down Expand Up @@ -94,3 +94,35 @@ func TestFormatBps(t *testing.T) {
})
}
}

func TestFormatDiskBps(t *testing.T) {
// Disk I/O is displayed in bytes/sec (KB/s, MB/s, GB/s), not bits/sec
tests := []struct {
name string
bytesPerSec int64
expected string
}{
// Common disk I/O limits
{"100 MB/s SSD limit", 100000000, "100 MB/s"},
{"500 MB/s NVMe limit", 500000000, "500 MB/s"},
{"1 GB/s high-perf limit", 1000000000, "1.0 GB/s"},
{"3.5 GB/s NVMe Gen4", 3500000000, "3.5 GB/s"},

// Smaller values
{"50 MB/s HDD limit", 50000000, "50 MB/s"},
{"10 MB/s throttled", 10000000, "10 MB/s"},
{"1 MB/s minimal", 1000000, "1 MB/s"},
{"500 KB/s very slow", 500000, "500 KB/s"},

// Edge cases
{"zero (no limit or disabled)", 0, "-"},
{"tiny value", 500, "500 B/s"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatDiskBps(tt.bytesPerSec)
assert.Equal(t, tt.expected, result)
})
}
}