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  }