Skip to content

Commit

Permalink
wip: tests for cpu heap and disk stats
Browse files Browse the repository at this point in the history
still figuring out golang testing. Basic use of mocks for files.
  • Loading branch information
bioe007 committed Mar 16, 2024
1 parent 483168a commit 56e44b6
Show file tree
Hide file tree
Showing 12 changed files with 903 additions and 25 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,18 @@ These are the parameters I'd like to show

## Display

initially this will just output some rolling format. will have to think
Initially this will just output some rolling format. will have to think
about something like a tui to properly place things for readability
though


## random thoughts
Is there a faster way to fetch all this data than reading a text file each time?

lots of repetition with a ThingStats that has prev, cur and does math on all the
fields. I could have something like a generic `Delta` struct that accepts the
same type twice, does the math between all fields etc.. But this would require
making everything public in the structs.

## References

- [/proc](https://www.man7.org/linux/man-pages/man5/proc.5.html)
Expand Down
2 changes: 1 addition & 1 deletion cpu/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (cpu *CpuInfo) InfoPrint(num_cpus int) string {
num_cpus = min(cpu.Siblings, num_cpus)
if len(cpu.OldStats) == 0 {
// TODO: - be smarter than just skipping it the first time through?
return ""
return "-mt-"
}

var sb strings.Builder
Expand Down
29 changes: 27 additions & 2 deletions cpu/cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,28 @@ func TestCPUStats(t *testing.T) {
}

func TestCPUHeapPopEmpty(t *testing.T) {
t.Error("not implemented")
c1 := new(CpuStat)
c2 := new(CpuStat)
c3 := new(CpuStat)
cstats := new(calculatedstats)
heap.Init(cstats)
heap.Push(cstats, c2)
heap.Push(cstats, c1)
heap.Push(cstats, c3)

if 3 != len(*cstats) {
t.Errorf("SHould have three elements: 3 != %d", len(*cstats))
}

// It seems like the consensus is golang should panic if we pop from an
// empty list. So here I'll just assert the length is right.
_ = heap.Pop(cstats)
_ = heap.Pop(cstats)
_ = heap.Pop(cstats)

if 0 != len(*cstats) {
t.Errorf("SHould have no elements: 0 != %d", len(*cstats))
}
}

func TestCPUHeapOrder(t *testing.T) {
Expand All @@ -52,8 +73,12 @@ func TestCPUHeapOrder(t *testing.T) {
heap.Push(cstats, c1)
heap.Push(cstats, c3)

c := cstats.Pop().(*CpuStat)
c := heap.Pop(cstats).(*CpuStat)
if c.nr != "1" {
t.Errorf("heap order failure: expected 1, got %s", c.nr)
}
c = heap.Pop(cstats).(*CpuStat)
if c.nr != "3" {
t.Errorf("heap order failure: expected 3, got %s", c.nr)
}
}
235 changes: 221 additions & 14 deletions disk/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package disk

import (
"bufio"
"container/heap"
"fmt"
"io/fs"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -40,10 +43,54 @@ type diskStat struct {
ms_spent_flushing int // This is the total number of milliseconds spent by all flush requests.
}

type Stat struct {
old diskStat
new diskStat
values statValues
type statValues struct {
major float32
minor float32
devname string
num_reads_completed float32
num_reads_merged float32
num_sectors_read float32
ms_reading float32
num_writes_completed float32
num_writes_merged float32
num_sectors_written float32
ms_writing float32
num_io_in_progress float32
ms_doing_io float32
ms_doing_io_weighted float32
num_discards_completed float32
num_discards_merged float32
num_sectors_discarded float32
ms_spent_discarding float32
num_flush_requests_completed float32
ms_spent_flushing float32
}

type diskHeap []*statValues

func (h diskHeap) Len() int { return len(h) }
func (h diskHeap) Less(
i, j int,
) bool {
return h[i].num_writes_completed > h[j].num_writes_completed
}
func (h diskHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *diskHeap) Push(x any) { *h = append(*h, x.(*statValues)) }
func (h *diskHeap) Pop() any {
old := *h
n := len(old)
if n == 0 {
return nil
}
x := old[n-1]
*h = old[0 : n-1]
return x
}

type DiskInfo struct {
old []*diskStat
new []*diskStat
values *diskHeap
}

type dsfields int
Expand Down Expand Up @@ -71,12 +118,57 @@ const (
DSFMS_SPENT_FLUSHING
)

type Disk struct {
sick int
const diskstats = "/proc/diskstats"

func getDiskStatPath() string {
return diskstats
}

func (disks *DiskInfo) estimate() {
if len(disks.old) == 0 {
return
}

prev := disks.old
cur := disks.new

disks.values = new(diskHeap)
heap.Init(disks.values)

for i := range cur {
d := new(statValues)
d.devname = cur[i].devname
d.major = float32(cur[i].major)
d.minor = float32(cur[i].minor)
d.num_reads_completed = float32(cur[i].num_reads_completed - prev[i].num_reads_completed)
d.num_reads_merged = float32(cur[i].num_reads_merged - prev[i].num_reads_merged)
d.num_sectors_read = float32(cur[i].num_sectors_read - prev[i].num_sectors_read)
d.ms_reading = float32(cur[i].ms_reading - prev[i].ms_reading)
d.num_writes_completed = float32(cur[i].num_writes_completed - prev[i].num_writes_completed)
d.num_writes_merged = float32(cur[i].num_writes_merged - prev[i].num_writes_merged)
d.num_sectors_written = float32(cur[i].num_sectors_written - prev[i].num_sectors_written)
d.ms_writing = float32(cur[i].ms_writing - prev[i].ms_writing)
d.num_io_in_progress = float32(cur[i].num_io_in_progress - prev[i].num_io_in_progress)
d.ms_doing_io = float32(cur[i].ms_doing_io - prev[i].ms_doing_io)
d.ms_doing_io_weighted = float32(cur[i].ms_doing_io_weighted - prev[i].ms_doing_io_weighted)
d.num_discards_completed = float32(
cur[i].num_discards_completed - prev[i].num_discards_completed,
)
d.num_discards_merged = float32(cur[i].num_discards_merged - prev[i].num_discards_merged)
d.num_sectors_discarded = float32(
cur[i].num_sectors_discarded - prev[i].num_sectors_discarded,
)
d.ms_spent_discarding = float32(cur[i].ms_spent_discarding - prev[i].ms_spent_discarding)
d.num_flush_requests_completed = float32(
cur[i].num_flush_requests_completed - prev[i].num_flush_requests_completed,
)
d.ms_spent_flushing = float32(cur[i].ms_spent_flushing - prev[i].ms_spent_flushing)
heap.Push(disks.values, d)
}
}

func diskparse(s string) (*DiskStat, error) {
ds := new(DiskStat)
func diskparse(s string) (*diskStat, error) {
ds := new(diskStat)

fields := strings.Fields(s)

Expand All @@ -96,22 +188,111 @@ func diskparse(s string) (*DiskStat, error) {
}
case DSFNAME:
ds.devname = fields[fieldnum]
case DSFNUM_READS_COMPLETED:
ds.num_reads_completed, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_READS_MERGED:
ds.num_reads_merged, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_SECTORS_READ:
ds.num_sectors_read, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_READING:
ds.ms_reading, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_WRITES_COMPLETED:
ds.num_writes_completed, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_WRITES_MERGED:
ds.num_writes_merged, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_SECTORS_WRITTEN:
ds.num_sectors_written, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_WRITING:
ds.ms_writing, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_IO_IN_PROGRESS:
ds.num_io_in_progress, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_DOING_IO:
ds.ms_doing_io, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_DOING_IO_WEIGHTED:
ds.ms_doing_io_weighted, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_DISCARDS_COMPLETED:
ds.num_discards_completed, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_DISCARDS_MERGED:
ds.num_discards_merged, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_SECTORS_DISCARDED:
ds.num_sectors_discarded, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_SPENT_DISCARDING:
ds.ms_spent_discarding, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFNUM_FLUSH_REQUESTS_COMPLETED:
ds.num_flush_requests_completed, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
case DSFMS_SPENT_FLUSHING:
ds.ms_spent_flushing, err = strconv.Atoi(fields[fieldnum])
if err != nil {
return nil, err
}
}
}

return ds, nil
}

// Create an array od diskStat and return it
func DiskStats() ([]*diskStat, error) {
var ds []*diskStat

f, err := os.Open("/proc/diskstats")
// Get a diskinfo and update it with new stats
func DiskStats(di *DiskInfo) (*DiskInfo, error) {
f, err := os.Open(getDiskStatPath())
if err != nil {
return nil, err
}
defer f.Close()
return getDiskStats(di, f)
}

func getDiskStats(di *DiskInfo, f fs.File) (*DiskInfo, error) {
var ds []*diskStat

di.old = di.new
scanner := bufio.NewScanner(f)
for linenum := 0; scanner.Scan(); linenum++ {
line := scanner.Text()
Expand All @@ -121,5 +302,31 @@ func DiskStats() ([]*diskStat, error) {
return nil, err
}
}
return ds, nil
di.new = ds
di.estimate()
return di, nil
}

func (disks *DiskInfo) InfoPrint(num_disks int) string {
if len(disks.old) == 0 {
return ""
}
total_disks := disks.values.Len()
disk_limit := max(min(total_disks, num_disks), 0)

if num_disks == 0 {
return "zero"
}

var sb strings.Builder
for i := 0; i < disk_limit; i++ {
disk := heap.Pop(disks.values).(*statValues)
sb.WriteString(
fmt.Sprintf("%s %.0f\t",
disk.devname,
disk.num_writes_completed,
))
}

return sb.String()
}
Loading

0 comments on commit 56e44b6

Please sign in to comment.