test and benchmark updates

Signed-off-by: Owen Williams <owen.williams@grafana.com>
This commit is contained in:
Owen Williams 2026-03-03 11:03:23 -05:00
parent 049827042a
commit f3b20d7bda
No known key found for this signature in database
GPG key ID: 711C61A216D34A69
4 changed files with 412 additions and 9 deletions

View file

@ -217,3 +217,218 @@ func BenchmarkDecode_Samples(b *testing.B) {
}
}
}
var (
histDataCases = testrecord.HistDataCases
histCounts = testrecord.HistCounts
)
/*
go test ./tsdb/record/... \
-run '^$' -bench '^BenchmarkEncode_Histograms' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee encode-hist.txt
benchstat -col /version encode-hist.txt
*/
func BenchmarkEncode_Histograms(b *testing.B) {
for _, ver := range versions {
for _, compr := range compressions {
for _, hcase := range histDataCases {
for _, count := range histCounts {
b.Run(fmt.Sprintf("version=%s/compr=%v/type=%s/n=%d", ver.name, compr, hcase.Name, count), func(b *testing.B) {
var (
samples = hcase.Gen(count, ver.enableST)
enc = record.Encoder{EnableSTStorage: ver.enableST}
buf []byte
cBuf []byte
)
cEnc, err := compression.NewEncoder()
require.NoError(b, err)
// Warm up.
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsHistogramSamples(samples, buf[:0])
} else {
buf, _ = enc.HistogramSamples(samples, buf[:0])
}
cBuf, _, err = cEnc.Encode(compr, buf, cBuf[:0])
require.NoError(b, err)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsHistogramSamples(samples, buf[:0])
} else {
buf, _ = enc.HistogramSamples(samples, buf[:0])
}
b.ReportMetric(float64(len(buf)), "B/rec")
cBuf, _, _ = cEnc.Encode(compr, buf, cBuf[:0])
b.ReportMetric(float64(len(cBuf)), "B/compressed-rec")
}
})
}
}
}
}
}
/*
go test ./tsdb/record/... \
-run '^$' -bench '^BenchmarkDecode_Histograms' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee decode-hist.txt
benchstat -col /version decode-hist.txt
*/
func BenchmarkDecode_Histograms(b *testing.B) {
for _, ver := range versions {
for _, compr := range compressions {
for _, hcase := range histDataCases {
for _, count := range histCounts {
b.Run(fmt.Sprintf("version=%s/compr=%v/type=%s/n=%d", ver.name, compr, hcase.Name, count), func(b *testing.B) {
var (
samples = hcase.Gen(count, ver.enableST)
enc = record.Encoder{EnableSTStorage: ver.enableST}
dec record.Decoder
cDec = compression.NewDecoder()
cBuf []byte
samplesBuf []record.RefHistogramSample
)
var buf []byte
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsHistogramSamples(samples, nil)
} else {
buf, _ = enc.HistogramSamples(samples, nil)
}
cEnc, err := compression.NewEncoder()
require.NoError(b, err)
buf, _, err = cEnc.Encode(compr, buf, nil)
require.NoError(b, err)
// Warm up.
cBuf, err = cDec.Decode(compr, buf, cBuf[:0])
require.NoError(b, err)
samplesBuf, err = dec.HistogramSamples(cBuf, samplesBuf[:0])
require.NoError(b, err)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
cBuf, _ = cDec.Decode(compr, buf, cBuf[:0])
samplesBuf, _ = dec.HistogramSamples(cBuf, samplesBuf[:0])
}
})
}
}
}
}
}
/*
go test ./tsdb/record/... \
-run '^$' -bench '^BenchmarkEncode_FloatHistograms' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee encode-fhist.txt
benchstat -col /version encode-fhist.txt
*/
func BenchmarkEncode_FloatHistograms(b *testing.B) {
for _, ver := range versions {
for _, compr := range compressions {
for _, hcase := range histDataCases {
for _, count := range histCounts {
b.Run(fmt.Sprintf("version=%s/compr=%v/type=%s/n=%d", ver.name, compr, hcase.Name, count), func(b *testing.B) {
var (
samples = testrecord.GenFloatHistograms(hcase.Gen(count, ver.enableST))
enc = record.Encoder{EnableSTStorage: ver.enableST}
buf []byte
cBuf []byte
)
cEnc, err := compression.NewEncoder()
require.NoError(b, err)
// Warm up.
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsFloatHistogramSamples(samples, buf[:0])
} else {
buf, _ = enc.FloatHistogramSamples(samples, buf[:0])
}
cBuf, _, err = cEnc.Encode(compr, buf, cBuf[:0])
require.NoError(b, err)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsFloatHistogramSamples(samples, buf[:0])
} else {
buf, _ = enc.FloatHistogramSamples(samples, buf[:0])
}
b.ReportMetric(float64(len(buf)), "B/rec")
cBuf, _, _ = cEnc.Encode(compr, buf, cBuf[:0])
b.ReportMetric(float64(len(cBuf)), "B/compressed-rec")
}
})
}
}
}
}
}
/*
go test ./tsdb/record/... \
-run '^$' -bench '^BenchmarkDecode_FloatHistograms' \
-benchtime 5s -count 6 -cpu 2 -timeout 999m \
| tee decode-fhist.txt
benchstat -col /version decode-fhist.txt
*/
func BenchmarkDecode_FloatHistograms(b *testing.B) {
for _, ver := range versions {
for _, compr := range compressions {
for _, hcase := range histDataCases {
for _, count := range histCounts {
b.Run(fmt.Sprintf("version=%s/compr=%v/type=%s/n=%d", ver.name, compr, hcase.Name, count), func(b *testing.B) {
var (
samples = testrecord.GenFloatHistograms(hcase.Gen(count, ver.enableST))
enc = record.Encoder{EnableSTStorage: ver.enableST}
dec record.Decoder
cDec = compression.NewDecoder()
cBuf []byte
samplesBuf []record.RefFloatHistogramSample
)
var buf []byte
if hcase.Name == "nhcb" {
buf = enc.CustomBucketsFloatHistogramSamples(samples, nil)
} else {
buf, _ = enc.FloatHistogramSamples(samples, nil)
}
cEnc, err := compression.NewEncoder()
require.NoError(b, err)
buf, _, err = cEnc.Encode(compr, buf, nil)
require.NoError(b, err)
// Warm up.
cBuf, err = cDec.Decode(compr, buf, cBuf[:0])
require.NoError(b, err)
samplesBuf, err = dec.FloatHistogramSamples(cBuf, samplesBuf[:0])
require.NoError(b, err)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
cBuf, _ = cDec.Decode(compr, buf, cBuf[:0])
samplesBuf, _ = dec.FloatHistogramSamples(cBuf, samplesBuf[:0])
}
})
}
}
}
}
}

View file

@ -1119,12 +1119,17 @@ func (*Encoder) histogramSamplesV2(histograms []RefHistogramSample, b []byte) ([
var customBucketHistograms []RefHistogramSample
// First sample: full varint values, no deltas, no ST marker.
first := histograms[0]
buf.PutVarint64(int64(first.Ref))
buf.PutVarint64(first.T)
buf.PutVarint64(first.ST)
EncodeHistogram(&buf, first.H)
// First sample: full varint values, no deltas, no ST marker.
if first.H.UsesCustomBuckets() {
customBucketHistograms = append(customBucketHistograms, first)
} else {
buf.PutVarint64(int64(first.Ref))
buf.PutVarint64(first.T)
buf.PutVarint64(first.ST)
EncodeHistogram(&buf, first.H)
}
// Subsequent samples: ref delta to prev, T delta to first, ST marker.
for i := 1; i < len(histograms); i++ {
@ -1321,10 +1326,15 @@ func (*Encoder) floatHistogramSamplesV2(histograms []RefFloatHistogramSample, b
var customBucketsFloatHistograms []RefFloatHistogramSample
first := histograms[0]
buf.PutVarint64(int64(first.Ref))
buf.PutVarint64(first.T)
buf.PutVarint64(first.ST)
EncodeFloatHistogram(&buf, first.FH)
if first.FH.UsesCustomBuckets() {
customBucketsFloatHistograms = append(customBucketsFloatHistograms, first)
} else {
buf.PutVarint64(int64(first.Ref))
buf.PutVarint64(first.T)
buf.PutVarint64(first.ST)
EncodeFloatHistogram(&buf, first.FH)
}
for i := 1; i < len(histograms); i++ {
h := histograms[i]

View file

@ -485,6 +485,94 @@ func TestRecord_EncodeDecode(t *testing.T) {
require.Equal(t, gaugeFloatHistsV2, decFloatHistsV2)
})
for _, enableSTStorage := range []bool{false, true} {
t.Run(fmt.Sprintf("int-histogram empty slice stStorage=%v", enableSTStorage), func(t *testing.T) {
enc := Encoder{EnableSTStorage: enableSTStorage}
histBuf, customBuckets := enc.HistogramSamples(nil, nil)
require.Nil(t, customBuckets)
decoded, err := dec.HistogramSamples(histBuf, nil)
require.NoError(t, err)
require.Empty(t, decoded)
})
t.Run(fmt.Sprintf("float-histogram empty slice stStorage=%v", enableSTStorage), func(t *testing.T) {
enc := Encoder{EnableSTStorage: enableSTStorage}
floatBuf, customBucketsFloat := enc.FloatHistogramSamples(nil, nil)
require.Nil(t, customBucketsFloat)
decoded, err := dec.FloatHistogramSamples(floatBuf, nil)
require.NoError(t, err)
require.Empty(t, decoded)
})
}
// When all histograms are custom-bucket, HistogramSamples must return an
// empty buffer (buf.Reset path) and pass every sample through as custom.
t.Run("V2 int-histogram all custom bucket", func(t *testing.T) {
allCustom := []RefHistogramSample{
{Ref: 56, T: 1234, ST: 1000, H: histograms[2].H},
{Ref: 67, T: 5678, ST: 1000, H: histograms[2].H},
}
histBuf, customBuckets := enc.HistogramSamples(allCustom, nil)
require.Empty(t, histBuf, "regular histogram buffer must be empty when all samples are custom bucket")
require.Equal(t, allCustom, customBuckets)
customBuf := enc.CustomBucketsHistogramSamples(customBuckets, nil)
decoded, err := dec.HistogramSamples(customBuf, nil)
require.NoError(t, err)
require.Equal(t, allCustom, decoded)
})
t.Run("V2 float-histogram all custom bucket", func(t *testing.T) {
allCustomFloat := []RefFloatHistogramSample{
{Ref: 56, T: 1234, ST: 1000, FH: histograms[2].H.ToFloat(nil)},
{Ref: 67, T: 5678, ST: 1000, FH: histograms[2].H.ToFloat(nil)},
}
floatBuf, customBucketsFloat := enc.FloatHistogramSamples(allCustomFloat, nil)
require.Empty(t, floatBuf, "regular float histogram buffer must be empty when all samples are custom bucket")
require.Equal(t, allCustomFloat, customBucketsFloat)
customFloatBuf := enc.CustomBucketsFloatHistogramSamples(customBucketsFloat, nil)
decoded, err := dec.FloatHistogramSamples(customFloatBuf, nil)
require.NoError(t, err)
require.Equal(t, allCustomFloat, decoded)
})
// When all histograms are custom-bucket, V1 HistogramSamples must return an
// empty buffer (buf.Reset path) and pass every sample through as custom.
t.Run("V1 int-histogram all custom bucket", func(t *testing.T) {
encV1 := Encoder{}
allCustom := []RefHistogramSample{
{Ref: 56, T: 1234, H: histograms[2].H},
{Ref: 67, T: 5678, H: histograms[2].H},
}
histBuf, customBuckets := encV1.HistogramSamples(allCustom, nil)
require.Empty(t, histBuf, "regular histogram buffer must be empty when all samples are custom bucket")
require.Equal(t, allCustom, customBuckets)
customBuf := encV1.CustomBucketsHistogramSamples(customBuckets, nil)
decoded, err := dec.HistogramSamples(customBuf, nil)
require.NoError(t, err)
require.Equal(t, allCustom, decoded)
})
t.Run("V1 float-histogram all custom bucket", func(t *testing.T) {
encV1 := Encoder{}
allCustomFloat := []RefFloatHistogramSample{
{Ref: 56, T: 1234, FH: histograms[2].H.ToFloat(nil)},
{Ref: 67, T: 5678, FH: histograms[2].H.ToFloat(nil)},
}
floatBuf, customBucketsFloat := encV1.FloatHistogramSamples(allCustomFloat, nil)
require.Empty(t, floatBuf, "regular float histogram buffer must be empty when all samples are custom bucket")
require.Equal(t, allCustomFloat, customBucketsFloat)
customFloatBuf := encV1.CustomBucketsFloatHistogramSamples(customBucketsFloat, nil)
decoded, err := dec.FloatHistogramSamples(customFloatBuf, nil)
require.NoError(t, err)
require.Equal(t, allCustomFloat, decoded)
})
// Backward compat: V1-encoded histograms decode with ST=0.
t.Run("V1 backward compat int-histogram ST=0", func(t *testing.T) {
encV1 := Encoder{}

View file

@ -17,6 +17,7 @@ import (
"math"
"testing"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/tsdb/chunks"
"github.com/prometheus/prometheus/tsdb/record"
)
@ -81,6 +82,95 @@ func GenTestRefSamplesCase(t testing.TB, c RefSamplesCase) []record.RefSample {
return ret
}
// GenExpHistograms generates n standard exponential histograms (schema=1)
// with incrementing refs, same timestamp, and realistic bucket distributions.
// If withST is true, all samples get a constant ST (simulating sameST marker path).
func GenExpHistograms(n int, withST bool) []record.RefHistogramSample {
out := make([]record.RefHistogramSample, n)
for i := range out {
out[i] = record.RefHistogramSample{
Ref: chunks.HeadSeriesRef(i),
T: 1709000000 + int64(i)*15,
H: &histogram.Histogram{
Count: uint64(10 + i%100),
ZeroCount: uint64(1 + i%5),
ZeroThreshold: 0.001,
Sum: float64(100+i) * 1.5,
Schema: 1,
PositiveSpans: []histogram.Span{
{Offset: 0, Length: 4},
{Offset: 2, Length: 3},
},
PositiveBuckets: []int64{1, 2, -1, 0, 3, -2, 1},
NegativeSpans: []histogram.Span{
{Offset: 0, Length: 2},
{Offset: 1, Length: 2},
},
NegativeBuckets: []int64{1, 1, -1, 0},
},
}
if withST {
out[i].ST = 1709000000
}
}
return out
}
// GenCustomBucketHistograms generates n custom-bucket (NHCB) histograms (schema=-53)
// with incrementing refs. If withST is true, all samples get a constant ST.
func GenCustomBucketHistograms(n int, withST bool) []record.RefHistogramSample {
out := make([]record.RefHistogramSample, n)
for i := range out {
out[i] = record.RefHistogramSample{
Ref: chunks.HeadSeriesRef(i),
T: 1709000000 + int64(i)*15,
H: &histogram.Histogram{
Count: uint64(10 + i%100),
Sum: float64(100+i) * 1.5,
Schema: histogram.CustomBucketsSchema,
PositiveSpans: []histogram.Span{
{Offset: 0, Length: 8},
},
PositiveBuckets: []int64{5, -2, 3, -1, 4, 0, -3, 2},
CustomValues: []float64{0.001, 0.01, 0.1, 1, 10, 100, 1000},
},
}
if withST {
out[i].ST = 1709000000
}
}
return out
}
// GenFloatHistograms converts int histograms to float histograms, preserving ST.
func GenFloatHistograms(src []record.RefHistogramSample) []record.RefFloatHistogramSample {
out := make([]record.RefFloatHistogramSample, len(src))
for i, h := range src {
out[i] = record.RefFloatHistogramSample{
Ref: h.Ref,
ST: h.ST,
T: h.T,
FH: h.H.ToFloat(nil),
}
}
return out
}
// HistDataCase pairs a name with a histogram generator for benchmark tables.
type HistDataCase struct {
Name string
Gen func(n int, withST bool) []record.RefHistogramSample
}
// HistDataCases is the standard set of histogram data cases for benchmarks.
var HistDataCases = []HistDataCase{
{"exp", GenExpHistograms},
{"nhcb", GenCustomBucketHistograms},
}
// HistCounts is the standard set of histogram counts for benchmarks.
var HistCounts = []int{10, 100, 1000}
func highVarianceInt(i int) int64 {
if i%2 == 0 {
return math.MinInt32