From f3b20d7bdabb68f9d0ae1a624bb2f1e703b159ee Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Tue, 3 Mar 2026 11:03:23 -0500 Subject: [PATCH] test and benchmark updates Signed-off-by: Owen Williams --- tsdb/record/bench_test.go | 215 +++++++++++++++++++++++++++++++++++++ tsdb/record/record.go | 28 +++-- tsdb/record/record_test.go | 88 +++++++++++++++ util/testrecord/record.go | 90 ++++++++++++++++ 4 files changed, 412 insertions(+), 9 deletions(-) diff --git a/tsdb/record/bench_test.go b/tsdb/record/bench_test.go index 37a5019181..fc3d650d2b 100644 --- a/tsdb/record/bench_test.go +++ b/tsdb/record/bench_test.go @@ -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]) + } + }) + } + } + } + } +} diff --git a/tsdb/record/record.go b/tsdb/record/record.go index 417c3ef0be..afcc48fb8c 100644 --- a/tsdb/record/record.go +++ b/tsdb/record/record.go @@ -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] diff --git a/tsdb/record/record_test.go b/tsdb/record/record_test.go index b688e6b294..ab5be5e897 100644 --- a/tsdb/record/record_test.go +++ b/tsdb/record/record_test.go @@ -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{} diff --git a/util/testrecord/record.go b/util/testrecord/record.go index e5071d42c8..ed28e7ec52 100644 --- a/util/testrecord/record.go +++ b/util/testrecord/record.go @@ -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