An Apple SSD presents three namespaces and the kernel lists each as a whole disk, so the dashboard was showing one drive as three. The Disks page has always folded them; now the dashboard, the Top tab and the disk.* sensors do too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
package metrics
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFoldAverages(t *testing.T) {
|
|
b := []Sample{
|
|
{T: 1, CPU: 10, Cores: []float64{10, 0}, Mem: Mem{Total: 100, Used: 10},
|
|
Temps: []Temp{{Chip: "c", Label: "l", C: 40}}, Net: []Net{{Iface: "eth0", RxBps: 100}},
|
|
Disks: []Disk{{Name: "sda", ReadBps: 10, UtilPct: 5}}},
|
|
{T: 2, CPU: 30, Cores: []float64{30, 100}, Mem: Mem{Total: 100, Used: 30},
|
|
Temps: []Temp{{Chip: "c", Label: "l", C: 60}}, Net: []Net{{Iface: "eth0", RxBps: 300}, {Iface: "new", RxBps: 7}},
|
|
Disks: []Disk{{Name: "sda", ReadBps: 30, UtilPct: 15}}},
|
|
}
|
|
f := fold(b)
|
|
if f.T != 2 {
|
|
t.Errorf("a folded point is stamped with the newest sample in it, got %d", f.T)
|
|
}
|
|
if f.CPU != 20 || f.Cores[0] != 20 || f.Cores[1] != 50 {
|
|
t.Errorf("cpu averaged: %v %v", f.CPU, f.Cores)
|
|
}
|
|
if f.Mem.Used != 20 || f.Mem.Total != 100 {
|
|
t.Errorf("memory: used averaged, total taken from the newest, got %+v", f.Mem)
|
|
}
|
|
if f.Temps[0].C != 50 || f.Net[0].RxBps != 200 || f.Disks[0].ReadBps != 20 || f.Disks[0].UtilPct != 10 {
|
|
t.Errorf("lists matched by name: %v %v %v", f.Temps, f.Net, f.Disks)
|
|
}
|
|
if len(f.Net) != 2 || f.Net[1].RxBps != 7 {
|
|
t.Errorf("an interface that appeared inside the bucket is averaged over the samples that have it: %v", f.Net)
|
|
}
|
|
// the template's own slices must not be touched: Latest() shares them
|
|
if b[1].Cores[0] != 30 || b[1].Temps[0].C != 60 {
|
|
t.Error("fold wrote through to the sample it templated from")
|
|
}
|
|
}
|
|
|
|
func TestStrideAndTrim(t *testing.T) {
|
|
c := &Collector{interval: 2 * time.Second, window: DefaultWindow}
|
|
if got := c.stride(); got != 2*time.Second {
|
|
t.Errorf("a window of %v over %d points is finer than the interval, so the interval wins; got %v", DefaultWindow, History, got)
|
|
}
|
|
c.window = 24 * time.Hour
|
|
if got := c.stride(); got != 24*time.Hour/History {
|
|
t.Errorf("stride at a day: %v", got)
|
|
}
|
|
// a full day of samples at the day stride keeps exactly the budget
|
|
c.interval, c.window = time.Second, time.Hour
|
|
stride := c.stride().Milliseconds()
|
|
for i := 0; i < 5*History; i++ {
|
|
c.keep(Sample{T: int64(i) * stride})
|
|
}
|
|
if len(c.ring) != History {
|
|
t.Errorf("the point count is the budget, got %d", len(c.ring))
|
|
}
|
|
if span := c.ring[len(c.ring)-1].T - c.ring[0].T; span > c.window.Milliseconds() {
|
|
t.Errorf("the kept history reaches back further than the window: %d ms", span)
|
|
}
|
|
// narrowing the window drops what is now too old, at once
|
|
c.SetWindow(MinWindow)
|
|
if span := c.ring[len(c.ring)-1].T - c.ring[0].T; span > MinWindow.Milliseconds() {
|
|
t.Errorf("narrowing the window left %d ms of history", span)
|
|
}
|
|
c.SetWindow(365 * 24 * time.Hour)
|
|
if c.Window() != MaxWindow {
|
|
t.Errorf("a window past the maximum is the maximum, got %v", c.Window())
|
|
}
|
|
}
|
|
|
|
// Points are one stride apart — not a stride and a sample — and jitter doesn't change that.
|
|
func TestKeepSpacing(t *testing.T) {
|
|
c := &Collector{interval: 2 * time.Second, window: time.Hour} // stride 12 s: six samples a point
|
|
at := int64(1_000_000)
|
|
for i := 0; i < 6*40; i++ {
|
|
jitter := int64((i*37)%41 - 20) // ±20 ms, deterministic
|
|
c.keep(Sample{T: at + jitter, CPU: float64(i % 6)})
|
|
at += 2000
|
|
}
|
|
if len(c.ring) != 40 {
|
|
t.Fatalf("240 samples at six a point are 40 points, got %d", len(c.ring))
|
|
}
|
|
for i := 1; i < len(c.ring); i++ {
|
|
if gap := c.ring[i].T - c.ring[i-1].T; gap < 11900 || gap > 12100 {
|
|
t.Fatalf("point %d is %d ms after the one before; a stride is 12000", i, gap)
|
|
}
|
|
}
|
|
if c.ring[3].CPU != 2.5 {
|
|
t.Errorf("each point is the mean of its own six samples (0…5 → 2.5), got %v", c.ring[3].CPU)
|
|
}
|
|
// and when the stride is the interval, every sample is a point
|
|
c = &Collector{interval: 2 * time.Second, window: DefaultWindow}
|
|
for i := 0; i < 50; i++ {
|
|
c.keep(Sample{T: int64(i)*2000 + int64(i%3) - 1})
|
|
}
|
|
if len(c.ring) != 50 {
|
|
t.Errorf("at the default window every sample is kept, got %d of 50", len(c.ring))
|
|
}
|
|
}
|
|
|
|
func TestFoldNVMe(t *testing.T) {
|
|
// an Apple SSD as the kernel lists it: one controller, three namespaces
|
|
got := foldNVMe([]Disk{
|
|
{Name: "nvme0n1", Model: "APPLE SSD AP0512Z", Size: 500 << 30, ReadBps: 100, WriteBps: 200, UtilPct: 12, Temp: 37},
|
|
{Name: "nvme0n2", Model: "APPLE SSD AP0512Z", Size: 3 << 20, ReadBps: 10, WriteBps: 20, UtilPct: 40},
|
|
{Name: "nvme0n3", Model: "APPLE SSD AP0512Z", Size: 128 << 20, ReadBps: 1, WriteBps: 2, UtilPct: 5},
|
|
{Name: "sda", Size: 100 << 30, ReadBps: 7, UtilPct: 3},
|
|
{Name: "nvme1n1", Size: 200 << 30, ReadBps: 5},
|
|
})
|
|
if len(got) != 3 {
|
|
t.Fatalf("three namespaces of one controller are one drive: %v", names(got))
|
|
}
|
|
d := got[0]
|
|
if d.Name != "nvme0" {
|
|
t.Errorf("the drive is the controller, got %q", d.Name)
|
|
}
|
|
if d.Size != 500<<30+3<<20+128<<20 {
|
|
t.Errorf("its size is all of its namespaces, got %d", d.Size)
|
|
}
|
|
if d.ReadBps != 111 || d.WriteBps != 222 {
|
|
t.Errorf("rates add up, got %v/%v", d.ReadBps, d.WriteBps)
|
|
}
|
|
if d.UtilPct != 40 {
|
|
t.Errorf("busy is the longest of them, not the sum: got %v", d.UtilPct)
|
|
}
|
|
if d.Temp != 37 {
|
|
t.Errorf("a temperature from any namespace is the drive's, got %v", d.Temp)
|
|
}
|
|
if got[1].Name != "sda" || got[1].UtilPct != 3 || got[2].Name != "nvme1" {
|
|
t.Errorf("everything else is left alone: %v", names(got))
|
|
}
|
|
if n := len(foldNVMe(nil)); n != 0 {
|
|
t.Errorf("no disks, no drives, got %d", n)
|
|
}
|
|
}
|
|
|
|
func names(d []Disk) []string {
|
|
out := make([]string, len(d))
|
|
for i, x := range d {
|
|
out[i] = x.Name
|
|
}
|
|
return out
|
|
}
|