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  }