lib/storage: new storage search benchmarks (#9620)

### Describe Your Changes

New benchmarks for storage search (data and index):
- Use the same dataset that accounts for prev and curr indexDBs and
deleted series
- The code is more structured
- Account for various numbers of series in response including higher
numbers (>10k) as this appears to be a quite common use case.

These bechmarks were used for investigating #9602 performance issue and
helped discover that prefetching metric names needed to be restored
#9619.

Signed-off-by: Artem Fetishev <rtm@victoriametrics.com>
This commit is contained in:
Artem Fetishev
2025-08-27 11:19:29 +02:00
committed by GitHub
parent e62e0685dc
commit 9517f5cf1a
2 changed files with 478 additions and 418 deletions

View File

@@ -1,107 +1,78 @@
package storage
import (
"fmt"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/google/go-cmp/cmp"
)
func BenchmarkSearch_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
func BenchmarkSearchData_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, false, benchmarkSearchData)
}
const numRows = 10_000
want := make([]MetricRow, numRows)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
mn := MetricName{
Tags: []Tag{
{[]byte("job"), []byte("webservice")},
{[]byte("instance"), []byte("1.2.3.4")},
},
}
for i := range numRows {
name := fmt.Sprintf("metric_%d", i)
mn.MetricGroup = []byte(name)
want[i].MetricNameRaw = mn.marshalRaw(nil)
want[i].Timestamp = tr.MinTimestamp + int64(i)*step
want[i].Value = float64(i)
}
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(want[:numRows/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(want[numRows/2:], defaultPrecisionBits)
s.DebugFlush()
func BenchmarkSearchData_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchData)
}
tfss := NewTagFilters()
if err := tfss.Add(nil, []byte("metric_.*"), false, true); err != nil {
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
func BenchmarkSearchData_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchData)
}
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
type metricBlock struct {
MetricName []byte
Block *Block
}
var mbs []metricBlock
for range b.N {
mbs = nil
var search Search
search.Init(nil, s, []*TagFilters{tfss}, tr, 1e9, noDeadline)
for search.NextMetricBlock() {
var (
block Block
mb metricBlock
)
search.MetricBlockRef.BlockRef.MustReadBlock(&block)
mb.MetricName = append(mb.MetricName, search.MetricBlockRef.MetricName...)
mb.Block = &block
mbs = append(mbs, mb)
}
if err := search.Error(); err != nil {
b.Fatalf("search error: %v", err)
}
search.MustClose()
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
var got []MetricRow
for _, mb := range mbs {
rb := newTestRawBlock(mb.Block, tr)
if err := mn.Unmarshal(mb.MetricName); err != nil {
b.Fatalf("cannot unmarshal MetricName %v: %v", string(mb.MetricName), err)
}
metricNameRaw := mn.marshalRaw(nil)
for i, timestamp := range rb.Timestamps {
mr := MetricRow{
MetricNameRaw: metricNameRaw,
Timestamp: timestamp,
Value: rb.Values[i],
}
got = append(got, mr)
}
}
testSortMetricRows(got)
testSortMetricRows(want)
if diff := cmp.Diff(mrsToString(want), mrsToString(got)); diff != "" {
b.Errorf("unexpected metric names (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
// benchmarkSearchData is a helper function used in various data search
// benchmarks.
func benchmarkSearchData(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
tfss := NewTagFilters()
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
type metricBlock struct {
MetricName []byte
Block *Block
}
mbs := make([]metricBlock, 0, len(mrs))
for b.Loop() {
mbs = mbs[:0]
var search Search
search.Init(nil, s, []*TagFilters{tfss}, tr, 1e9, noDeadline)
for search.NextMetricBlock() {
var (
block Block
mb metricBlock
)
search.MetricBlockRef.BlockRef.MustReadBlock(&block)
mb.MetricName = append(mb.MetricName, search.MetricBlockRef.MetricName...)
mb.Block = &block
mbs = append(mbs, mb)
}
if err := search.Error(); err != nil {
b.Fatalf("search error: %v", err)
}
search.MustClose()
}
var mn MetricName
got := make([]MetricRow, len(mrs))
for i, mb := range mbs {
rb := newTestRawBlock(mb.Block, tr)
if err := mn.Unmarshal(mb.MetricName); err != nil {
b.Fatalf("cannot unmarshal MetricName %v: %v", string(mb.MetricName), err)
}
metricNameRaw := mn.marshalRaw(nil)
for j, timestamp := range rb.Timestamps {
mr := MetricRow{
MetricNameRaw: metricNameRaw,
Timestamp: timestamp,
Value: rb.Values[j],
}
got[i] = mr
}
}
testSortMetricRows(got)
want := mrs
testSortMetricRows(want)
if diff := cmp.Diff(mrsToString(want), mrsToString(got)); diff != "" {
b.Errorf("unexpected metric rows (-want, +got):\n%s", diff)
}
}

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"sync/atomic"
"testing"
"time"
@@ -101,331 +102,6 @@ func BenchmarkStorageAddRows_VariousTimeRanges(b *testing.B) {
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
func BenchmarkStorageSearchMetricNames_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
const numRows = 10_000
mrs := make([]MetricRow, numRows)
want := make([]string, numRows)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
mn := MetricName{
Tags: []Tag{
{[]byte("job"), []byte("webservice")},
{[]byte("instance"), []byte("1.2.3.4")},
},
}
for i := range numRows {
name := fmt.Sprintf("metric_%d", i)
mn.MetricGroup = []byte(name)
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
want[i] = name
}
slices.Sort(want)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
s.DebugFlush()
tfss := NewTagFilters()
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
var (
got []string
err error
)
for range b.N {
got, err = s.SearchMetricNames(nil, []*TagFilters{tfss}, tr, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
}
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
for i, name := range got {
var mn MetricName
if err := mn.UnmarshalString(name); err != nil {
b.Fatalf("Could not unmarshal metric name %q: %v", name, err)
}
got[i] = string(mn.MetricGroup)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
b.Errorf("unexpected metric names (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
func BenchmarkStorageSearchLabelNames_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
const numRows = 10_000
mrs := make([]MetricRow, numRows)
want := make([]string, numRows)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
mn := MetricName{
MetricGroup: []byte("metric"),
Tags: []Tag{
{
Key: []byte("tbd"),
Value: []byte("value"),
},
},
}
for i := range numRows {
labelName := fmt.Sprintf("label_%d", i)
mn.Tags[0].Key = []byte(labelName)
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
want[i] = labelName
}
want = append(want, "__name__")
slices.Sort(want)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
s.DebugFlush()
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
var (
got []string
err error
)
for range b.N {
got, err = s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchLabelNames() failed unexpectedly: %v", err)
}
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
b.Errorf("unexpected label names (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
func BenchmarkStorageSearchLabelValues_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
const numRows = 10_000
mrs := make([]MetricRow, numRows)
want := make([]string, numRows)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numRows)
mn := MetricName{
MetricGroup: []byte("metric"),
Tags: []Tag{
{
Key: []byte("label"),
Value: []byte("tbd"),
},
},
}
for i := range numRows {
labelValue := fmt.Sprintf("value_%d", i)
mn.Tags[0].Value = []byte(labelValue)
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
want[i] = labelValue
}
slices.Sort(want)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrs[:numRows/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrs[numRows/2:], defaultPrecisionBits)
s.DebugFlush()
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
var (
got []string
err error
)
for range b.N {
got, err = s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchLabelValues() failed unexpectedly: %v", err)
}
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
b.Errorf("unexpected label values (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
func BenchmarkStorageSearchTagValueSuffixes_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
const numMetrics = 10_000
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("prefix.metric%04d", i)
mn := MetricName{MetricGroup: []byte(name)}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
want[i] = fmt.Sprintf("metric%04d", i)
}
slices.Sort(want)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrs[:numMetrics/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrs[numMetrics/2:], defaultPrecisionBits)
s.DebugFlush()
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
var (
got []string
err error
)
for range b.N {
got, err = s.SearchTagValueSuffixes(nil, tr, "", "prefix.", '.', 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchTagValueSuffixes() failed unexpectedly: %v", err)
}
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected tag value suffixes (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
func BenchmarkStorageSearchGraphitePaths_VariousTimeRanges(b *testing.B) {
f := func(b *testing.B, tr TimeRange) {
b.Helper()
const numMetrics = 10_000
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("prefix.metric%04d", i)
mn := MetricName{MetricGroup: []byte(name)}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
want[i] = name
}
slices.Sort(want)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrs[:numMetrics/2], defaultPrecisionBits)
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrs[numMetrics/2:], defaultPrecisionBits)
s.DebugFlush()
// Reset timer to exclude expensive initialization from measurement.
b.ResetTimer()
var (
got []string
err error
)
for range b.N {
got, err = s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchGraphitePaths() failed unexpectedly: %v", err)
}
}
// Stop timer to exclude expensive correctness check and cleanup from
// measurement.
b.StopTimer()
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected graphite paths (-want, +got):\n%s", diff)
}
s.MustClose()
fs.MustRemoveDir(b.Name())
// Start timer again to conclude the benchmark correctly.
b.StartTimer()
}
benchmarkStorageOpOnVariousTimeRanges(b, f)
}
// benchmarkStorageOpOnVariousTimeRanges measures the execution time of some
// storage operation on various time ranges: 1h, 1d, 1m, etc.
func benchmarkStorageOpOnVariousTimeRanges(b *testing.B, op func(b *testing.B, tr TimeRange)) {
@@ -612,3 +288,416 @@ func benchmarkDirSize(path string) int64 {
}
return size
}
func BenchmarkSearchMetricNames_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, false, benchmarkSearchMetricNames)
}
func BenchmarkSearchMetricNames_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchMetricNames)
}
func BenchmarkSearchMetricNames_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchMetricNames)
}
func BenchmarkSearchLabelNames_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, false, benchmarkSearchLabelNames)
}
func BenchmarkSearchLabelNames_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchLabelNames)
}
func BenchmarkSearchLabelNames_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchLabelNames)
}
func BenchmarkSearchLabelValues_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, false, benchmarkSearchLabelValues)
}
func BenchmarkSearchLabelValues_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, false, benchmarkSearchLabelValues)
}
func BenchmarkSearchLabelValues_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, false, benchmarkSearchLabelValues)
}
func BenchmarkSearchTagValueSuffixes_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, true, benchmarkSearchTagValueSuffixes)
}
func BenchmarkSearchTagValueSuffixes_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, true, benchmarkSearchTagValueSuffixes)
}
func BenchmarkSearchTagValueSuffixes_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, true, benchmarkSearchTagValueSuffixes)
}
func BenchmarkSearchGraphitePaths_variableSeries(b *testing.B) {
benchmarkSearch_variableSeries(b, true, benchmarkSearchGraphitePaths)
}
func BenchmarkSearchGraphitePaths_variableDeletedSeries(b *testing.B) {
benchmarkSearch_variableDeletedSeries(b, true, benchmarkSearchGraphitePaths)
}
func BenchmarkSearchGraphitePaths_variableTimeRange(b *testing.B) {
benchmarkSearch_variableTimeRange(b, true, benchmarkSearchGraphitePaths)
}
// benchmarkSearch_variableSeries measures the execution time of some search
// operation on a fixed time trange and variable number of series. The number of
// deleted series is 0.
func benchmarkSearch_variableSeries(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
const numDeletedSeries = 0
tr := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
// Using only a few numbers that represent orders of magnitude so that
// routine running of the benchmarks does not take too long. However, when
// debugging it is often helpful to add more numbers in between these
// numbers.
for _, numSeries := range []int{100, 1000, 10_000, 100_000, 1_000_000} {
name := fmt.Sprintf("%d", numSeries)
b.Run(name, func(b *testing.B) {
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
})
}
}
// benchmarkSearch_variableDeletedSeries measures the execution time of some
// storage operation on a fixed time, fixed number of series and variable
// number of deleted series.
func benchmarkSearch_variableDeletedSeries(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
// Deployments that we aware of often have tens and hundreds of thouthands
// series in their query results, sometimes even millions. Chosen 100K as
// something in the middle.
const numSeries = 100_000
tr := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
for _, numDeletedSeries := range []int{100, 1000, 10_000, 100_000, 1_000_000} {
name := fmt.Sprintf("%d-%d", numSeries, numDeletedSeries)
b.Run(name, func(b *testing.B) {
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
})
}
}
// benchmarkSearch_variableTimeRange measures the execution time of some search
// operation on various time trages and fixed number of series. The number of
// deleted series is 0.
func benchmarkSearch_variableTimeRange(b *testing.B, graphite bool, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
const (
numSeries = 100
numDeletedSeries = 0
)
tr1d := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
tr1w := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 7, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
tr1m := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
tr2m := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 2, 28, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
tr6m := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
trNames := []string{"1d", "1w", "1m", "2m", "6m"}
for i, tr := range []TimeRange{tr1d, tr1w, tr2m, tr1m, tr6m} {
name := trNames[i]
b.Run(name, func(b *testing.B) {
benchmarkSearch(b, graphite, numSeries, numDeletedSeries, tr, op)
})
}
}
// benchmarkSearchMetricNames is a helper function used in various
// SearchMetricNames benchmarks.
func benchmarkSearchMetricNames(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
tfss := NewTagFilters()
if err := tfss.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
var (
got []string
err error
)
for b.Loop() {
got, err = s.SearchMetricNames(nil, []*TagFilters{tfss}, tr, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
}
}
var mn MetricName
for i, name := range got {
if err := mn.UnmarshalString(name); err != nil {
b.Fatalf("Could not unmarshal metric name %q: %v", name, err)
}
got[i] = string(mn.MetricGroup)
}
slices.Sort(got)
want := make([]string, len(mrs))
for i, mr := range mrs {
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
b.Fatalf("could not unmarshal metric row: %v", err)
}
want[i] = string(mn.MetricGroup)
}
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
}
// benchmarkSearchLabelNames is a helper function used in various
// SearchLabelNames benchmarks.
func benchmarkSearchLabelNames(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
var (
got []string
err error
)
for b.Loop() {
got, err = s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchLabelNames() failed unexpectedly: %v", err)
}
}
slices.Sort(got)
var mn MetricName
want := make([]string, len(mrs))
for i, mr := range mrs {
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
b.Fatalf("could not unmarshal metric row: %v", err)
}
for _, tag := range mn.Tags {
labelName := string(tag.Key)
if labelName != "label" {
want[i] = labelName
}
}
}
want = append(want, "__name__", "label")
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected label names (-want, +got):\n%s", diff)
}
}
// benchmarkSearchLabelValues is a helper function used in various
// SearchLabelValues benchmarks.
func benchmarkSearchLabelValues(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
var (
got []string
err error
)
for b.Loop() {
got, err = s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchLabelValues() failed unexpectedly: %v", err)
}
}
slices.Sort(got)
want := make([]string, len(mrs))
for i, mr := range mrs {
var mn MetricName
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
b.Fatalf("could not unmarshal metric row: %v", err)
}
for _, tag := range mn.Tags {
if string(tag.Key) == "label" {
want[i] = string(tag.Value)
}
}
}
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected label values (-want, +got):\n%s", diff)
}
}
// benchmarkSearchTagValueSuffixes is a helper function used in various
// SearchTagValueSuffixes benchmarks.
func benchmarkSearchTagValueSuffixes(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
var (
prefix = "graphite."
got []string
err error
)
for b.Loop() {
got, err = s.SearchTagValueSuffixes(nil, tr, "", prefix, '.', 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchTagValueSuffixes() failed unexpectedly: %v", err)
}
}
slices.Sort(got)
want := make([]string, len(mrs))
for i, mr := range mrs {
var mn MetricName
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
b.Fatalf("could not unmarshal metric row: %v", err)
}
var found bool
metricName := string(mn.MetricGroup)
want[i], found = strings.CutPrefix(metricName, prefix)
if !found {
b.Fatalf("metric name %q does not have %q prefix", metricName, prefix)
}
}
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected tag value suffixes (-want, +got):\n%s", diff)
}
}
// benchmarkSearchGraphitePaths is a helper function used in various
// SearchGraphitePaths benchmarks.
func benchmarkSearchGraphitePaths(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow) {
b.Helper()
var (
got []string
err error
)
for b.Loop() {
got, err = s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
if err != nil {
b.Fatalf("SearchGraphitePaths() failed unexpectedly: %v", err)
}
}
slices.Sort(got)
want := make([]string, len(mrs))
for i, mr := range mrs {
var mn MetricName
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
b.Fatalf("could not unmarshal metric row: %v", err)
}
want[i] = string(mn.MetricGroup)
}
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
b.Fatalf("unexpected graphite paths (-want, +got):\n%s", diff)
}
}
// benchmarkSearch implements the core logic of benchmark of a search operation.
//
// It generates the test data, inserts it into the storage and runs the search
// operation against it. The index data is split evenly across prev and curr
// indexDBs.
//
// The number of series is controlled with numSeries.
//
// The function also generates the deleted series and saves them to the storage.
// If the deleted series are not needed, set numDeletedSeries to 0.
//
// The data is spread evenly across the provided time range.
//
// The test data is designed so that it can be reused by all types of search
// operations. It is also passes to the search op callback to that the search
// operation could perform all necessary assertions to make sure that the search
// result is correct.
func benchmarkSearch(b *testing.B, graphite bool, numSeries, numDeletedSeries int, tr TimeRange, op func(b *testing.B, s *Storage, tr TimeRange, mrs []MetricRow)) {
b.Helper()
graphitePrefix := ""
if graphite {
graphitePrefix = "graphite."
}
genRows := func(n int, prefix string, tr TimeRange) []MetricRow {
mrs := make([]MetricRow, n)
if n == 0 {
return mrs
}
step := (tr.MaxTimestamp - tr.MinTimestamp) / int64(n)
for i := range n {
name := fmt.Sprintf("%s%s_%09d", graphitePrefix, prefix, i)
labelName := fmt.Sprintf("%s_label_%09d", prefix, i)
labelValue := fmt.Sprintf("%s_value_%09d", prefix, i)
mn := MetricName{
MetricGroup: []byte(name),
Tags: []Tag{
{[]byte(labelName), []byte("value")},
{[]byte("label"), []byte(labelValue)},
},
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = tr.MinTimestamp + int64(i)*step
mrs[i].Value = float64(i)
}
return mrs
}
deleteSeries := func(s *Storage, prefix string, want int) {
b.Helper()
tfs := NewTagFilters()
re := fmt.Sprintf(`%s%s.*`, graphitePrefix, prefix)
if err := tfs.Add(nil, []byte(re), false, true); err != nil {
b.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
got, err := s.DeleteSeries(nil, []*TagFilters{tfs}, 1e9)
if err != nil {
b.Fatalf("could not delete series unexpectedly: %v", err)
}
if got != want {
b.Fatalf("unexpected number of deleted series: got %d, want %d", got, want)
}
}
trPrev := TimeRange{
MinTimestamp: tr.MinTimestamp,
MaxTimestamp: tr.MinTimestamp + (tr.MaxTimestamp-tr.MinTimestamp)/2,
}
trCurr := TimeRange{
MinTimestamp: tr.MinTimestamp + (tr.MaxTimestamp-tr.MinTimestamp)/2 + 1,
MaxTimestamp: tr.MaxTimestamp,
}
numDeletedSeriesPrev := numDeletedSeries / 2
mrsToDeletePrev := genRows(numDeletedSeriesPrev, "prev", trPrev)
mrsPrev := genRows(numSeries/2, "prev", trPrev)
numDeletedSeriesCurr := numDeletedSeries / 2
mrsToDeleteCurr := genRows(numDeletedSeriesCurr, "curr", trCurr)
mrsCurr := genRows(numSeries/2, "curr", trCurr)
s := MustOpenStorage(b.Name(), OpenOptions{})
s.AddRows(mrsToDeletePrev, defaultPrecisionBits)
s.DebugFlush()
deleteSeries(s, "prev", numDeletedSeriesPrev)
s.DebugFlush()
s.AddRows(mrsPrev, defaultPrecisionBits)
s.DebugFlush()
// Rotate the indexDB to ensure that the search operation covers both current and prev indexDBs.
s.mustRotateIndexDB(time.Now())
s.AddRows(mrsToDeleteCurr, defaultPrecisionBits)
s.DebugFlush()
deleteSeries(s, "curr", numDeletedSeriesCurr)
s.DebugFlush()
s.AddRows(mrsCurr, defaultPrecisionBits)
s.DebugFlush()
mrs := slices.Concat(mrsPrev, mrsCurr)
op(b, s, tr, mrs)
s.MustClose()
_ = os.RemoveAll(b.Name())
}