mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-28 14:07:06 +03:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user