diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-18 14:14:52 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-18 14:14:52 +0200 |
| commit | f7887117c5269ed0336cc058c78c4b222e0e6b34 (patch) | |
| tree | 53e9c4b5163b29b0987beda78572ecdb13c31cf0 /internal/app | |
| parent | 69f5017434298f1ffd4cdc30c30b95d0f4bd344f (diff) | |
feat: add disk I/O stats (read/write throughput bars, hotkey 5, auto-scale)
Add a new disk stats category that reads /proc/diskstats, shows read/write
throughput as colored bars (purple/darker purple like network RX/TX), with
optional utilization % overlay in extended mode. Hotkey 5 cycles: aggregate
(sum all whole-disk devices) → per-device → off. Auto-scale with decay and
optional fixed override via --diskmax / diskmax config key.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/app')
| -rw-r--r-- | internal/app/store.go | 26 | ||||
| -rw-r--r-- | internal/app/store_test.go | 71 |
2 files changed, 94 insertions, 3 deletions
diff --git a/internal/app/store.go b/internal/app/store.go index ffe00c8..5864d8d 100644 --- a/internal/app/store.go +++ b/internal/app/store.go @@ -19,6 +19,7 @@ type hostData struct { mem map[string]int64 net map[string]stats.NetStamp cpu map[string]collector.CPULine + disk map[string]stats.DiskStamp } // Compile-time interface satisfaction checks. @@ -62,6 +63,19 @@ func (s *Store) SetNet(host, iface string, net collector.NetLine, stamp float64) d.net[iface] = stats.NetStamp{B: net.B, Tb: net.Tb, Stamp: stamp} } +// SetDisk sets the disk stamp for the given host and device. +func (s *Store) SetDisk(host, device string, disk collector.DiskLine, stamp float64) { + s.mu.Lock() + defer s.mu.Unlock() + d := s.getOrCreate(host) + d.disk[device] = stats.DiskStamp{ + SectorsRead: disk.SectorsRead, + SectorsWrite: disk.SectorsWrite, + IoTicks: disk.IoTicks, + Stamp: stamp, + } +} + // Snapshot returns a copy of current stats for all hosts for the display. func (s *Store) Snapshot() map[string]*stats.HostStats { s.mu.RLock() @@ -80,6 +94,10 @@ func (s *Store) Snapshot() map[string]*stats.HostStats { for k, v := range d.cpu { cpu[k] = v } + disk := make(map[string]stats.DiskStamp, len(d.disk)) + for k, v := range d.disk { + disk[k] = v + } out[h] = &stats.HostStats{ LoadAvg1: d.load1, LoadAvg5: d.load5, @@ -87,6 +105,7 @@ func (s *Store) Snapshot() map[string]*stats.HostStats { Mem: mem, Net: net, CPU: cpu, + Disk: disk, } } return out @@ -95,9 +114,10 @@ func (s *Store) Snapshot() map[string]*stats.HostStats { func (s *Store) getOrCreate(host string) *hostData { if s.hosts[host] == nil { s.hosts[host] = &hostData{ - mem: make(map[string]int64), - net: make(map[string]stats.NetStamp), - cpu: make(map[string]collector.CPULine), + mem: make(map[string]int64), + net: make(map[string]stats.NetStamp), + cpu: make(map[string]collector.CPULine), + disk: make(map[string]stats.DiskStamp), } } return s.hosts[host] diff --git a/internal/app/store_test.go b/internal/app/store_test.go new file mode 100644 index 0000000..037d7d4 --- /dev/null +++ b/internal/app/store_test.go @@ -0,0 +1,71 @@ +package app + +import ( + "testing" + + "codeberg.org/snonux/loadbars/internal/collector" + "codeberg.org/snonux/loadbars/internal/stats" +) + +func TestSetDisk(t *testing.T) { + s := NewStore() + disk := collector.DiskLine{ + Device: "sda", + SectorsRead: 1000, + SectorsWrite: 2000, + IoTicks: 50, + } + s.SetDisk("host1", "sda", disk, 1.0) + + snap := s.Snapshot() + h := snap["host1"] + if h == nil { + t.Fatal("expected host1 in snapshot") + } + ds, ok := h.Disk["sda"] + if !ok { + t.Fatal("expected sda in Disk map") + } + if ds.SectorsRead != 1000 || ds.SectorsWrite != 2000 || ds.IoTicks != 50 || ds.Stamp != 1.0 { + t.Errorf("disk stamp mismatch: got %+v", ds) + } +} + +func TestSnapshotDiskDeepCopy(t *testing.T) { + s := NewStore() + disk := collector.DiskLine{ + Device: "sda", + SectorsRead: 100, + SectorsWrite: 200, + } + s.SetDisk("host1", "sda", disk, 1.0) + + snap1 := s.Snapshot() + + // Mutate the store after snapshot + disk2 := collector.DiskLine{ + Device: "sda", + SectorsRead: 999, + SectorsWrite: 888, + } + s.SetDisk("host1", "sda", disk2, 2.0) + + // snap1 should be unchanged (deep copy) + ds := snap1["host1"].Disk["sda"] + if ds.SectorsRead != 100 || ds.SectorsWrite != 200 { + t.Errorf("snapshot was mutated: got sr=%d sw=%d, want sr=100 sw=200", + ds.SectorsRead, ds.SectorsWrite) + } + + // Verify new snapshot has updated values + snap2 := s.Snapshot() + ds2 := snap2["host1"].Disk["sda"] + if ds2.SectorsRead != 999 || ds2.SectorsWrite != 888 { + t.Errorf("snap2: got sr=%d sw=%d, want sr=999 sw=888", + ds2.SectorsRead, ds2.SectorsWrite) + } +} + +// Ensure Store still satisfies the collector.StatsStore interface (which now includes SetDisk). +var _ collector.StatsStore = (*Store)(nil) +var _ stats.Source = (*Store)(nil) |
