diff --git a/pkg/metricstore/stats.go b/pkg/metricstore/stats.go index f9db2d43..a253d777 100644 --- a/pkg/metricstore/stats.go +++ b/pkg/metricstore/stats.go @@ -20,6 +20,33 @@ type Stats struct { Max schema.Float } +// recomputeStats rebuilds the buffer's running statistics from a single full +// scan of its data and marks them valid. Used after an overwrite invalidated +// the incremental aggregate, and at checkpoint load time. +func (b *buffer) recomputeStats() { + sum, samples := 0.0, 0 + min, max := math.MaxFloat32, -math.MaxFloat32 + for _, v := range b.data { + xf := float64(v) + if math.IsNaN(xf) { + continue + } + samples++ + sum += xf + if xf < min { + min = xf + } + if xf > max { + max = xf + } + } + b.statSum = sum + b.statSamples = samples + b.statMin = min + b.statMax = max + b.statsValid = true +} + func (b *buffer) stats(from, to int64) (Stats, int64, int64, error) { if from < b.start { if b.prev != nil { diff --git a/pkg/metricstore/stats_test.go b/pkg/metricstore/stats_test.go index 4063c531..2dd33009 100644 --- a/pkg/metricstore/stats_test.go +++ b/pkg/metricstore/stats_test.go @@ -84,3 +84,39 @@ func TestWriteOverwriteInvalidatesStats(t *testing.T) { t.Error("statsValid = true, want false after overwrite") } } + +func TestRecomputeStatsRebuildsAfterOverwrite(t *testing.T) { + b := newBuffer(100, 10) + b.write(100, schema.Float(1.0)) + b.write(110, schema.Float(2.0)) + b.write(100, schema.Float(99.0)) // overwrite -> statsValid false, min stale + + b.recomputeStats() + + if !b.statsValid { + t.Error("statsValid = false, want true after recompute") + } + if b.statSamples != 2 { + t.Errorf("statSamples = %d, want 2", b.statSamples) + } + if b.statSum != 101.0 { + t.Errorf("statSum = %v, want 101.0", b.statSum) + } + if b.statMin != 2.0 { + t.Errorf("statMin = %v, want 2.0 (1.0 was overwritten)", b.statMin) + } + if b.statMax != 99.0 { + t.Errorf("statMax = %v, want 99.0", b.statMax) + } +} + +func TestRecomputeStatsEmptyBuffer(t *testing.T) { + b := newBuffer(100, 10) + b.recomputeStats() + if b.statSamples != 0 || b.statSum != 0 { + t.Errorf("empty buffer stats = samples %d sum %v, want 0/0", b.statSamples, b.statSum) + } + if b.statMin != math.MaxFloat32 || b.statMax != -math.MaxFloat32 { + t.Errorf("empty buffer min/max = %v/%v, want sentinels", b.statMin, b.statMax) + } +}