Files
pulse/internal/monitoring/ratetracker_test.go
T
2026-07-24 10:26:50 +01:00

237 lines
7.9 KiB
Go

package monitoring
import (
"math"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func fullCounterSample(at time.Time, diskRead, diskWrite, networkIn, networkOut int64) models.IOMetrics {
return models.IOMetrics{
DiskRead: diskRead,
DiskWrite: diskWrite,
NetworkIn: networkIn,
NetworkOut: networkOut,
Timestamp: at,
}
}
func TestRateTrackerConstantRatesUseActualElapsedTime(t *testing.T) {
for _, interval := range []time.Duration{30 * time.Second, 60 * time.Second, 90 * time.Second} {
t.Run(interval.String(), func(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, 1_000, 2_000, 3_000, 4_000))
seconds := int64(interval / time.Second)
read, write, in, out := tracker.CalculateRates(
"guest",
fullCounterSample(
start.Add(interval),
1_000+seconds*1_024,
2_000+seconds*2_048,
3_000+seconds*4_096,
4_000+seconds*8_192,
),
)
if read != 1_024 || write != 2_048 || in != 4_096 || out != 8_192 {
t.Fatalf("rates = (%v, %v, %v, %v), want (1024, 2048, 4096, 8192)", read, write, in, out)
}
})
}
}
func TestRateTrackerBurstUsesAdjacentObservationInterval(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, 0, 0, 0, 0))
read, _, _, _ := tracker.CalculateRates("guest", fullCounterSample(start.Add(90*time.Second), 90_000, 0, 0, 0))
if read != 1_000 {
t.Fatalf("burst rate = %v, want 1000 B/s", read)
}
read, _, _, _ = tracker.CalculateRates("guest", fullCounterSample(start.Add(180*time.Second), 90_000, 0, 0, 0))
if read != 0 {
t.Fatalf("idle interval rate = %v, want valid zero", read)
}
}
func TestRateTrackerMissingAndExplicitZeroAreDistinct(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
diskOnly := models.IOCounterPresence{Explicit: true, DiskRead: true}
tracker.CalculateRates("guest", models.IOMetrics{
DiskRead: 0,
Timestamp: start,
Presence: diskOnly,
})
read, write, in, out := tracker.CalculateRates("guest", models.IOMetrics{
DiskRead: 0,
Timestamp: start.Add(60 * time.Second),
Presence: diskOnly,
})
if read != 0 {
t.Fatalf("explicit zero rate = %v, want 0", read)
}
if write != -1 || in != -1 || out != -1 {
t.Fatalf("missing rates = (%v, %v, %v), want unknown", write, in, out)
}
}
func TestRateTrackerPartialSampleKeepsIndependentBaseline(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, 0, 0, 0, 0))
read, write, in, out := tracker.CalculateRates("guest", models.IOMetrics{
DiskRead: 60_000,
Timestamp: start.Add(60 * time.Second),
Presence: models.IOCounterPresence{
Explicit: true,
DiskRead: true,
},
})
if read != 1_000 || write != -1 || in != -1 || out != -1 {
t.Fatalf("partial rates = (%v, %v, %v, %v)", read, write, in, out)
}
_, write, _, _ = tracker.CalculateRates("guest", models.IOMetrics{
DiskWrite: 180_000,
Timestamp: start.Add(90 * time.Second),
Presence: models.IOCounterPresence{
Explicit: true,
DiskWrite: true,
},
})
if write != 2_000 {
t.Fatalf("disk write rate = %v, want 2000 over its 90s observation gap", write)
}
}
func TestRateTrackerUsesEachCounterReceiptTimeForPartialSources(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, 0, 0, 0, 0))
read, write, _, _ := tracker.CalculateRates("guest", models.IOMetrics{
DiskRead: 30_000,
DiskWrite: 180_000,
Timestamp: start.Add(90 * time.Second),
Presence: models.IOCounterPresence{
Explicit: true,
DiskRead: true,
DiskWrite: true,
},
ObservedAt: models.IOCounterObservationTimes{
DiskRead: start.Add(30 * time.Second),
DiskWrite: start.Add(90 * time.Second),
},
})
if read != 1_000 || write != 2_000 {
t.Fatalf("rates = (%v, %v), want per-counter receipt rates (1000, 2000)", read, write)
}
}
func TestRateTrackerResetOrWrapRebasesCounterEpoch(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, math.MaxInt64-1_000, 50_000, 10_000, 20_000))
read, write, in, out := tracker.CalculateRates(
"guest",
fullCounterSample(start.Add(30*time.Second), 500, 1_000, 100, 200),
)
if read != 0 || write != 0 || in != 0 || out != 0 {
t.Fatalf("reset rates = (%v, %v, %v, %v), want zeros", read, write, in, out)
}
read, _, _, _ = tracker.CalculateRates("guest", fullCounterSample(start.Add(60*time.Second), 30_500, 1_000, 100, 200))
if read != 1_000 {
t.Fatalf("post-reset rate = %v, want 1000", read)
}
}
func TestRateTrackerUptimeRollbackRebasesEvenWhenCounterSurpassesOldValue(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
beforeRestart := fullCounterSample(start, 1_000, 2_000, 3_000, 4_000)
beforeRestart.SourceUptime = 10_000
tracker.CalculateRates("guest", beforeRestart)
afterRestart := fullCounterSample(start.Add(90*time.Second), 91_000, 182_000, 273_000, 364_000)
afterRestart.SourceUptime = 30
read, write, in, out := tracker.CalculateRates("guest", afterRestart)
if read != -1 || write != -1 || in != -1 || out != -1 {
t.Fatalf("first sample in restarted epoch = (%v, %v, %v, %v), want unknown", read, write, in, out)
}
next := fullCounterSample(start.Add(120*time.Second), 121_000, 242_000, 363_000, 484_000)
next.SourceUptime = 60
read, write, in, out = tracker.CalculateRates("guest", next)
if read != 1_000 || write != 2_000 || in != 3_000 || out != 4_000 {
t.Fatalf("post-restart rates = (%v, %v, %v, %v), want (1000, 2000, 3000, 4000)", read, write, in, out)
}
}
func TestRateTrackerRejectsOutOfOrderSamplesWithoutChangingBaseline(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRates("guest", fullCounterSample(start, 0, 0, 0, 0))
tracker.CalculateRates("guest", fullCounterSample(start.Add(60*time.Second), 60_000, 0, 0, 0))
read, _, _, _ := tracker.CalculateRates("guest", fullCounterSample(start.Add(30*time.Second), 90_000, 0, 0, 0))
if read != -1 {
t.Fatalf("out-of-order rate = %v, want unknown", read)
}
read, _, _, _ = tracker.CalculateRates("guest", fullCounterSample(start.Add(90*time.Second), 90_000, 0, 0, 0))
if read != 1_000 {
t.Fatalf("rate after rejected sample = %v, want 1000", read)
}
}
func TestRateTrackerCleanupUsesLatestSampleEvenWhenIdleOrPartial(t *testing.T) {
tracker := NewRateTracker()
now := time.Now()
tracker.CalculateRates("idle", fullCounterSample(now.Add(-2*time.Hour), 100, 100, 100, 100))
tracker.CalculateRates("idle", models.IOMetrics{
Timestamp: now,
Presence: models.IOCounterPresence{Explicit: true},
})
tracker.CalculateRates("stale", fullCounterSample(now.Add(-2*time.Hour), 100, 100, 100, 100))
if removed := tracker.Cleanup(now.Add(-time.Hour)); removed != 1 {
t.Fatalf("removed = %d, want 1", removed)
}
if _, ok := tracker.history["idle"]; !ok {
t.Fatal("idle resource was removed despite a recent sample")
}
}
func TestRateTrackerDiskBusyUsesElapsedMilliseconds(t *testing.T) {
tracker := NewRateTracker()
start := time.Unix(1_700_000_000, 0)
tracker.CalculateRatesWithBusy("disk", models.IOMetrics{DiskBusy: 100, Timestamp: start})
_, _, busy, _, _ := tracker.CalculateRatesWithBusy("disk", models.IOMetrics{
DiskBusy: 15_100,
Timestamp: start.Add(30 * time.Second),
})
if busy != 50 {
t.Fatalf("busy = %v, want 50%%", busy)
}
}
func TestRateTrackerClearRestoresFirstSampleUnknown(t *testing.T) {
tracker := NewRateTracker()
now := time.Now()
tracker.CalculateRates("guest", fullCounterSample(now, 1, 2, 3, 4))
tracker.Clear()
read, write, in, out := tracker.CalculateRates("guest", fullCounterSample(now.Add(time.Second), 2, 3, 4, 5))
if read != -1 || write != -1 || in != -1 || out != -1 {
t.Fatalf("first rates after clear = (%v, %v, %v, %v)", read, write, in, out)
}
}