prometheus/promql/histogram_stats_iterator_test.go
beorn7 5010bd4bb1 promql: Optimize HistogramStatsIterator by disallowing integer histograms
The `HistogramStatsIterator` is only meant to be used within PromQL.
PromQL only ever uses float histograms. If `HistogramStatsIterator` is
capable of handling integer histograms, it will still be used, for
example by the `BufferedSeriesIterator`, which buffers samples and
will use an integer `Histogram` for it, if the underlying chunk is an
integer histogram chunk (which is common).

However, we can simply intercept the `Next` and `Seek` calls and
pretend to only ever be able te return float histograms. This has the
welcome side effect that we do not have to handle a mix of float and
integer histograms in the `HistogramStatsIterator` anymore.

With this commit, the `AtHistogram` call has been changed to panic so
that we ensure it is never called.

Benchmark differences between this and the previous commit:

name                                                                       old time/op    new time/op    delta
NativeHistograms/histogram_count_with_short_rate_interval-16                  837ms ± 3%     616ms ± 2%  -26.36%  (p=0.008 n=5+5)
NativeHistograms/histogram_count_with_long_rate_interval-16                   1.11s ± 1%     0.91s ± 3%  -17.75%  (p=0.008 n=5+5)
NativeHistogramsCustomBuckets/histogram_count_with_short_rate_interval-16     751ms ± 6%     581ms ± 1%  -22.63%  (p=0.008 n=5+5)
NativeHistogramsCustomBuckets/histogram_count_with_long_rate_interval-16      1.13s ±11%     0.85s ± 2%  -24.59%  (p=0.008 n=5+5)

name                                                                       old alloc/op   new alloc/op   delta
NativeHistograms/histogram_count_with_short_rate_interval-16                  531MB ± 0%     148MB ± 0%  -72.08%  (p=0.008 n=5+5)
NativeHistograms/histogram_count_with_long_rate_interval-16                   528MB ± 0%     145MB ± 0%  -72.60%  (p=0.016 n=5+4)
NativeHistogramsCustomBuckets/histogram_count_with_short_rate_interval-16     452MB ± 0%     145MB ± 0%  -67.97%  (p=0.016 n=5+4)
NativeHistogramsCustomBuckets/histogram_count_with_long_rate_interval-16      452MB ± 0%     141MB ± 0%  -68.70%  (p=0.016 n=5+4)

name                                                                       old allocs/op  new allocs/op  delta
NativeHistograms/histogram_count_with_short_rate_interval-16                  8.95M ± 0%     1.60M ± 0%  -82.15%  (p=0.008 n=5+5)
NativeHistograms/histogram_count_with_long_rate_interval-16                   8.84M ± 0%     1.49M ± 0%  -83.16%  (p=0.008 n=5+5)
NativeHistogramsCustomBuckets/histogram_count_with_short_rate_interval-16     5.96M ± 0%     1.57M ± 0%  -73.68%  (p=0.008 n=5+5)
NativeHistogramsCustomBuckets/histogram_count_with_long_rate_interval-16      5.86M ± 0%     1.46M ± 0%  -75.05%  (p=0.016 n=5+4)

Signed-off-by: beorn7 <beorn@grafana.com>
2025-09-04 14:06:19 +02:00

230 lines
7.5 KiB
Go

// Copyright 2015 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promql
import (
"math"
"testing"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/tsdbutil"
)
func TestHistogramStatsDecoding(t *testing.T) {
cases := []struct {
name string
histograms []*histogram.Histogram
expectedHints []histogram.CounterResetHint
}{
{
name: "unknown counter reset for later sample triggers detection",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(0, histogram.NotCounterReset),
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(2, histogram.CounterReset),
tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.NotCounterReset,
histogram.NotCounterReset,
histogram.CounterReset,
histogram.NotCounterReset,
},
},
{
name: "unknown counter reset for first sample does not trigger detection",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(0, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(2, histogram.CounterReset),
tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.UnknownCounterReset,
histogram.NotCounterReset,
histogram.CounterReset,
histogram.NotCounterReset,
},
},
{
name: "stale sample before unknown reset hint",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(0, histogram.NotCounterReset),
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
{Sum: math.Float64frombits(value.StaleNaN)},
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.NotCounterReset,
histogram.NotCounterReset,
histogram.UnknownCounterReset,
histogram.NotCounterReset,
},
},
{
name: "unknown counter reset at the beginning",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.UnknownCounterReset,
},
},
{
name: "detect real counter reset",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.UnknownCounterReset,
histogram.CounterReset,
},
},
{
name: "detect real counter reset after stale NaN",
histograms: []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
{Sum: math.Float64frombits(value.StaleNaN)},
tsdbutil.GenerateTestHistogramWithHint(1, histogram.UnknownCounterReset),
},
expectedHints: []histogram.CounterResetHint{
histogram.UnknownCounterReset,
histogram.UnknownCounterReset,
histogram.CounterReset,
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
check := func(statsIterator *HistogramStatsIterator) {
decodedStats := make([]*histogram.FloatHistogram, 0)
for statsIterator.Next() != chunkenc.ValNone {
_, h := statsIterator.AtFloatHistogram(nil)
decodedStats = append(decodedStats, h)
}
for i := 0; i < len(tc.histograms); i++ {
require.Equal(t, tc.expectedHints[i], decodedStats[i].CounterResetHint)
fh := tc.histograms[i].ToFloat(nil)
if value.IsStaleNaN(fh.Sum) {
require.True(t, value.IsStaleNaN(decodedStats[i].Sum))
require.Equal(t, float64(0), decodedStats[i].Count)
} else {
require.Equal(t, fh.Count, decodedStats[i].Count)
require.Equal(t, fh.Sum, decodedStats[i].Sum)
}
}
}
// Check that we get the expected results with a fresh iterator.
statsIterator := NewHistogramStatsIterator(newHistogramSeries(tc.histograms).Iterator(nil))
check(statsIterator)
// Check that we get the same results if we reset and reuse that iterator.
statsIterator.Reset(newHistogramSeries(tc.histograms).Iterator(nil))
check(statsIterator)
})
}
}
func TestHistogramStatsMixedUse(t *testing.T) {
histograms := []*histogram.Histogram{
tsdbutil.GenerateTestHistogramWithHint(2, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(4, histogram.UnknownCounterReset),
tsdbutil.GenerateTestHistogramWithHint(0, histogram.UnknownCounterReset),
}
series := newHistogramSeries(histograms)
it := series.Iterator(nil)
statsIterator := NewHistogramStatsIterator(it)
expectedHints := []histogram.CounterResetHint{
histogram.UnknownCounterReset,
histogram.NotCounterReset,
histogram.CounterReset,
}
// Note that statsIterator always returns float histograms.
actualHints := make([]histogram.CounterResetHint, 3)
typ := statsIterator.Next()
require.Equal(t, chunkenc.ValFloatHistogram, typ)
_, h := statsIterator.AtFloatHistogram(nil)
actualHints[0] = h.CounterResetHint
typ = statsIterator.Next()
require.Equal(t, chunkenc.ValFloatHistogram, typ)
_, h = statsIterator.AtFloatHistogram(nil)
actualHints[1] = h.CounterResetHint
typ = statsIterator.Next()
require.Equal(t, chunkenc.ValFloatHistogram, typ)
_, fh := statsIterator.AtFloatHistogram(nil)
actualHints[2] = fh.CounterResetHint
require.Equal(t, chunkenc.ValNone, statsIterator.Next())
require.Equal(t, expectedHints, actualHints)
}
type histogramSeries struct {
histograms []*histogram.Histogram
}
func newHistogramSeries(histograms []*histogram.Histogram) *histogramSeries {
return &histogramSeries{
histograms: histograms,
}
}
func (histogramSeries) Labels() labels.Labels { return labels.EmptyLabels() }
func (m histogramSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
return &histogramIterator{
i: -1,
histograms: m.histograms,
}
}
type histogramIterator struct {
i int
histograms []*histogram.Histogram
}
func (h *histogramIterator) Next() chunkenc.ValueType {
h.i++
if h.i < len(h.histograms) {
return chunkenc.ValHistogram
}
return chunkenc.ValNone
}
func (*histogramIterator) Seek(int64) chunkenc.ValueType { panic("not implemented") }
func (*histogramIterator) At() (int64, float64) { panic("not implemented") }
func (h *histogramIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
return 0, h.histograms[h.i]
}
func (h *histogramIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
return 0, h.histograms[h.i].ToFloat(nil)
}
func (*histogramIterator) AtT() int64 { return 0 }
func (*histogramIterator) Err() error { return nil }