github.com/grafana/pyroscope@v1.18.0/pkg/segmentwriter/memdb/head_test.go (about) 1 package memdb 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "strconv" 10 "sync" 11 "testing" 12 "time" 13 14 "connectrpc.com/connect" 15 "github.com/google/pprof/profile" 16 "github.com/google/uuid" 17 "github.com/parquet-go/parquet-go" 18 "github.com/prometheus/common/model" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "go.uber.org/goleak" 22 23 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 24 ingestv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1" 25 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1" 26 "github.com/grafana/pyroscope/pkg/iter" 27 phlaremodel "github.com/grafana/pyroscope/pkg/model" 28 "github.com/grafana/pyroscope/pkg/og/convert/pprof/bench" 29 "github.com/grafana/pyroscope/pkg/phlaredb" 30 testutil2 "github.com/grafana/pyroscope/pkg/phlaredb/block/testutil" 31 "github.com/grafana/pyroscope/pkg/phlaredb/symdb" 32 "github.com/grafana/pyroscope/pkg/pprof" 33 "github.com/grafana/pyroscope/pkg/pprof/testhelper" 34 "github.com/grafana/pyroscope/pkg/segmentwriter/memdb/testutil" 35 ) 36 37 var defaultAnnotations []*typesv1.ProfileAnnotation 38 39 func TestHeadLabelValues(t *testing.T) { 40 head := newTestHead() 41 head.Ingest(newProfileFoo(), uuid.New(), []*typesv1.LabelPair{{Name: "job", Value: "foo"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 42 head.Ingest(newProfileBar(), uuid.New(), []*typesv1.LabelPair{{Name: "job", Value: "bar"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 43 44 q := flushTestHead(t, head) 45 46 res, err := q.LabelValues(context.Background(), connect.NewRequest(&typesv1.LabelValuesRequest{Name: "cluster"})) 47 require.NoError(t, err) 48 require.Equal(t, []string{}, res.Msg.Names) 49 50 res, err = q.LabelValues(context.Background(), connect.NewRequest(&typesv1.LabelValuesRequest{Name: "job"})) 51 require.NoError(t, err) 52 require.Equal(t, []string{"bar", "foo"}, res.Msg.Names) 53 } 54 55 func TestHeadLabelNames(t *testing.T) { 56 head := newTestHead() 57 head.Ingest(newProfileFoo(), uuid.New(), []*typesv1.LabelPair{{Name: "job", Value: "foo"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 58 head.Ingest(newProfileBar(), uuid.New(), []*typesv1.LabelPair{{Name: "job", Value: "bar"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 59 60 q := flushTestHead(t, head) 61 62 res, err := q.LabelNames(context.Background(), connect.NewRequest(&typesv1.LabelNamesRequest{})) 63 require.NoError(t, err) 64 require.Equal(t, []string{"__period_type__", "__period_unit__", "__profile_type__", "__type__", "__unit__", "job", "namespace"}, res.Msg.Names) 65 } 66 67 func TestHeadSeries(t *testing.T) { 68 head := newTestHead() 69 fooLabels := phlaremodel.NewLabelsBuilder(nil).Set("namespace", "phlare").Set("job", "foo").Labels() 70 barLabels := phlaremodel.NewLabelsBuilder(nil).Set("namespace", "phlare").Set("job", "bar").Labels() 71 head.Ingest(newProfileFoo(), uuid.New(), fooLabels, defaultAnnotations) 72 head.Ingest(newProfileBar(), uuid.New(), barLabels, defaultAnnotations) 73 74 lblBuilder := phlaremodel.NewLabelsBuilder(nil). 75 Set("namespace", "phlare"). 76 Set("job", "foo"). 77 Set("__period_type__", "type"). 78 Set("__period_unit__", "unit"). 79 Set("__type__", "type"). 80 Set("__unit__", "unit"). 81 Set("__profile_type__", ":type:unit:type:unit") 82 expected := lblBuilder.Labels() 83 84 q := flushTestHead(t, head) 85 86 res, err := q.Series(context.Background(), &ingestv1.SeriesRequest{Matchers: []string{`{job="foo"}`}}) 87 require.NoError(t, err) 88 require.Equal(t, []*typesv1.Labels{{Labels: expected}}, res) 89 90 // Test we can filter labelNames 91 res, err = q.Series(context.Background(), &ingestv1.SeriesRequest{LabelNames: []string{"job", "not-existing"}}) 92 require.NoError(t, err) 93 lblBuilder.Reset(nil) 94 jobFoo := lblBuilder.Set("job", "foo").Labels() 95 lblBuilder.Reset(nil) 96 jobBar := lblBuilder.Set("job", "bar").Labels() 97 require.Len(t, res, 2) 98 require.Contains(t, res, &typesv1.Labels{Labels: jobFoo}) 99 require.Contains(t, res, &typesv1.Labels{Labels: jobBar}) 100 } 101 102 func TestHeadProfileTypes(t *testing.T) { 103 head := newTestHead() 104 head.Ingest(newProfileFoo(), uuid.New(), []*typesv1.LabelPair{{Name: "__name__", Value: "foo"}, {Name: "job", Value: "foo"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 105 head.Ingest(newProfileBar(), uuid.New(), []*typesv1.LabelPair{{Name: "__name__", Value: "bar"}, {Name: "namespace", Value: "phlare"}}, defaultAnnotations) 106 107 q := flushTestHead(t, head) 108 109 res, err := q.ProfileTypes(context.Background(), connect.NewRequest(&ingestv1.ProfileTypesRequest{})) 110 require.NoError(t, err) 111 require.Equal(t, []*typesv1.ProfileType{ 112 mustParseProfileSelector(t, "bar:type:unit:type:unit"), 113 mustParseProfileSelector(t, "foo:type:unit:type:unit"), 114 }, res.Msg.ProfileTypes) 115 } 116 117 func TestHead_SelectMatchingProfiles_Order(t *testing.T) { 118 const n = 15 119 head := NewHead(NewHeadMetricsWithPrefix(nil, "")) 120 121 now := time.Now() 122 for i := 0; i < n; i++ { 123 x := newProfileFoo() 124 // Make sure some of our profiles have matching timestamps. 125 x.TimeNanos = now.Add(time.Second * time.Duration(i-i%2)).UnixNano() 126 head.Ingest(x, uuid.UUID{}, []*typesv1.LabelPair{ 127 {Name: "job", Value: "foo"}, 128 {Name: "x", Value: strconv.Itoa(i)}, 129 }, defaultAnnotations) 130 } 131 132 q := flushTestHead(t, head) 133 134 typ, err := phlaremodel.ParseProfileTypeSelector(":type:unit:type:unit") 135 require.NoError(t, err) 136 req := &ingestv1.SelectProfilesRequest{ 137 LabelSelector: "{}", 138 Type: typ, 139 End: now.Add(time.Hour).UnixMilli(), 140 } 141 142 profiles := make([]phlaredb.Profile, 0, n) 143 i, err := q.SelectMatchingProfiles(context.Background(), req) 144 require.NoError(t, err) 145 s, err := iter.Slice(i) 146 require.NoError(t, err) 147 profiles = append(profiles, s...) 148 149 assert.Equal(t, n, len(profiles)) 150 for i, p := range profiles { 151 x, err := strconv.Atoi(p.Labels().Get("x")) 152 require.NoError(t, err) 153 require.Equal(t, i, x, "SelectMatchingProfiles order mismatch") 154 } 155 } 156 157 const testdataPrefix = "../../phlaredb" 158 159 func TestHeadFlushQuery(t *testing.T) { 160 testdata := []struct { 161 path string 162 profile *profilev1.Profile 163 svc string 164 }{ 165 {testdataPrefix + "/testdata/heap", nil, "svc_heap"}, 166 {testdataPrefix + "/testdata/profile", nil, "svc_profile"}, 167 {testdataPrefix + "/testdata/profile_uncompressed", nil, "svc_profile_uncompressed"}, 168 {testdataPrefix + "/testdata/profile_python", nil, "svc_python"}, 169 {testdataPrefix + "/testdata/profile_java", nil, "svc_java"}, 170 } 171 for i := range testdata { 172 td := &testdata[i] 173 p := parseProfile(t, td.path) 174 td.profile = p 175 } 176 177 head := newTestHead() 178 ctx := context.Background() 179 180 for pos := range testdata { 181 head.Ingest(testdata[pos].profile.CloneVT(), uuid.New(), []*typesv1.LabelPair{ 182 {Name: phlaremodel.LabelNameServiceName, Value: testdata[pos].svc}, 183 }, defaultAnnotations) 184 } 185 186 flushed, err := head.Flush(ctx) 187 require.NoError(t, err) 188 189 assert.Equal(t, 14192, int(flushed.Meta.NumSamples)) 190 assert.Equal(t, 11, int(flushed.Meta.NumSeries)) // different value from original phlaredb test because service_name label added 191 assert.Equal(t, 11, int(flushed.Meta.NumProfiles)) 192 assert.Equal(t, []string{ 193 ":CPU:nanoseconds:CPU:nanoseconds", 194 ":alloc_objects:count:space:bytes", 195 ":alloc_space:bytes:space:bytes", 196 ":cpu:nanoseconds:cpu:nanoseconds", 197 ":inuse_objects:count:space:bytes", 198 ":inuse_space:bytes:space:bytes", 199 ":sample:count:CPU:nanoseconds", 200 ":samples:count:cpu:nanoseconds", 201 }, flushed.Meta.ProfileTypeNames) 202 203 q := createBlockFromFlushedHead(t, flushed) 204 205 for _, td := range testdata { 206 for stIndex := range td.profile.SampleType { 207 p, err := q.SelectMergePprof(context.Background(), &ingestv1.SelectProfilesRequest{ 208 LabelSelector: fmt.Sprintf("{%s=\"%s\"}", phlaremodel.LabelNameServiceName, td.svc), 209 Type: profileTypeFromProfile(td.profile, stIndex), 210 Start: time.Unix(0, td.profile.TimeNanos).UnixMilli(), 211 End: time.Unix(0, td.profile.TimeNanos).Add(time.Millisecond).UnixMilli(), 212 }, 163840, nil, 213 ) 214 require.NoError(t, err) 215 require.NotNil(t, p) 216 217 compareProfile(t, td.profile, stIndex, p) 218 } 219 } 220 } 221 222 func TestHead_Concurrent_Ingest(t *testing.T) { 223 head := newTestHead() 224 225 wg := sync.WaitGroup{} 226 227 profilesPerSeries := 330 228 229 for i := 0; i < 3; i++ { 230 wg.Add(1) 231 // ingester 232 go func(i int) { 233 defer wg.Done() 234 tick := time.NewTicker(time.Millisecond) 235 defer tick.Stop() 236 for j := 0; j < profilesPerSeries; j++ { 237 <-tick.C 238 ingestThreeProfileStreams(profilesPerSeries*i+j, head.Ingest) 239 } 240 t.Logf("ingest stream %s done", streams[i]) 241 }(i) 242 } 243 244 wg.Wait() 245 246 _ = flushTestHead(t, head) 247 } 248 249 func profileWithID(id int) (*profilev1.Profile, uuid.UUID) { 250 p := newProfileFoo() 251 p.TimeNanos = int64(id) 252 return p, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", id)) 253 } 254 255 func TestHead_ProfileOrder(t *testing.T) { 256 head := newTestHead() 257 258 p, u := profileWithID(1) 259 head.Ingest(p, u, []*typesv1.LabelPair{ 260 {Name: phlaremodel.LabelNameProfileName, Value: "memory"}, 261 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 262 {Name: phlaremodel.LabelNameServiceName, Value: "service-a"}, 263 }, defaultAnnotations) 264 265 p, u = profileWithID(2) 266 head.Ingest(p, u, []*typesv1.LabelPair{ 267 {Name: phlaremodel.LabelNameProfileName, Value: "memory"}, 268 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 269 {Name: phlaremodel.LabelNameServiceName, Value: "service-b"}, 270 {Name: "____Label", Value: "important"}, 271 }, defaultAnnotations) 272 273 p, u = profileWithID(3) 274 head.Ingest(p, u, []*typesv1.LabelPair{ 275 {Name: phlaremodel.LabelNameProfileName, Value: "memory"}, 276 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 277 {Name: phlaremodel.LabelNameServiceName, Value: "service-c"}, 278 {Name: "AAALabel", Value: "important"}, 279 }, defaultAnnotations) 280 281 p, u = profileWithID(4) 282 head.Ingest(p, u, []*typesv1.LabelPair{ 283 {Name: phlaremodel.LabelNameProfileName, Value: "cpu"}, 284 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 285 {Name: phlaremodel.LabelNameServiceName, Value: "service-a"}, 286 {Name: "000Label", Value: "important"}, 287 }, defaultAnnotations) 288 289 p, u = profileWithID(5) 290 head.Ingest(p, u, []*typesv1.LabelPair{ 291 {Name: phlaremodel.LabelNameProfileName, Value: "cpu"}, 292 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 293 {Name: phlaremodel.LabelNameServiceName, Value: "service-b"}, 294 }, defaultAnnotations) 295 296 p, u = profileWithID(6) 297 head.Ingest(p, u, []*typesv1.LabelPair{ 298 {Name: phlaremodel.LabelNameProfileName, Value: "cpu"}, 299 {Name: phlaremodel.LabelNameOrder, Value: phlaremodel.LabelOrderEnforced}, 300 {Name: phlaremodel.LabelNameServiceName, Value: "service-b"}, 301 }, defaultAnnotations) 302 303 flushed, err := head.Flush(context.Background()) 304 require.NoError(t, err) 305 306 // test that the profiles are ordered correctly 307 type row struct{ TimeNanos uint64 } 308 rows, err := parquet.Read[row](bytes.NewReader(flushed.Profiles), int64(len(flushed.Profiles))) 309 require.NoError(t, err) 310 require.Equal(t, []row{ 311 {4}, {5}, {6}, {1}, {2}, {3}, 312 }, rows) 313 } 314 315 func TestFlushEmptyHead(t *testing.T) { 316 head := newTestHead() 317 flushed, err := head.Flush(context.Background()) 318 require.NoError(t, err) 319 require.NotNil(t, flushed) 320 require.Equal(t, 0, len(flushed.Profiles)) 321 } 322 323 func TestMergeProfilesStacktraces(t *testing.T) { 324 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 325 326 // ingest some sample data 327 var ( 328 end = time.Unix(0, int64(time.Hour)) 329 start = end.Add(-time.Minute) 330 step = 15 * time.Second 331 ) 332 333 db := newTestHead() 334 335 ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step, 336 defaultAnnotations, 337 &typesv1.LabelPair{Name: "namespace", Value: "my-namespace"}, 338 &typesv1.LabelPair{Name: "pod", Value: "my-pod"}, 339 ) 340 341 q := flushTestHead(t, db) 342 343 // create client 344 client, cleanup := testutil.IngesterClientForTest(t, []phlaredb.Querier{q}) 345 defer cleanup() 346 347 t.Run("request the one existing series", func(t *testing.T) { 348 bidi := client.MergeProfilesStacktraces(context.Background()) 349 350 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{ 351 Request: &ingestv1.SelectProfilesRequest{ 352 LabelSelector: `{pod="my-pod"}`, 353 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 354 Start: start.UnixMilli(), 355 End: end.UnixMilli(), 356 }, 357 })) 358 359 resp, err := bidi.Receive() 360 require.NoError(t, err) 361 require.Nil(t, resp.Result) 362 require.Len(t, resp.SelectedProfiles.Fingerprints, 1) 363 require.Len(t, resp.SelectedProfiles.Profiles, 5) 364 365 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{ 366 Profiles: []bool{true}, 367 })) 368 369 // expect empty response 370 resp, err = bidi.Receive() 371 require.NoError(t, err) 372 require.Nil(t, resp.Result) 373 374 // received result 375 resp, err = bidi.Receive() 376 require.NoError(t, err) 377 require.NotNil(t, resp.Result) 378 379 at, err := phlaremodel.UnmarshalTree(resp.Result.TreeBytes) 380 require.NoError(t, err) 381 require.Equal(t, int64(500000000), at.Total()) 382 }) 383 384 t.Run("request non existing series", func(t *testing.T) { 385 bidi := client.MergeProfilesStacktraces(context.Background()) 386 387 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{ 388 Request: &ingestv1.SelectProfilesRequest{ 389 LabelSelector: `{pod="not-my-pod"}`, 390 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 391 Start: start.UnixMilli(), 392 End: end.UnixMilli(), 393 }, 394 })) 395 396 // expect empty resp to signal it is finished 397 resp, err := bidi.Receive() 398 require.NoError(t, err) 399 require.Nil(t, resp.Result) 400 require.Nil(t, resp.SelectedProfiles) 401 402 // still receiving a result 403 resp, err = bidi.Receive() 404 require.NoError(t, err) 405 require.NotNil(t, resp.Result) 406 require.Len(t, resp.Result.Stacktraces, 0) 407 require.Len(t, resp.Result.FunctionNames, 0) 408 require.Nil(t, resp.SelectedProfiles) 409 }) 410 411 t.Run("empty request fails", func(t *testing.T) { 412 bidi := client.MergeProfilesStacktraces(context.Background()) 413 414 // It is possible that the error returned by server side of the 415 // stream closes the net.Conn before bidi.Send has finished. The 416 // short timing for that to happen with real HTTP servers makes this 417 // unlikely, but it does happen with the synchronous in memory 418 // net.Pipe() that is used here. 419 // See https://github.com/grafana/pyroscope/issues/3549 for more details. 420 if err := bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{}); !errors.Is(err, io.EOF) { 421 require.NoError(t, err) 422 } 423 424 _, err := bidi.Receive() 425 require.EqualError(t, err, "invalid_argument: missing initial select request") 426 }) 427 428 t.Run("test cancellation", func(t *testing.T) { 429 ctx, cancel := context.WithCancel(context.Background()) 430 bidi := client.MergeProfilesStacktraces(ctx) 431 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{ 432 Request: &ingestv1.SelectProfilesRequest{ 433 LabelSelector: `{pod="my-pod"}`, 434 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 435 Start: start.UnixMilli(), 436 End: end.UnixMilli(), 437 }, 438 })) 439 cancel() 440 }) 441 442 t.Run("test close request", func(t *testing.T) { 443 bidi := client.MergeProfilesStacktraces(context.Background()) 444 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesStacktracesRequest{ 445 Request: &ingestv1.SelectProfilesRequest{ 446 LabelSelector: `{pod="my-pod"}`, 447 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 448 Start: start.UnixMilli(), 449 End: end.UnixMilli(), 450 }, 451 })) 452 require.NoError(t, bidi.CloseRequest()) 453 }) 454 } 455 456 func TestMergeProfilesLabels(t *testing.T) { 457 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 458 459 // ingest some sample data 460 var ( 461 end = time.Unix(0, int64(time.Hour)) 462 start = end.Add(-time.Minute) 463 step = 15 * time.Second 464 ) 465 466 db := newTestHead() 467 468 ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step, 469 []*typesv1.ProfileAnnotation{ 470 {Key: "foo", Value: "test annotation"}, 471 }, 472 &typesv1.LabelPair{Name: "namespace", Value: "my-namespace"}, 473 &typesv1.LabelPair{Name: "pod", Value: "my-pod"}, 474 ) 475 476 q := flushTestHead(t, db) 477 478 // create client 479 client, cleanup := testutil.IngesterClientForTest(t, []phlaredb.Querier{q}) 480 defer cleanup() 481 482 t.Run("request the one existing series", func(t *testing.T) { 483 bidi := client.MergeProfilesLabels(context.Background()) 484 485 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesLabelsRequest{ 486 Request: &ingestv1.SelectProfilesRequest{ 487 LabelSelector: `{pod="my-pod"}`, 488 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 489 Start: start.UnixMilli(), 490 End: end.UnixMilli(), 491 }, 492 })) 493 494 resp, err := bidi.Receive() 495 require.NoError(t, err) 496 require.Nil(t, resp.Series) 497 require.Len(t, resp.SelectedProfiles.Fingerprints, 1) 498 require.Len(t, resp.SelectedProfiles.Profiles, 5) 499 500 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesLabelsRequest{ 501 Profiles: []bool{true}, 502 })) 503 504 // expect empty response 505 resp, err = bidi.Receive() 506 require.NoError(t, err) 507 require.Nil(t, resp.Series) 508 509 // received result 510 resp, err = bidi.Receive() 511 require.NoError(t, err) 512 require.NotNil(t, resp.Series) 513 514 require.NoError(t, err) 515 require.Equal(t, 1, len(resp.Series)) 516 point := resp.Series[0].Points[0] 517 require.Equal(t, int64(3540000), point.Timestamp) 518 require.Equal(t, float64(500000000), point.Value) 519 require.Equal(t, "test annotation", point.Annotations[0].Value) 520 }) 521 } 522 523 func TestMergeProfilesPprof(t *testing.T) { 524 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 525 526 // ingest some sample data 527 var ( 528 end = time.Unix(0, int64(time.Hour)) 529 start = end.Add(-time.Minute) 530 step = 15 * time.Second 531 ) 532 533 db := NewHead(NewHeadMetricsWithPrefix(nil, "")) 534 535 ingestProfiles(t, db, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step, 536 defaultAnnotations, 537 &typesv1.LabelPair{Name: "namespace", Value: "my-namespace"}, 538 &typesv1.LabelPair{Name: "pod", Value: "my-pod"}, 539 ) 540 541 q := flushTestHead(t, db) 542 543 // create client 544 client, cleanup := testutil.IngesterClientForTest(t, []phlaredb.Querier{q}) 545 defer cleanup() 546 547 t.Run("request the one existing series", func(t *testing.T) { 548 bidi := client.MergeProfilesPprof(context.Background()) 549 550 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 551 Request: &ingestv1.SelectProfilesRequest{ 552 LabelSelector: `{pod="my-pod"}`, 553 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 554 Start: start.UnixMilli(), 555 End: end.UnixMilli(), 556 }, 557 })) 558 559 resp, err := bidi.Receive() 560 require.NoError(t, err) 561 require.Nil(t, resp.Result) 562 require.Len(t, resp.SelectedProfiles.Fingerprints, 1) 563 require.Len(t, resp.SelectedProfiles.Profiles, 5) 564 565 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 566 Profiles: []bool{true}, 567 })) 568 569 // expect empty resp to signal it is finished 570 resp, err = bidi.Receive() 571 require.NoError(t, err) 572 require.Nil(t, resp.Result) 573 574 // received result 575 resp, err = bidi.Receive() 576 require.NoError(t, err) 577 require.NotNil(t, resp.Result) 578 p, err := profile.ParseUncompressed(resp.Result) 579 require.NoError(t, err) 580 require.Len(t, p.Sample, 48) 581 require.Len(t, p.Location, 287) 582 }) 583 584 t.Run("request non existing series", func(t *testing.T) { 585 bidi := client.MergeProfilesPprof(context.Background()) 586 587 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 588 Request: &ingestv1.SelectProfilesRequest{ 589 LabelSelector: `{pod="not-my-pod"}`, 590 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 591 Start: start.UnixMilli(), 592 End: end.UnixMilli(), 593 }, 594 })) 595 596 // expect empty resp to signal it is finished 597 resp, err := bidi.Receive() 598 require.NoError(t, err) 599 require.Nil(t, resp.Result) 600 require.Nil(t, resp.SelectedProfiles) 601 602 // still receiving a result 603 resp, err = bidi.Receive() 604 require.NoError(t, err) 605 require.NotNil(t, resp.Result) 606 p, err := profile.ParseUncompressed(resp.Result) 607 require.NoError(t, err) 608 require.Len(t, p.Sample, 0) 609 require.Len(t, p.Location, 0) 610 require.Nil(t, resp.SelectedProfiles) 611 }) 612 613 t.Run("empty request fails", func(t *testing.T) { 614 bidi := client.MergeProfilesPprof(context.Background()) 615 616 // It is possible that the error returned by server side of the 617 // stream closes the net.Conn before bidi.Send has finished. The 618 // short timing for that to happen with real HTTP servers makes this 619 // unlikely, but it does happen with the synchronous in memory 620 // net.Pipe() that is used here. 621 // See https://github.com/grafana/pyroscope/issues/3549 for more details. 622 if err := bidi.Send(&ingestv1.MergeProfilesPprofRequest{}); !errors.Is(err, io.EOF) { 623 require.NoError(t, err) 624 } 625 626 _, err := bidi.Receive() 627 require.EqualError(t, err, "invalid_argument: missing initial select request") 628 }) 629 630 t.Run("test cancellation", func(t *testing.T) { 631 ctx, cancel := context.WithCancel(context.Background()) 632 bidi := client.MergeProfilesPprof(ctx) 633 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 634 Request: &ingestv1.SelectProfilesRequest{ 635 LabelSelector: `{pod="my-pod"}`, 636 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 637 Start: start.UnixMilli(), 638 End: end.UnixMilli(), 639 }, 640 })) 641 cancel() 642 }) 643 644 t.Run("test close request", func(t *testing.T) { 645 bidi := client.MergeProfilesPprof(context.Background()) 646 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 647 Request: &ingestv1.SelectProfilesRequest{ 648 LabelSelector: `{pod="my-pod"}`, 649 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 650 Start: start.UnixMilli(), 651 End: end.UnixMilli(), 652 }, 653 })) 654 require.NoError(t, bidi.CloseRequest()) 655 }) 656 657 t.Run("timerange with no Profiles", func(t *testing.T) { 658 bidi := client.MergeProfilesPprof(context.Background()) 659 require.NoError(t, bidi.Send(&ingestv1.MergeProfilesPprofRequest{ 660 Request: &ingestv1.SelectProfilesRequest{ 661 LabelSelector: `{pod="my-pod"}`, 662 Type: mustParseProfileSelector(t, "process_cpu:cpu:nanoseconds:cpu:nanoseconds"), 663 Start: 0, 664 End: 1, 665 }, 666 })) 667 _, err := bidi.Receive() 668 require.NoError(t, err) 669 _, err = bidi.Receive() 670 require.NoError(t, err) 671 }) 672 } 673 674 // See https://github.com/grafana/pyroscope/pull/3356 675 func Test_HeadFlush_DuplicateLabels(t *testing.T) { 676 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 677 678 // ingest some sample data 679 var ( 680 end = time.Unix(0, int64(time.Hour)) 681 start = end.Add(-time.Minute) 682 step = 15 * time.Second 683 ) 684 685 head := newTestHead() 686 687 ingestProfiles(t, head, cpuProfileGenerator, start.UnixNano(), end.UnixNano(), step, 688 defaultAnnotations, 689 &typesv1.LabelPair{Name: "namespace", Value: "my-namespace"}, 690 &typesv1.LabelPair{Name: "pod", Value: "my-pod"}, 691 &typesv1.LabelPair{Name: "pod", Value: "not-my-pod"}, 692 ) 693 } 694 695 // TODO: move into the symbolizer package when available 696 func TestUnsymbolized(t *testing.T) { 697 testCases := []struct { 698 name string 699 profile *profilev1.Profile 700 expectUnsymbolized bool 701 }{ 702 { 703 name: "fully symbolized profile", 704 profile: &profilev1.Profile{ 705 StringTable: []string{"", "a"}, 706 Function: []*profilev1.Function{ 707 {Id: 4, Name: 1}, 708 }, 709 Mapping: []*profilev1.Mapping{ 710 {Id: 239, HasFunctions: true}, 711 }, 712 Location: []*profilev1.Location{ 713 {Id: 5, MappingId: 239, Line: []*profilev1.Line{{FunctionId: 4, Line: 1}}}, 714 }, 715 Sample: []*profilev1.Sample{ 716 {LocationId: []uint64{5}, Value: []int64{1}}, 717 }, 718 }, 719 expectUnsymbolized: false, 720 }, 721 { 722 name: "mapping without functions", 723 profile: &profilev1.Profile{ 724 StringTable: []string{"", "a"}, 725 Function: []*profilev1.Function{ 726 {Id: 4, Name: 1}, 727 }, 728 Mapping: []*profilev1.Mapping{ 729 {Id: 239, HasFunctions: false}, 730 }, 731 Location: []*profilev1.Location{ 732 {Id: 5, MappingId: 239, Line: []*profilev1.Line{{FunctionId: 4, Line: 1}}}, 733 }, 734 Sample: []*profilev1.Sample{ 735 {LocationId: []uint64{5}, Value: []int64{1}}, 736 }, 737 }, 738 expectUnsymbolized: true, 739 }, 740 { 741 name: "multiple locations with mixed symbolization", 742 profile: &profilev1.Profile{ 743 StringTable: []string{"", "a", "b"}, 744 Function: []*profilev1.Function{ 745 {Id: 4, Name: 1}, 746 {Id: 5, Name: 2}, 747 }, 748 Mapping: []*profilev1.Mapping{ 749 {Id: 239, HasFunctions: true}, 750 {Id: 240, HasFunctions: false}, 751 }, 752 Location: []*profilev1.Location{ 753 {Id: 5, MappingId: 239, Line: []*profilev1.Line{{FunctionId: 4, Line: 1}}}, 754 {Id: 6, MappingId: 240, Line: nil}, 755 }, 756 Sample: []*profilev1.Sample{ 757 {LocationId: []uint64{5, 6}, Value: []int64{1}}, 758 }, 759 }, 760 expectUnsymbolized: true, 761 }, 762 } 763 764 for _, tc := range testCases { 765 t.Run(tc.name, func(t *testing.T) { 766 symbols := symdb.NewPartitionWriter(0, &symdb.Config{ 767 Version: symdb.FormatV3, 768 }) 769 symbols.WriteProfileSymbols(tc.profile) 770 unsymbolized := HasUnsymbolizedProfiles(symbols.Symbols()) 771 assert.Equal(t, tc.expectUnsymbolized, unsymbolized) 772 }) 773 } 774 } 775 776 func BenchmarkHeadIngestProfiles(t *testing.B) { 777 var ( 778 profilePaths = []string{ 779 testdataPrefix + "/testdata/heap", 780 testdataPrefix + "/testdata/profile", 781 } 782 profileCount = 0 783 ) 784 785 head := newTestHead() 786 787 t.ReportAllocs() 788 789 for n := 0; n < t.N; n++ { 790 for pos := range profilePaths { 791 p := parseProfile(t, profilePaths[pos]) 792 head.Ingest(p, uuid.New(), []*typesv1.LabelPair{}, defaultAnnotations) 793 profileCount++ 794 } 795 } 796 } 797 798 func newTestHead() *Head { 799 head := NewHead(NewHeadMetricsWithPrefix(nil, "")) 800 return head 801 } 802 803 func parseProfile(t testing.TB, path string) *profilev1.Profile { 804 p, err := pprof.OpenFile(path) 805 require.NoError(t, err, "failed opening profile: ", path) 806 if p.Mapping == nil { 807 // Add fake mappings to some profiles, otherwise query may panic in symdb or return wrong unpredictable results 808 p.Mapping = []*profilev1.Mapping{ 809 {Id: 0}, 810 } 811 } 812 return p.Profile 813 } 814 815 var valueTypeStrings = []string{"unit", "type"} 816 817 func newValueType() *profilev1.ValueType { 818 return &profilev1.ValueType{ 819 Unit: 1, 820 Type: 2, 821 } 822 } 823 824 func newProfileFoo() *profilev1.Profile { 825 baseTable := append([]string{""}, valueTypeStrings...) 826 baseTableLen := int64(len(baseTable)) + 0 827 return &profilev1.Profile{ 828 Function: []*profilev1.Function{ 829 { 830 Id: 1, 831 Name: baseTableLen + 0, 832 }, 833 { 834 Id: 2, 835 Name: baseTableLen + 1, 836 }, 837 }, 838 Location: []*profilev1.Location{ 839 { 840 Id: 1, 841 MappingId: 1, 842 Address: 0x1337, 843 }, 844 { 845 Id: 2, 846 MappingId: 1, 847 Address: 0x1338, 848 }, 849 }, 850 Mapping: []*profilev1.Mapping{ 851 {Id: 1, Filename: baseTableLen + 2}, 852 }, 853 StringTable: append(baseTable, []string{ 854 "func_a", 855 "func_b", 856 "my-foo-binary", 857 }...), 858 TimeNanos: 123456, 859 PeriodType: newValueType(), 860 SampleType: []*profilev1.ValueType{newValueType()}, 861 Sample: []*profilev1.Sample{ 862 { 863 Value: []int64{0o123}, 864 LocationId: []uint64{1}, 865 }, 866 { 867 Value: []int64{1234}, 868 LocationId: []uint64{1, 2}, 869 }, 870 }, 871 } 872 } 873 874 func newProfileBar() *profilev1.Profile { 875 baseTable := append([]string{""}, valueTypeStrings...) 876 baseTableLen := int64(len(baseTable)) + 0 877 return &profilev1.Profile{ 878 Function: []*profilev1.Function{ 879 { 880 Id: 10, 881 Name: baseTableLen + 1, 882 }, 883 { 884 Id: 21, 885 Name: baseTableLen + 0, 886 }, 887 }, 888 Location: []*profilev1.Location{ 889 { 890 Id: 113, 891 MappingId: 1, 892 Address: 0x1337, 893 Line: []*profilev1.Line{ 894 {FunctionId: 10, Line: 1}, 895 }, 896 }, 897 }, 898 Mapping: []*profilev1.Mapping{ 899 {Id: 1, Filename: baseTableLen + 2}, 900 }, 901 StringTable: append(baseTable, []string{ 902 "func_b", 903 "func_a", 904 "my-bar-binary", 905 }...), 906 TimeNanos: 123456, 907 PeriodType: newValueType(), 908 SampleType: []*profilev1.ValueType{newValueType()}, 909 Sample: []*profilev1.Sample{ 910 { 911 Value: []int64{2345}, 912 LocationId: []uint64{113}, 913 }, 914 }, 915 } 916 } 917 918 var streams = []string{"stream-a", "stream-b", "stream-c"} 919 920 func ingestThreeProfileStreams(i int, ingest func(*profilev1.Profile, uuid.UUID, []*typesv1.LabelPair, []*typesv1.ProfileAnnotation)) { 921 p := testhelper.NewProfileBuilder(time.Second.Nanoseconds() * int64(i)) 922 p.CPUProfile() 923 p.WithLabels( 924 "job", "foo", 925 "stream", streams[i%3], 926 ) 927 p.UUID = uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", i)) 928 p.ForStacktraceString("func1", "func2").AddSamples(10) 929 p.ForStacktraceString("func1").AddSamples(20) 930 p.Annotations = []*typesv1.ProfileAnnotation{ 931 {Key: "foo", Value: "bar"}, 932 } 933 934 ingest(p.Profile, p.UUID, p.Labels, p.Annotations) 935 } 936 937 func profileTypeFromProfile(p *profilev1.Profile, stIndex int) *typesv1.ProfileType { 938 t := &typesv1.ProfileType{ 939 SampleType: p.StringTable[p.SampleType[stIndex].Type], 940 SampleUnit: p.StringTable[p.SampleType[stIndex].Unit], 941 PeriodType: p.StringTable[p.PeriodType.Type], 942 PeriodUnit: p.StringTable[p.PeriodType.Unit], 943 } 944 t.ID = fmt.Sprintf(":%s:%s:%s:%s", t.SampleType, t.SampleUnit, t.PeriodType, t.PeriodUnit) 945 return t 946 } 947 948 func compareProfile(t *testing.T, expected *profilev1.Profile, expectedSampleTypeIndex int, actual *profilev1.Profile) { 949 actualCollapsed := bench.StackCollapseProto(actual, 0, 1.0) 950 expectedCollapsed := bench.StackCollapseProto(expected, expectedSampleTypeIndex, 1.0) 951 assert.Equal(t, expectedCollapsed, actualCollapsed) 952 } 953 954 func flushTestHead(t *testing.T, head *Head) phlaredb.Querier { 955 flushed, err := head.Flush(context.Background()) 956 require.NoError(t, err) 957 958 q := createBlockFromFlushedHead(t, flushed) 959 return q 960 } 961 962 func createBlockFromFlushedHead(t *testing.T, flushed *FlushedHead) phlaredb.Querier { 963 dir := t.TempDir() 964 block := testutil2.OpenBlockFromMemory(t, dir, model.TimeFromUnixNano(flushed.Meta.MinTimeNanos), model.TimeFromUnixNano(flushed.Meta.MinTimeNanos), flushed.Profiles, flushed.Index, flushed.Symbols) 965 q := block.Queriers() 966 err := q.Open(context.Background()) 967 require.NoError(t, err) 968 require.Len(t, q, 1) 969 return q[0] 970 } 971 972 func mustParseProfileSelector(t testing.TB, selector string) *typesv1.ProfileType { 973 ps, err := phlaremodel.ParseProfileTypeSelector(selector) 974 require.NoError(t, err) 975 return ps 976 } 977 978 //nolint:unparam 979 func ingestProfiles(b testing.TB, db *Head, generator func(tsNano int64, t testing.TB) (*profilev1.Profile, string), from, to int64, step time.Duration, annotations []*typesv1.ProfileAnnotation, externalLabels ...*typesv1.LabelPair) { 980 b.Helper() 981 for i := from; i <= to; i += int64(step) { 982 p, name := generator(i, b) 983 db.Ingest( 984 p, 985 uuid.New(), 986 append(externalLabels, &typesv1.LabelPair{Name: model.MetricNameLabel, Value: name}), 987 annotations, 988 ) 989 } 990 } 991 992 var cpuProfileGenerator = func(tsNano int64, t testing.TB) (*profilev1.Profile, string) { 993 p := parseProfile(t, testdataPrefix+"/testdata/profile") 994 p.TimeNanos = tsNano 995 return p, "process_cpu" 996 }