github.com/grafana/pyroscope@v1.18.0/pkg/querybackend/query_time_series_test.go (about) 1 package querybackend 2 3 import ( 4 "bytes" 5 "context" 6 "os" 7 "path/filepath" 8 "slices" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 "google.golang.org/grpc/codes" 15 "google.golang.org/grpc/status" 16 17 metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1" 18 queryv1 "github.com/grafana/pyroscope/api/gen/proto/go/query/v1" 19 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 20 "github.com/grafana/pyroscope/pkg/block" 21 "github.com/grafana/pyroscope/pkg/block/metadata" 22 "github.com/grafana/pyroscope/pkg/objstore" 23 "github.com/grafana/pyroscope/pkg/objstore/providers/memory" 24 "github.com/grafana/pyroscope/pkg/querybackend/queryplan" 25 "github.com/grafana/pyroscope/pkg/test" 26 ) 27 28 func TestValidateExemplarType(t *testing.T) { 29 tests := []struct { 30 name string 31 exemplarType typesv1.ExemplarType 32 expectedInclude bool 33 expectedErrorMsg string 34 expectedCode codes.Code 35 }{ 36 { 37 name: "UNSPECIFIED returns false, no error", 38 exemplarType: typesv1.ExemplarType_EXEMPLAR_TYPE_UNSPECIFIED, 39 expectedInclude: false, 40 }, 41 { 42 name: "NONE returns false, no error", 43 exemplarType: typesv1.ExemplarType_EXEMPLAR_TYPE_NONE, 44 expectedInclude: false, 45 }, 46 { 47 name: "INDIVIDUAL returns true, no error", 48 exemplarType: typesv1.ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL, 49 expectedInclude: true, 50 }, 51 { 52 name: "SPAN returns error with Unimplemented code", 53 exemplarType: typesv1.ExemplarType_EXEMPLAR_TYPE_SPAN, 54 expectedInclude: false, 55 expectedErrorMsg: "exemplar type span is not implemented", 56 expectedCode: codes.Unimplemented, 57 }, 58 { 59 name: "Unknown type returns error with InvalidArgument code", 60 exemplarType: typesv1.ExemplarType(999), 61 expectedInclude: false, 62 expectedErrorMsg: "unknown exemplar type", 63 expectedCode: codes.InvalidArgument, 64 }, 65 } 66 67 for _, tt := range tests { 68 t.Run(tt.name, func(t *testing.T) { 69 include, err := validateExemplarType(tt.exemplarType) 70 if tt.expectedErrorMsg != "" { 71 require.Error(t, err) 72 assert.Contains(t, err.Error(), tt.expectedErrorMsg) 73 st, ok := status.FromError(err) 74 require.True(t, ok) 75 assert.Equal(t, tt.expectedCode, st.Code()) 76 } else { 77 require.NoError(t, err) 78 assert.Equal(t, tt.expectedInclude, include) 79 } 80 }) 81 } 82 } 83 84 type benchmarkFixture struct { 85 ctx context.Context 86 reader *BlockReader 87 plan *queryv1.QueryPlan 88 tenant []string 89 startTime time.Time 90 } 91 92 // setupBenchmarkFixture creates a benchmark fixture with real block data. 93 func setupBenchmarkFixture(b *testing.B) *benchmarkFixture { 94 b.Helper() 95 96 bucket := memory.NewInMemBucket() 97 var blocks []*metastorev1.BlockMeta 98 99 err := filepath.WalkDir("testdata/samples", func(path string, e os.DirEntry, err error) error { 100 if err != nil || e.IsDir() { 101 return err 102 } 103 data, err := os.ReadFile(path) 104 if err != nil { 105 return err 106 } 107 var md metastorev1.BlockMeta 108 if err = metadata.Decode(data, &md); err != nil { 109 return err 110 } 111 md.Size = uint64(len(data)) 112 blocks = append(blocks, &md) 113 return bucket.Upload(context.Background(), block.ObjectPath(&md), bytes.NewReader(data)) 114 }) 115 if err != nil { 116 b.Fatalf("failed to load test data: %v", err) 117 } 118 119 logger := test.NewTestingLogger(b) 120 reader := NewBlockReader(logger, &objstore.ReaderAtBucket{Bucket: bucket}, nil) 121 122 meta := make([]*metastorev1.BlockMeta, len(blocks)) 123 for i, block := range blocks { 124 meta[i] = block.CloneVT() 125 } 126 sanitizeMetadata(meta) 127 128 plan := queryplan.Build(meta, 10, 10) 129 130 var tenant []string 131 for _, b := range plan.Root.Blocks { 132 for _, d := range b.Datasets { 133 tenant = append(tenant, b.StringTable[d.Tenant]) 134 } 135 } 136 137 // Extract start time from blocks 138 var minTime int64 = -1 139 for _, block := range blocks { 140 if minTime == -1 || block.MinTime < minTime { 141 minTime = block.MinTime 142 } 143 } 144 startTime := time.UnixMilli(minTime) 145 146 return &benchmarkFixture{ 147 ctx: context.Background(), 148 reader: reader, 149 plan: plan, 150 tenant: tenant, 151 startTime: startTime, 152 } 153 } 154 155 // sanitizeMetadata removes duplicate datasets (logic from testSuite.sanitizeMetadata) 156 func sanitizeMetadata(meta []*metastorev1.BlockMeta) { 157 for _, m := range meta { 158 for _, d := range m.Datasets { 159 if block.DatasetFormat(d.Format) == block.DatasetFormat1 { 160 m.Datasets = slices.DeleteFunc(m.Datasets, func(x *metastorev1.Dataset) bool { 161 return x.Format == 0 162 }) 163 break 164 } 165 } 166 } 167 } 168 169 // runTimeSeriesQuery executes a timeseries query with the given parameters. 170 func (f *benchmarkFixture) runTimeSeriesQuery(b *testing.B, req *queryv1.InvokeRequest) { 171 b.Helper() 172 resp, err := f.reader.Invoke(f.ctx, req) 173 if err != nil { 174 b.Fatalf("query failed: %v", err) 175 } 176 for _, r := range resp.Reports { 177 if r.ReportType != queryv1.ReportType_REPORT_TIME_SERIES { 178 continue 179 } 180 for _, s := range r.TimeSeries.TimeSeries { 181 for _, p := range s.Points { 182 if p.Value > 0 { 183 return 184 } 185 } 186 } 187 } 188 panic("no data found") 189 } 190 191 // makeTimeSeriesRequest creates a timeseries query request with the given parameters. 192 func (f *benchmarkFixture) makeTimeSeriesRequest( 193 startTime, endTime time.Time, 194 labelSelector string, 195 groupBy []string, 196 exemplarType typesv1.ExemplarType, 197 ) *queryv1.InvokeRequest { 198 return &queryv1.InvokeRequest{ 199 StartTime: startTime.UnixMilli(), 200 EndTime: endTime.UnixMilli(), 201 LabelSelector: labelSelector, 202 QueryPlan: f.plan, 203 Query: []*queryv1.Query{{ 204 QueryType: queryv1.QueryType_QUERY_TIME_SERIES, 205 TimeSeries: &queryv1.TimeSeriesQuery{ 206 Step: 60.0, // 1 minute resolution 207 GroupBy: groupBy, 208 ExemplarType: exemplarType, 209 }, 210 }}, 211 Tenant: f.tenant, 212 } 213 } 214 215 // BenchmarkTimeSeriesQuery measures the performance impact of exemplar collection. 216 // 217 // go test -bench=BenchmarkTimeSeriesQuery$ -benchmem ./pkg/querybackend/ 218 // 219 // Expected results: Exemplar overhead should be < 30% for typical queries. 220 func BenchmarkTimeSeriesQuery(b *testing.B) { 221 fixture := setupBenchmarkFixture(b) 222 223 benchmarks := []struct { 224 name string 225 exemplarType typesv1.ExemplarType 226 }{ 227 {"NoExemplars", typesv1.ExemplarType_EXEMPLAR_TYPE_NONE}, 228 {"WithExemplars", typesv1.ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL}, 229 } 230 231 for _, bm := range benchmarks { 232 b.Run(bm.name, func(b *testing.B) { 233 req := fixture.makeTimeSeriesRequest( 234 fixture.startTime, fixture.startTime.Add(time.Hour), 235 "{}", 236 []string{"service_name"}, 237 bm.exemplarType, 238 ) 239 240 b.ResetTimer() 241 for i := 0; i < b.N; i++ { 242 fixture.runTimeSeriesQuery(b, req) 243 } 244 }) 245 } 246 } 247 248 // BenchmarkTimeSeriesQuery_TimeRange measures how performance scales with time range. 249 // 250 // This tests whether exemplar overhead grows linearly or non-linearly with data size. 251 // Run with: 252 // 253 // go test -bench=BenchmarkTimeSeriesQuery_TimeRange -benchmem ./pkg/querybackend/ 254 // 255 // Expected results: Overhead ratio should remain constant across time ranges. 256 func BenchmarkTimeSeriesQuery_TimeRange(b *testing.B) { 257 fixture := setupBenchmarkFixture(b) 258 259 timeRanges := []struct { 260 name string 261 duration time.Duration 262 }{ 263 {"1Minute", 1 * time.Minute}, 264 {"5Minutes", 5 * time.Minute}, 265 {"15Minutes", 15 * time.Minute}, 266 {"1Hour", 1 * time.Hour}, 267 } 268 269 exemplarTypes := []struct { 270 name string 271 typ typesv1.ExemplarType 272 }{ 273 {"NoExemplars", typesv1.ExemplarType_EXEMPLAR_TYPE_NONE}, 274 {"WithExemplars", typesv1.ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL}, 275 } 276 277 for _, tr := range timeRanges { 278 b.Run(tr.name, func(b *testing.B) { 279 for _, et := range exemplarTypes { 280 b.Run(et.name, func(b *testing.B) { 281 req := fixture.makeTimeSeriesRequest( 282 fixture.startTime, fixture.startTime.Add(tr.duration), 283 "{}", 284 []string{"service_name"}, 285 et.typ, 286 ) 287 288 b.ResetTimer() 289 for i := 0; i < b.N; i++ { 290 fixture.runTimeSeriesQuery(b, req) 291 } 292 }) 293 } 294 }) 295 } 296 }