github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/sample_merge_test.go (about) 1 package phlaredb 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/google/go-cmp/cmp/cmpopts" 12 "github.com/google/pprof/profile" 13 "github.com/google/uuid" 14 "github.com/prometheus/common/model" 15 "github.com/stretchr/testify/require" 16 "google.golang.org/protobuf/proto" 17 18 googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 19 ingestv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1" 20 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 21 "github.com/grafana/pyroscope/pkg/iter" 22 phlaremodel "github.com/grafana/pyroscope/pkg/model" 23 "github.com/grafana/pyroscope/pkg/objstore/providers/filesystem" 24 "github.com/grafana/pyroscope/pkg/pprof" 25 pprofth "github.com/grafana/pyroscope/pkg/pprof/testhelper" 26 "github.com/grafana/pyroscope/pkg/testhelper" 27 ) 28 29 func TestMergeSampleByStacktraces(t *testing.T) { 30 for _, tc := range []struct { 31 name string 32 in func() ([]*pprofth.ProfileBuilder, *phlaremodel.Tree) 33 }{ 34 { 35 name: "single profile", 36 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 37 p := pprofth.NewProfileBuilder(int64(15 * time.Second)).CPUProfile() 38 p.ForStacktraceString("my", "other").AddSamples(1) 39 p.ForStacktraceString("my", "other").AddSamples(3) 40 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 41 ps = append(ps, p) 42 tree = new(phlaremodel.Tree) 43 tree.InsertStack(4, "other", "my") 44 tree.InsertStack(3, "stack", "other", "my") 45 return ps, tree 46 }, 47 }, 48 { 49 name: "multiple profiles", 50 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 51 for i := 0; i < 3000; i++ { 52 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 53 CPUProfile().WithLabels("series", fmt.Sprintf("%d", i)) 54 p.ForStacktraceString("my", "other").AddSamples(1) 55 p.ForStacktraceString("my", "other").AddSamples(3) 56 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 57 ps = append(ps, p) 58 } 59 tree = new(phlaremodel.Tree) 60 tree.InsertStack(12000, "other", "my") 61 tree.InsertStack(9000, "stack", "other", "my") 62 return ps, tree 63 }, 64 }, 65 { 66 name: "filtering multiple profiles", 67 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 68 for i := 0; i < 3000; i++ { 69 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 70 MemoryProfile().WithLabels("series", fmt.Sprintf("%d", i)) 71 p.ForStacktraceString("my", "other").AddSamples(1, 2, 3, 4) 72 p.ForStacktraceString("my", "other").AddSamples(3, 2, 3, 4) 73 p.ForStacktraceString("my", "other", "stack").AddSamples(3, 3, 3, 3) 74 ps = append(ps, p) 75 } 76 for i := 0; i < 3000; i++ { 77 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 78 CPUProfile().WithLabels("series", fmt.Sprintf("%d", i)) 79 p.ForStacktraceString("my", "other").AddSamples(1) 80 p.ForStacktraceString("my", "other").AddSamples(3) 81 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 82 ps = append(ps, p) 83 } 84 tree = new(phlaremodel.Tree) 85 tree.InsertStack(12000, "other", "my") 86 tree.InsertStack(9000, "stack", "other", "my") 87 return ps, tree 88 }, 89 }, 90 } { 91 tc := tc 92 t.Run(tc.name, func(t *testing.T) { 93 ctx := testContext(t) 94 db, err := New(ctx, Config{ 95 DataPath: contextDataDir(ctx), 96 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 97 }, NoLimit, ctx.localBucketClient) 98 require.NoError(t, err) 99 100 input, expected := tc.in() 101 for _, p := range input { 102 require.NoError(t, db.Ingest(ctx, p.Profile, p.UUID, nil, p.Labels...)) 103 } 104 105 require.NoError(t, db.Flush(context.Background(), true, "")) 106 107 b, err := filesystem.NewBucket(filepath.Join(contextDataDir(ctx), PathLocal)) 108 require.NoError(t, err) 109 110 // open resulting block 111 q := NewBlockQuerier(ctx, b) 112 require.NoError(t, q.Sync(context.Background())) 113 114 profiles, err := q.queriers[0].SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 115 LabelSelector: `{}`, 116 Type: &typesv1.ProfileType{ 117 Name: "process_cpu", 118 SampleType: "cpu", 119 SampleUnit: "nanoseconds", 120 PeriodType: "cpu", 121 PeriodUnit: "nanoseconds", 122 }, 123 Start: int64(model.TimeFromUnixNano(0)), 124 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 125 }) 126 require.NoError(t, err) 127 128 r, err := q.queriers[0].MergeByStacktraces(ctx, profiles, 0) 129 require.NoError(t, err) 130 require.Equal(t, expected.String(), r.String()) 131 }) 132 } 133 } 134 135 func TestHeadMergeSampleByStacktraces(t *testing.T) { 136 for _, tc := range []struct { 137 name string 138 in func() ([]*pprofth.ProfileBuilder, *phlaremodel.Tree) 139 }{ 140 { 141 name: "single profile", 142 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 143 p := pprofth.NewProfileBuilder(int64(15 * time.Second)).CPUProfile() 144 p.ForStacktraceString("my", "other").AddSamples(1) 145 p.ForStacktraceString("my", "other").AddSamples(3) 146 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 147 ps = append(ps, p) 148 tree = new(phlaremodel.Tree) 149 tree.InsertStack(4, "other", "my") 150 tree.InsertStack(3, "stack", "other", "my") 151 return ps, tree 152 }, 153 }, 154 { 155 name: "multiple profiles", 156 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 157 for i := 0; i < 3000; i++ { 158 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 159 CPUProfile().WithLabels("series", fmt.Sprintf("%d", i)) 160 p.ForStacktraceString("my", "other").AddSamples(1) 161 p.ForStacktraceString("my", "other").AddSamples(3) 162 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 163 ps = append(ps, p) 164 } 165 tree = new(phlaremodel.Tree) 166 tree.InsertStack(12000, "other", "my") 167 tree.InsertStack(9000, "stack", "other", "my") 168 return ps, tree 169 }, 170 }, 171 { 172 name: "filtering multiple profiles", 173 in: func() (ps []*pprofth.ProfileBuilder, tree *phlaremodel.Tree) { 174 for i := 0; i < 3000; i++ { 175 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 176 MemoryProfile().WithLabels("series", fmt.Sprintf("%d", i)) 177 p.ForStacktraceString("my", "other").AddSamples(1, 2, 3, 4) 178 p.ForStacktraceString("my", "other").AddSamples(3, 2, 3, 4) 179 p.ForStacktraceString("my", "other", "stack").AddSamples(3, 3, 3, 3) 180 ps = append(ps, p) 181 } 182 for i := 0; i < 3000; i++ { 183 p := pprofth.NewProfileBuilder(int64(15*time.Second)). 184 CPUProfile().WithLabels("series", fmt.Sprintf("%d", i)) 185 p.ForStacktraceString("my", "other").AddSamples(1) 186 p.ForStacktraceString("my", "other").AddSamples(3) 187 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 188 ps = append(ps, p) 189 } 190 tree = new(phlaremodel.Tree) 191 tree.InsertStack(12000, "other", "my") 192 tree.InsertStack(9000, "stack", "other", "my") 193 return ps, tree 194 }, 195 }, 196 } { 197 tc := tc 198 t.Run(tc.name, func(t *testing.T) { 199 ctx := testContext(t) 200 db, err := New(ctx, Config{ 201 DataPath: contextDataDir(ctx), 202 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 203 }, NoLimit, ctx.localBucketClient) 204 require.NoError(t, err) 205 206 input, expected := tc.in() 207 for _, p := range input { 208 require.NoError(t, db.Ingest(ctx, p.Profile, p.UUID, nil, p.Labels...)) 209 } 210 profiles, err := db.queriers().SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 211 LabelSelector: `{}`, 212 Type: &typesv1.ProfileType{ 213 Name: "process_cpu", 214 SampleType: "cpu", 215 SampleUnit: "nanoseconds", 216 PeriodType: "cpu", 217 PeriodUnit: "nanoseconds", 218 }, 219 Start: int64(model.TimeFromUnixNano(0)), 220 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 221 }) 222 require.NoError(t, err) 223 r, err := db.queriers()[0].MergeByStacktraces(ctx, profiles, 0) 224 require.NoError(t, err) 225 require.Equal(t, expected.String(), r.String()) 226 }) 227 } 228 } 229 230 func TestMergeSampleByLabels(t *testing.T) { 231 for _, tc := range []struct { 232 name string 233 in func() []*pprofth.ProfileBuilder 234 expected []*typesv1.Series 235 by []string 236 }{ 237 { 238 name: "single profile", 239 in: func() (ps []*pprofth.ProfileBuilder) { 240 p := pprofth.NewProfileBuilder(int64(15 * time.Second)).CPUProfile() 241 p.ForStacktraceString("my", "other").AddSamples(1) 242 p.ForStacktraceString("my", "other").AddSamples(3) 243 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 244 ps = append(ps, p) 245 return 246 }, 247 expected: []*typesv1.Series{ 248 { 249 Labels: []*typesv1.LabelPair{}, 250 Points: []*typesv1.Point{{Timestamp: 15000, Value: 7, Annotations: []*typesv1.ProfileAnnotation{}}}, 251 }, 252 }, 253 }, 254 { 255 name: "multiple profiles", 256 by: []string{"foo"}, 257 in: func() (ps []*pprofth.ProfileBuilder) { 258 p := pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "bar") 259 p.ForStacktraceString("my", "other").AddSamples(1) 260 ps = append(ps, p) 261 262 p = pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "buzz") 263 p.ForStacktraceString("my", "other").AddSamples(1) 264 ps = append(ps, p) 265 266 p = pprofth.NewProfileBuilder(int64(30*time.Second)).CPUProfile().WithLabels("foo", "bar") 267 p.ForStacktraceString("my", "other").AddSamples(1) 268 ps = append(ps, p) 269 return 270 }, 271 expected: []*typesv1.Series{ 272 { 273 Labels: []*typesv1.LabelPair{{Name: "foo", Value: "bar"}}, 274 Points: []*typesv1.Point{ 275 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 276 {Timestamp: 30000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 277 }, 278 { 279 Labels: []*typesv1.LabelPair{{Name: "foo", Value: "buzz"}}, 280 Points: []*typesv1.Point{{Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 281 }, 282 }, 283 }, 284 { 285 name: "multiple profile no by", 286 by: []string{}, 287 in: func() (ps []*pprofth.ProfileBuilder) { 288 p := pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "bar") 289 p.ForStacktraceString("my", "other").AddSamples(1) 290 ps = append(ps, p) 291 292 p = pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "buzz") 293 p.ForStacktraceString("my", "other").AddSamples(1) 294 ps = append(ps, p) 295 296 p = pprofth.NewProfileBuilder(int64(30*time.Second)).CPUProfile().WithLabels("foo", "bar") 297 p.ForStacktraceString("my", "other").AddSamples(1) 298 ps = append(ps, p) 299 return 300 }, 301 expected: []*typesv1.Series{ 302 { 303 Labels: []*typesv1.LabelPair{}, 304 Points: []*typesv1.Point{ 305 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 306 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 307 {Timestamp: 30000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 308 }, 309 }, 310 }, 311 } { 312 tc := tc 313 t.Run(tc.name, func(t *testing.T) { 314 ctx := testContext(t) 315 db, err := New(ctx, Config{ 316 DataPath: contextDataDir(ctx), 317 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 318 }, NoLimit, ctx.localBucketClient) 319 require.NoError(t, err) 320 321 for _, p := range tc.in() { 322 require.NoError(t, db.Ingest(ctx, p.Profile, p.UUID, nil, p.Labels...)) 323 } 324 325 require.NoError(t, db.Flush(context.Background(), true, "")) 326 327 b, err := filesystem.NewBucket(filepath.Join(contextDataDir(ctx), PathLocal)) 328 require.NoError(t, err) 329 330 // open resulting block 331 q := NewBlockQuerier(context.Background(), b) 332 require.NoError(t, q.Sync(context.Background())) 333 334 profileIt, err := q.queriers[0].SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 335 LabelSelector: `{}`, 336 Type: &typesv1.ProfileType{ 337 Name: "process_cpu", 338 SampleType: "cpu", 339 SampleUnit: "nanoseconds", 340 PeriodType: "cpu", 341 PeriodUnit: "nanoseconds", 342 }, 343 Start: int64(model.TimeFromUnixNano(0)), 344 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 345 }) 346 require.NoError(t, err) 347 profiles, err := iter.Slice(profileIt) 348 require.NoError(t, err) 349 350 q.queriers[0].Sort(profiles) 351 series, err := q.queriers[0].MergeByLabels(ctx, iter.NewSliceIterator(profiles), nil, tc.by...) 352 require.NoError(t, err) 353 354 testhelper.EqualProto(t, tc.expected, series) 355 }) 356 } 357 } 358 359 func TestHeadMergeSampleByLabels(t *testing.T) { 360 for _, tc := range []struct { 361 name string 362 in func() []*pprofth.ProfileBuilder 363 expected []*typesv1.Series 364 by []string 365 }{ 366 { 367 name: "single profile", 368 in: func() (ps []*pprofth.ProfileBuilder) { 369 p := pprofth.NewProfileBuilder(int64(15 * time.Second)).CPUProfile() 370 p.ForStacktraceString("my", "other").AddSamples(1) 371 p.ForStacktraceString("my", "other").AddSamples(3) 372 p.ForStacktraceString("my", "other", "stack").AddSamples(3) 373 ps = append(ps, p) 374 return 375 }, 376 expected: []*typesv1.Series{ 377 { 378 Labels: []*typesv1.LabelPair{}, 379 Points: []*typesv1.Point{{Timestamp: 15000, Value: 7, Annotations: []*typesv1.ProfileAnnotation{}}}, 380 }, 381 }, 382 }, 383 { 384 name: "multiple profiles", 385 by: []string{"foo"}, 386 in: func() (ps []*pprofth.ProfileBuilder) { 387 p := pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "bar") 388 p.ForStacktraceString("my", "other").AddSamples(1) 389 ps = append(ps, p) 390 391 p = pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "buzz") 392 p.ForStacktraceString("my", "other").AddSamples(1) 393 ps = append(ps, p) 394 395 p = pprofth.NewProfileBuilder(int64(30*time.Second)).CPUProfile().WithLabels("foo", "bar") 396 p.ForStacktraceString("my", "other").AddSamples(1) 397 ps = append(ps, p) 398 return 399 }, 400 expected: []*typesv1.Series{ 401 { 402 Labels: []*typesv1.LabelPair{{Name: "foo", Value: "bar"}}, 403 Points: []*typesv1.Point{ 404 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 405 {Timestamp: 30000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 406 }, 407 { 408 Labels: []*typesv1.LabelPair{{Name: "foo", Value: "buzz"}}, 409 Points: []*typesv1.Point{{Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 410 }, 411 }, 412 }, 413 { 414 name: "multiple profile no by", 415 by: []string{}, 416 in: func() (ps []*pprofth.ProfileBuilder) { 417 p := pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "bar") 418 p.ForStacktraceString("my", "other").AddSamples(1) 419 ps = append(ps, p) 420 421 p = pprofth.NewProfileBuilder(int64(15*time.Second)).CPUProfile().WithLabels("foo", "buzz") 422 p.ForStacktraceString("my", "other").AddSamples(1) 423 ps = append(ps, p) 424 425 p = pprofth.NewProfileBuilder(int64(30*time.Second)).CPUProfile().WithLabels("foo", "bar") 426 p.ForStacktraceString("my", "other").AddSamples(1) 427 ps = append(ps, p) 428 return 429 }, 430 expected: []*typesv1.Series{ 431 { 432 Labels: []*typesv1.LabelPair{}, 433 Points: []*typesv1.Point{ 434 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 435 {Timestamp: 15000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}, 436 {Timestamp: 30000, Value: 1, Annotations: []*typesv1.ProfileAnnotation{}}}, 437 }, 438 }, 439 }, 440 } { 441 tc := tc 442 t.Run(tc.name, func(t *testing.T) { 443 ctx := testContext(t) 444 db, err := New(ctx, Config{ 445 DataPath: contextDataDir(ctx), 446 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 447 }, NoLimit, ctx.localBucketClient) 448 require.NoError(t, err) 449 450 for _, p := range tc.in() { 451 require.NoError(t, db.Ingest(ctx, p.Profile, p.UUID, nil, p.Labels...)) 452 } 453 454 profileIt, err := db.queriers().SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 455 LabelSelector: `{}`, 456 Type: &typesv1.ProfileType{ 457 Name: "process_cpu", 458 SampleType: "cpu", 459 SampleUnit: "nanoseconds", 460 PeriodType: "cpu", 461 PeriodUnit: "nanoseconds", 462 }, 463 Start: int64(model.TimeFromUnixNano(0)), 464 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 465 }) 466 require.NoError(t, err) 467 profiles, err := iter.Slice(profileIt) 468 require.NoError(t, err) 469 470 db.headQueriers()[0].Sort(profiles) 471 series, err := db.headQueriers()[0].MergeByLabels(ctx, iter.NewSliceIterator(profiles), nil, tc.by...) 472 require.NoError(t, err) 473 474 testhelper.EqualProto(t, tc.expected, series) 475 }) 476 } 477 } 478 479 func TestMergePprof(t *testing.T) { 480 ctx := testContext(t) 481 db, err := New(ctx, Config{ 482 DataPath: contextDataDir(ctx), 483 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 484 }, NoLimit, ctx.localBucketClient) 485 require.NoError(t, err) 486 487 for i := 0; i < 3; i++ { 488 require.NoError(t, db.Ingest(ctx, generateProfile(t, i*1000), uuid.New(), nil, &typesv1.LabelPair{ 489 Name: model.MetricNameLabel, 490 Value: "process_cpu", 491 })) 492 } 493 494 require.NoError(t, db.Flush(context.Background(), true, "")) 495 496 b, err := filesystem.NewBucket(filepath.Join(contextDataDir(ctx), PathLocal)) 497 require.NoError(t, err) 498 499 // open resulting block 500 q := NewBlockQuerier(context.Background(), b) 501 require.NoError(t, q.Sync(context.Background())) 502 503 profileIt, err := q.queriers[0].SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 504 LabelSelector: `{}`, 505 Type: &typesv1.ProfileType{ 506 Name: "process_cpu", 507 SampleType: "cpu", 508 SampleUnit: "nanoseconds", 509 PeriodType: "cpu", 510 PeriodUnit: "nanoseconds", 511 }, 512 Start: int64(model.TimeFromUnixNano(0)), 513 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 514 }) 515 require.NoError(t, err) 516 profiles, err := iter.Slice(profileIt) 517 require.NoError(t, err) 518 519 q.queriers[0].Sort(profiles) 520 result, err := q.queriers[0].MergePprof(ctx, iter.NewSliceIterator(profiles), 0, nil) 521 require.NoError(t, err) 522 523 data, err := proto.Marshal(generateProfile(t, 1)) 524 require.NoError(t, err) 525 expected, err := profile.ParseUncompressed(data) 526 require.NoError(t, err) 527 for _, sample := range expected.Sample { 528 sample.Value = []int64{sample.Value[0] * 3} 529 } 530 data, err = proto.Marshal(result) 531 require.NoError(t, err) 532 actual, err := profile.ParseUncompressed(data) 533 require.NoError(t, err) 534 compareProfile(t, expected.Compact(), actual.Compact()) 535 } 536 537 func TestHeadMergePprof(t *testing.T) { 538 ctx := testContext(t) 539 db, err := New(ctx, Config{ 540 DataPath: contextDataDir(ctx), 541 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 542 }, NoLimit, ctx.localBucketClient) 543 require.NoError(t, err) 544 545 for i := 0; i < 3; i++ { 546 require.NoError(t, db.Ingest(ctx, generateProfile(t, i*1000), uuid.New(), nil, &typesv1.LabelPair{ 547 Name: model.MetricNameLabel, 548 Value: "process_cpu", 549 })) 550 } 551 552 profileIt, err := db.queriers().SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 553 LabelSelector: `{}`, 554 Type: &typesv1.ProfileType{ 555 Name: "process_cpu", 556 SampleType: "cpu", 557 SampleUnit: "nanoseconds", 558 PeriodType: "cpu", 559 PeriodUnit: "nanoseconds", 560 }, 561 Start: int64(model.TimeFromUnixNano(0)), 562 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 563 }) 564 require.NoError(t, err) 565 profiles, err := iter.Slice(profileIt) 566 require.NoError(t, err) 567 568 db.headQueriers()[0].Sort(profiles) 569 result, err := db.headQueriers()[0].MergePprof(ctx, iter.NewSliceIterator(profiles), 0, nil) 570 require.NoError(t, err) 571 572 data, err := proto.Marshal(generateProfile(t, 1)) 573 require.NoError(t, err) 574 expected, err := profile.ParseUncompressed(data) 575 require.NoError(t, err) 576 for _, sample := range expected.Sample { 577 sample.Value = []int64{sample.Value[0] * 3} 578 } 579 data, err = proto.Marshal(result) 580 require.NoError(t, err) 581 actual, err := profile.ParseUncompressed(data) 582 require.NoError(t, err) 583 compareProfile(t, expected.Compact(), actual.Compact()) 584 } 585 586 func TestMergeSpans(t *testing.T) { 587 ctx := testContext(t) 588 db, err := New(ctx, Config{ 589 DataPath: contextDataDir(ctx), 590 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 591 }, NoLimit, ctx.localBucketClient) 592 require.NoError(t, err) 593 594 require.NoError(t, db.Ingest(ctx, generateProfileWithSpans(t, 1000), uuid.New(), nil, &typesv1.LabelPair{ 595 Name: model.MetricNameLabel, 596 Value: "process_cpu", 597 })) 598 599 require.NoError(t, db.Flush(context.Background(), true, "")) 600 601 b, err := filesystem.NewBucket(filepath.Join(contextDataDir(ctx), PathLocal)) 602 require.NoError(t, err) 603 604 // open resulting block 605 q := NewBlockQuerier(context.Background(), b) 606 require.NoError(t, q.Sync(context.Background())) 607 608 profileIt, err := q.queriers[0].SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 609 LabelSelector: `{}`, 610 Type: &typesv1.ProfileType{ 611 Name: "process_cpu", 612 SampleType: "cpu", 613 SampleUnit: "nanoseconds", 614 PeriodType: "cpu", 615 PeriodUnit: "nanoseconds", 616 }, 617 Start: int64(model.TimeFromUnixNano(0)), 618 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 619 }) 620 require.NoError(t, err) 621 profiles, err := iter.Slice(profileIt) 622 require.NoError(t, err) 623 624 q.queriers[0].Sort(profiles) 625 spanSelector, err := phlaremodel.NewSpanSelector([]string{"badbadbadbadbadb"}) 626 require.NoError(t, err) 627 result, err := q.queriers[0].MergeBySpans(ctx, iter.NewSliceIterator(profiles), spanSelector) 628 require.NoError(t, err) 629 630 expected := new(phlaremodel.Tree) 631 expected.InsertStack(1, "bar", "foo") 632 expected.InsertStack(2, "foo") 633 634 require.Equal(t, expected.String(), result.String()) 635 } 636 637 func TestHeadMergeSpans(t *testing.T) { 638 ctx := testContext(t) 639 db, err := New(ctx, Config{ 640 DataPath: contextDataDir(ctx), 641 MaxBlockDuration: time.Duration(100000) * time.Minute, // we will manually flush 642 }, NoLimit, ctx.localBucketClient) 643 require.NoError(t, err) 644 645 require.NoError(t, db.Ingest(ctx, generateProfileWithSpans(t, 1000), uuid.New(), nil, &typesv1.LabelPair{ 646 Name: model.MetricNameLabel, 647 Value: "process_cpu", 648 })) 649 650 profileIt, err := db.headQueriers().SelectMatchingProfiles(ctx, &ingestv1.SelectProfilesRequest{ 651 LabelSelector: `{}`, 652 Type: &typesv1.ProfileType{ 653 Name: "process_cpu", 654 SampleType: "cpu", 655 SampleUnit: "nanoseconds", 656 PeriodType: "cpu", 657 PeriodUnit: "nanoseconds", 658 }, 659 Start: int64(model.TimeFromUnixNano(0)), 660 End: int64(model.TimeFromUnixNano(int64(1 * time.Minute))), 661 }) 662 require.NoError(t, err) 663 profiles, err := iter.Slice(profileIt) 664 require.NoError(t, err) 665 666 db.headQueriers()[0].Sort(profiles) 667 spanSelector, err := phlaremodel.NewSpanSelector([]string{"badbadbadbadbadb"}) 668 require.NoError(t, err) 669 670 result, err := db.headQueriers()[0].MergeBySpans(ctx, iter.NewSliceIterator(profiles), spanSelector) 671 require.NoError(t, err) 672 673 expected := new(phlaremodel.Tree) 674 expected.InsertStack(1, "bar", "foo") 675 expected.InsertStack(2, "foo") 676 677 require.Equal(t, expected.String(), result.String()) 678 } 679 680 func generateProfile(t *testing.T, ts int) *googlev1.Profile { 681 t.Helper() 682 683 prof, err := pprof.FromProfile(pprofth.FooBarProfile) 684 685 require.NoError(t, err) 686 prof.TimeNanos = int64(ts) 687 return prof 688 } 689 690 func generateProfileWithSpans(t *testing.T, ts int) *googlev1.Profile { 691 t.Helper() 692 693 prof, err := pprof.FromProfile(pprofth.FooBarProfileWithSpans) 694 695 require.NoError(t, err) 696 prof.TimeNanos = int64(ts) 697 return prof 698 } 699 700 func compareProfile(t *testing.T, expected, actual *profile.Profile) { 701 t.Helper() 702 compareProfileSlice(t, expected.Sample, actual.Sample) 703 compareProfileSlice(t, expected.Mapping, actual.Mapping) 704 compareProfileSlice(t, expected.Location, actual.Location) 705 compareProfileSlice(t, expected.Function, actual.Function) 706 } 707 708 // compareProfileSlice compares two slices of profile data. 709 // It ignores ID, un-exported fields. 710 func compareProfileSlice[T any](t *testing.T, expected, actual []T) { 711 t.Helper() 712 lessMapping := func(a, b *profile.Mapping) bool { return a.BuildID < b.BuildID } 713 lessSample := func(a, b *profile.Sample) bool { 714 if len(a.Value) != len(b.Value) { 715 return len(a.Value) < len(b.Value) 716 } 717 for i := range a.Value { 718 if a.Value[i] != b.Value[i] { 719 return a.Value[i] < b.Value[i] 720 } 721 } 722 return false 723 } 724 lessLocation := func(a, b *profile.Location) bool { return a.Address < b.Address } 725 lessFunction := func(a, b *profile.Function) bool { return a.Name < b.Name } 726 727 if diff := cmp.Diff(expected, actual, cmpopts.IgnoreUnexported( 728 profile.Mapping{}, profile.Function{}, profile.Line{}, profile.Location{}, profile.Sample{}, profile.ValueType{}, profile.Profile{}, 729 ), cmpopts.SortSlices(lessMapping), cmpopts.SortSlices(lessSample), cmpopts.SortSlices(lessLocation), cmpopts.SortSlices(lessFunction), 730 cmpopts.IgnoreFields(profile.Mapping{}, "ID"), 731 cmpopts.IgnoreFields(profile.Location{}, "ID"), 732 cmpopts.IgnoreFields(profile.Function{}, "ID"), 733 ); diff != "" { 734 t.Errorf("result mismatch (-want +got):\n%s", diff) 735 } 736 }