diff options
Diffstat (limited to 'internal/display/disk.go')
| -rw-r--r-- | internal/display/disk.go | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/internal/display/disk.go b/internal/display/disk.go new file mode 100644 index 0000000..0143b68 --- /dev/null +++ b/internal/display/disk.go @@ -0,0 +1,220 @@ +package display + +import ( + "regexp" + "sort" + "strings" + + "codeberg.org/snonux/loadbars/internal/constants" + "codeberg.org/snonux/loadbars/internal/stats" + "github.com/veandco/go-sdl2/sdl" +) + +// partitionSuffix matches trailing partition numbers on SCSI-style names (sda1, vda2, xvda3) +// and NVMe partition suffixes (nvme0n1p1). Loop, ram, and dm- devices are handled separately. +var partitionSuffix = regexp.MustCompile(`^(sd|vd|xvd|hd)[a-z]+\d+$`) +var nvmePartition = regexp.MustCompile(`^nvme\d+n\d+p\d+$`) + +// isWholeDisk returns true if the device name represents a whole disk (not a partition, +// loop, ram, or device-mapper device). Used to filter /proc/diskstats entries. +func isWholeDisk(name string) bool { + if strings.HasPrefix(name, "loop") || strings.HasPrefix(name, "ram") || strings.HasPrefix(name, "dm-") { + return false + } + if partitionSuffix.MatchString(name) { + return false + } + if nvmePartition.MatchString(name) { + return false + } + return true +} + +// sortedDiskNames returns the list of disk device names to display based on the disk mode. +// In aggregate mode, returns ["all"]; in device mode, returns sorted whole-disk names; +// in off mode, returns nil. +func sortedDiskNames(disk map[string]stats.DiskStamp, diskMode int) []string { + switch diskMode { + case constants.DiskModeAggregate: + return []string{"all"} + case constants.DiskModeDevices: + var names []string + for dev := range disk { + if isWholeDisk(dev) { + names = append(names, dev) + } + } + sort.Strings(names) + return names + default: + return nil + } +} + +// sumAllDisks sums sectors and picks the latest timestamp across all whole-disk devices. +func sumAllDisks(disk map[string]stats.DiskStamp) stats.DiskStamp { + var sum stats.DiskStamp + for dev, ds := range disk { + if !isWholeDisk(dev) { + continue + } + sum.SectorsRead += ds.SectorsRead + sum.SectorsWrite += ds.SectorsWrite + sum.IoTicks += ds.IoTicks + if ds.Stamp > sum.Stamp { + sum.Stamp = ds.Stamp + } + } + return sum +} + +// updateDiskPeak updates the auto-scale disk peak (bytes/sec) with slow decay. +// When diskMax > 0, the fixed value is used instead. +func updateDiskPeak(snap map[string]*stats.HostStats, state *runState, diskMax float64) { + if diskMax > 0 { + state.diskPeak = diskMax + return + } + // Slow per-frame decay toward idle baseline + state.diskPeak *= 0.9999 + const floorBps = 1048576.0 // 1 MB/s floor + if state.diskPeak < floorBps { + state.diskPeak = floorBps + } + // Scan current disk data to find if any host exceeds the peak + for host, h := range snap { + if h == nil || h.Disk == nil { + continue + } + diskNames := sortedDiskNames(h.Disk, state.diskMode) + for _, name := range diskNames { + key := host + ";disk;" + name + var cur stats.DiskStamp + if name == "all" { + cur = sumAllDisks(h.Disk) + } else { + cur = h.Disk[name] + } + prev, ok := state.prevDisk[key] + if !ok || cur.Stamp <= prev.Stamp || prev.Stamp == 0 { + continue + } + dt := cur.Stamp - prev.Stamp + if dt <= 0 { + continue + } + readBps := float64(cur.SectorsRead-prev.SectorsRead) * 512 / dt + writeBps := float64(cur.SectorsWrite-prev.SectorsWrite) * 512 / dt + totalBps := readBps + writeBps + if totalBps > state.diskPeak { + state.diskPeak = totalBps + } + } + } +} + +// drawDiskBarSmoothed draws a single disk bar with read (top, purple) and write (bottom, +// darker purple). Returns the current DiskStamp to be stored as previous for the next frame. +func drawDiskBarSmoothed(renderer *sdl.Renderer, cur stats.DiskStamp, cfg *runState, smoothed *struct{ readPct, writePct float64 }, prev stats.DiskStamp, factor float64, barW, x, y, barH int32, extended bool) stats.DiskStamp { + // Clear this slot to black + renderer.SetDrawColor(constants.Black.R, constants.Black.G, constants.Black.B, 255) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: barH}) + + // Only recompute when the collector has provided new data (same guard as net bars). + if cur.Stamp > prev.Stamp && prev.Stamp > 0 { + prev = smoothDiskUtilization(cur, prev, cfg, smoothed, factor) + } else if prev.Stamp == 0 { + // First sample: record but don't draw yet (no delta available) + return cur + } + + drawDiskHalves(renderer, smoothed, x, y, barW, barH) + + // In extended mode, overlay a utilization % line + if extended { + drawDiskUtilLine(renderer, cur, prev, cfg, x, y, barW, barH) + } + return prev +} + +// smoothDiskUtilization computes read/write throughput as % of diskPeak and smooths. +func smoothDiskUtilization(cur, prev stats.DiskStamp, state *runState, smoothed *struct{ readPct, writePct float64 }, factor float64) stats.DiskStamp { + peak := state.diskPeak + if peak <= 0 { + peak = 1048576 // 1 MB/s fallback + } + dt := cur.Stamp - prev.Stamp + if dt > 0 { + deltaRead := cur.SectorsRead - prev.SectorsRead + deltaWrite := cur.SectorsWrite - prev.SectorsWrite + if deltaRead < 0 { + deltaRead = 0 + } + if deltaWrite < 0 { + deltaWrite = 0 + } + readBps := float64(deltaRead) * 512 / dt + writeBps := float64(deltaWrite) * 512 / dt + targetRead := 100 * readBps / peak + targetWrite := 100 * writeBps / peak + smoothed.readPct += (targetRead - smoothed.readPct) * factor + smoothed.writePct += (targetWrite - smoothed.writePct) * factor + } + return cur // advance the baseline +} + +// drawDiskHalves draws read from top (purple) and write from bottom (darker purple). +func drawDiskHalves(renderer *sdl.Renderer, smoothed *struct{ readPct, writePct float64 }, x, y, barW, barH int32) { + halfH := barH / 2 + pxPerPct := float64(barH) / 100.0 + + // Read from top (purple) + readH := int32(smoothed.readPct * pxPerPct) + if readH > halfH { + readH = halfH + } + if readH > 0 { + renderer.SetDrawColor(constants.DiskRead.R, constants.DiskRead.G, constants.DiskRead.B, 255) + renderer.FillRect(&sdl.Rect{X: x, Y: y, W: barW, H: readH}) + } + + // Write from bottom (darker purple) + writeH := int32(smoothed.writePct * pxPerPct) + if writeH > halfH { + writeH = halfH + } + if writeH > 0 { + renderer.SetDrawColor(constants.DiskWrite.R, constants.DiskWrite.G, constants.DiskWrite.B, 255) + renderer.FillRect(&sdl.Rect{X: x, Y: y + barH - writeH, W: barW, H: writeH}) + } +} + +// drawDiskUtilLine draws a 3px-thick horizontal line showing disk utilization % +// (fraction of time the device had I/O in progress) in extended mode. +func drawDiskUtilLine(renderer *sdl.Renderer, cur, prev stats.DiskStamp, state *runState, x, y, barW, barH int32) { + dt := cur.Stamp - prev.Stamp + if dt <= 0 { + return + } + // IoTicks is cumulative ms; utilization = delta_io_ticks / (dt * 1000) + deltaIo := cur.IoTicks - prev.IoTicks + if deltaIo < 0 { + deltaIo = 0 + } + utilPct := float64(deltaIo) / (dt * 1000) * 100 + if utilPct > 100 { + utilPct = 100 + } + lineY := y + int32(utilPct/100*float64(barH)) + if lineY >= y+barH { + lineY = y + barH - 1 + } + renderer.SetDrawColor(constants.DiskUtil.R, constants.DiskUtil.G, constants.DiskUtil.B, 255) + // Draw 3px band for visibility + for dy := int32(-1); dy <= 1; dy++ { + ly := lineY + dy + if ly >= y && ly < y+barH { + renderer.DrawLine(x, ly, x+barW-1, ly) + } + } +} |
