The process table's columns fit the box they are in, the rate control saves, curves are timed on the viewer's clock and keep to the stride, the power icon is drawn, and a young history draws in from the right instead of being stretched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
100 lines
3.8 KiB
Go
100 lines
3.8 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))
|
|
}
|
|
}
|