github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/symdb/resolver_pprof_test.go (about)

     1  package symdb
     2  
     3  import (
     4  	"context"
     5  	"slices"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    13  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    14  	v1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    15  )
    16  
    17  func Test_memory_Resolver_ResolvePprof(t *testing.T) {
    18  	s := newMemSuite(t, [][]string{{"testdata/profile.pb.gz"}})
    19  	expectedFingerprint := pprofFingerprint(s.profiles[0], 0)
    20  	r := NewResolver(context.Background(), s.db)
    21  	defer r.Release()
    22  	r.AddSamples(0, s.indexed[0][0].Samples)
    23  	resolved, err := r.Pprof()
    24  	require.NoError(t, err)
    25  	require.Equal(t, expectedFingerprint, pprofFingerprint(resolved, 0))
    26  }
    27  
    28  func Test_block_Resolver_ResolvePprof_multiple_partitions(t *testing.T) {
    29  	s := newBlockSuite(t, [][]string{
    30  		{"testdata/profile.pb.gz"},
    31  		{"testdata/profile.pb.gz"},
    32  	})
    33  	defer s.teardown()
    34  	expectedFingerprint := pprofFingerprint(s.profiles[0], 0)
    35  	for i := range expectedFingerprint {
    36  		expectedFingerprint[i][1] *= 2
    37  	}
    38  	r := NewResolver(context.Background(), s.reader)
    39  	defer r.Release()
    40  	r.AddSamples(0, s.indexed[0][0].Samples)
    41  	r.AddSamples(1, s.indexed[1][0].Samples)
    42  	resolved, err := r.Pprof()
    43  	require.NoError(t, err)
    44  	require.Equal(t, expectedFingerprint, pprofFingerprint(resolved, 0))
    45  }
    46  
    47  func Benchmark_Resolver_ResolvePprof_Small(b *testing.B) {
    48  	s := newMemSuite(b, [][]string{{"testdata/profile.pb.gz"}})
    49  	samples := s.indexed[0][0].Samples
    50  	b.Run("0", benchmarkResolverResolvePprof(s.db, samples, 0))
    51  	b.Run("1K", benchmarkResolverResolvePprof(s.db, samples, 1<<10))
    52  	b.Run("8K", benchmarkResolverResolvePprof(s.db, samples, 8<<10))
    53  }
    54  
    55  func Benchmark_Resolver_ResolvePprof_Big(b *testing.B) {
    56  	s := memSuite{t: b, files: [][]string{{"testdata/big-profile.pb.gz"}}}
    57  	s.config = DefaultConfig().WithDirectory(b.TempDir())
    58  	s.init()
    59  	samples := s.indexed[0][0].Samples
    60  	b.Run("0", benchmarkResolverResolvePprof(s.db, samples, 0))
    61  	b.Run("8K", benchmarkResolverResolvePprof(s.db, samples, 8<<10))
    62  	b.Run("16K", benchmarkResolverResolvePprof(s.db, samples, 16<<10))
    63  	b.Run("32K", benchmarkResolverResolvePprof(s.db, samples, 32<<10))
    64  	b.Run("64K", benchmarkResolverResolvePprof(s.db, samples, 64<<10))
    65  }
    66  
    67  func benchmarkResolverResolvePprof(sym SymbolsReader, samples v1.Samples, n int64) func(b *testing.B) {
    68  	return func(b *testing.B) {
    69  		b.ResetTimer()
    70  		b.ReportAllocs()
    71  		for i := 0; i < b.N; i++ {
    72  			r := NewResolver(context.Background(), sym, WithResolverMaxNodes(n))
    73  			r.AddSamples(0, samples)
    74  			_, _ = r.Pprof()
    75  		}
    76  	}
    77  }
    78  
    79  func Test_Pprof_subtree(t *testing.T) {
    80  	profile := &googlev1.Profile{
    81  		StringTable: []string{"", "a", "b", "c", "d"},
    82  		Function: []*googlev1.Function{
    83  			{Id: 1, Name: 1},
    84  			{Id: 2, Name: 2},
    85  			{Id: 3, Name: 3},
    86  			{Id: 4, Name: 4},
    87  		},
    88  		Mapping: []*googlev1.Mapping{{Id: 1}},
    89  		Location: []*googlev1.Location{
    90  			{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
    91  			{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
    92  			{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
    93  			{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
    94  			{Id: 5, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 4, Line: 1}}}, // d
    95  		},
    96  		Sample: []*googlev1.Sample{
    97  			{LocationId: []uint64{4, 2, 1}, Value: []int64{1}}, // a, b:1, c
    98  			{LocationId: []uint64{3, 1}, Value: []int64{1}},    // a, b:2
    99  			{LocationId: []uint64{4, 1}, Value: []int64{1}},    // a, c
   100  			{LocationId: []uint64{5}, Value: []int64{1}},       // d
   101  		},
   102  	}
   103  
   104  	db := NewSymDB(DefaultConfig().WithDirectory(t.TempDir()))
   105  	w := db.WriteProfileSymbols(0, profile)
   106  	r := NewResolver(context.Background(), db,
   107  		WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   108  			CallSite: []*typesv1.Location{{Name: "a"}, {Name: "b"}},
   109  		}))
   110  
   111  	r.AddSamples(0, w[0].Samples)
   112  	actual, err := r.Pprof()
   113  	require.NoError(t, err)
   114  	// Sample order is not deterministic.
   115  	sort.Slice(actual.Sample, func(i, j int) bool {
   116  		return slices.Compare(actual.Sample[i].LocationId, actual.Sample[j].LocationId) >= 0
   117  	})
   118  
   119  	expected := &googlev1.Profile{
   120  		PeriodType:  &googlev1.ValueType{},
   121  		SampleType:  []*googlev1.ValueType{{}},
   122  		StringTable: []string{"", "a", "b", "c"},
   123  		Function: []*googlev1.Function{
   124  			{Id: 1, Name: 1},
   125  			{Id: 2, Name: 2},
   126  			{Id: 3, Name: 3},
   127  		},
   128  		Mapping: []*googlev1.Mapping{{Id: 1}},
   129  		Location: []*googlev1.Location{
   130  			{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
   131  			{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
   132  			{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
   133  			{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
   134  		},
   135  		Sample: []*googlev1.Sample{
   136  			{LocationId: []uint64{4, 2, 1}, Value: []int64{1}}, // a, b:1, c
   137  			{LocationId: []uint64{3, 1}, Value: []int64{1}},    // a, b:2
   138  		},
   139  	}
   140  
   141  	require.Equal(t, expected, actual)
   142  }
   143  
   144  func Test_Pprof_subtree_multiple_versions(t *testing.T) {
   145  	profile := &googlev1.Profile{
   146  		StringTable: []string{"", "a", "b", "c", "d"},
   147  		Function: []*googlev1.Function{
   148  			{Id: 1, Name: 1},               // a
   149  			{Id: 2, Name: 2},               // b
   150  			{Id: 3, Name: 3},               // c
   151  			{Id: 4, Name: 4, StartLine: 1}, // d
   152  			{Id: 5, Name: 4, StartLine: 2}, // d(2)
   153  		},
   154  		Mapping: []*googlev1.Mapping{{Id: 1}},
   155  		Location: []*googlev1.Location{
   156  			{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
   157  			{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
   158  			{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
   159  			{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
   160  			{Id: 5, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 4, Line: 1}}}, // d
   161  			{Id: 6, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 5, Line: 1}}}, // d(2)
   162  		},
   163  		Sample: []*googlev1.Sample{
   164  			{LocationId: []uint64{5, 4, 2, 1}, Value: []int64{1}}, // a, b:1, c, d
   165  			{LocationId: []uint64{6, 4, 3, 1}, Value: []int64{1}}, // a, b:2, c, d(2)
   166  			{LocationId: []uint64{3, 1}, Value: []int64{1}},       // a, b:2
   167  			{LocationId: []uint64{4, 1}, Value: []int64{1}},       // a, c
   168  			{LocationId: []uint64{5}, Value: []int64{1}},          // d
   169  			{LocationId: []uint64{6}, Value: []int64{1}},          // d (2)
   170  		},
   171  	}
   172  
   173  	db := NewSymDB(DefaultConfig().WithDirectory(t.TempDir()))
   174  	w := db.WriteProfileSymbols(0, profile)
   175  	r := NewResolver(context.Background(), db,
   176  		WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   177  			CallSite: []*typesv1.Location{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"}},
   178  		}))
   179  
   180  	r.AddSamples(0, w[0].Samples)
   181  	actual, err := r.Pprof()
   182  	require.NoError(t, err)
   183  	// Sample order is not deterministic.
   184  	sort.Slice(actual.Sample, func(i, j int) bool {
   185  		return slices.Compare(actual.Sample[i].LocationId, actual.Sample[j].LocationId) >= 0
   186  	})
   187  
   188  	expected := &googlev1.Profile{
   189  		PeriodType:  &googlev1.ValueType{},
   190  		SampleType:  []*googlev1.ValueType{{}},
   191  		StringTable: []string{"", "a", "b", "c", "d"},
   192  		Function: []*googlev1.Function{
   193  			{Id: 1, Name: 1},               // a
   194  			{Id: 2, Name: 2},               // b
   195  			{Id: 3, Name: 3},               // c
   196  			{Id: 4, Name: 4, StartLine: 1}, // d
   197  			{Id: 5, Name: 4, StartLine: 2}, // d(2)
   198  		},
   199  		Mapping: []*googlev1.Mapping{{Id: 1}},
   200  		Location: []*googlev1.Location{
   201  			{Id: 1, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 1, Line: 1}}}, // a
   202  			{Id: 2, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 1}}}, // b:1
   203  			{Id: 3, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 2, Line: 2}}}, // b:2
   204  			{Id: 4, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 3, Line: 1}}}, // c
   205  			{Id: 5, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 4, Line: 1}}}, // d
   206  			{Id: 6, MappingId: 1, Line: []*googlev1.Line{{FunctionId: 5, Line: 1}}}, // d(2)
   207  		},
   208  		Sample: []*googlev1.Sample{
   209  			{LocationId: []uint64{6, 4, 3, 1}, Value: []int64{1}}, // a, b:2, c, d(2)
   210  			{LocationId: []uint64{5, 4, 2, 1}, Value: []int64{1}}, // a, b:1, c, d
   211  		},
   212  	}
   213  
   214  	require.Equal(t, expected, actual)
   215  }
   216  
   217  func Test_Resolver_pprof_options(t *testing.T) {
   218  	s := newMemSuite(t, [][]string{{"testdata/profile.pb.gz"}})
   219  	samples := s.indexed[0][0].Samples
   220  	const samplesTotal = 561
   221  
   222  	var sc PartitionStats
   223  	s.db.partitions[0].WriteStats(&sc)
   224  	t.Logf("%#v\n", sc)
   225  
   226  	type testCase struct {
   227  		name     string
   228  		expected int
   229  		options  []ResolverOption
   230  	}
   231  
   232  	testCases := []testCase{
   233  		{
   234  			name:     "no options",
   235  			expected: samplesTotal,
   236  		},
   237  		{
   238  			name:     "0 max nodes",
   239  			expected: samplesTotal,
   240  			options: []ResolverOption{
   241  				WithResolverMaxNodes(0),
   242  			},
   243  		},
   244  		{
   245  			name:     "10 max nodes",
   246  			expected: 22,
   247  			options: []ResolverOption{
   248  				WithResolverMaxNodes(10),
   249  			},
   250  		},
   251  
   252  		{
   253  			name:     "callSite",
   254  			expected: 54,
   255  			options: []ResolverOption{
   256  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   257  					CallSite: []*typesv1.Location{{Name: "runtime.main"}},
   258  				}),
   259  			},
   260  		},
   261  		{
   262  			name:     "callSite 10 max nodes",
   263  			expected: 14,
   264  			options: []ResolverOption{
   265  				WithResolverMaxNodes(10),
   266  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   267  					CallSite: []*typesv1.Location{{Name: "runtime.main"}},
   268  				}),
   269  			},
   270  		},
   271  		{
   272  			name:     "nil StackTraceSelector",
   273  			expected: samplesTotal,
   274  			options: []ResolverOption{
   275  				WithResolverStackTraceSelector(nil),
   276  			},
   277  		},
   278  		{
   279  			name:     "nil StackTraceSelector 10 max nodes",
   280  			expected: 22,
   281  			options: []ResolverOption{
   282  				WithResolverMaxNodes(10),
   283  				WithResolverStackTraceSelector(nil),
   284  			},
   285  		},
   286  		{
   287  			name:     "empty StackTraceSelector.CallSite",
   288  			expected: samplesTotal,
   289  			options: []ResolverOption{
   290  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   291  					CallSite: []*typesv1.Location{},
   292  				}),
   293  			},
   294  		},
   295  		{
   296  			name:     "StackTraceSelector GoPGO empty",
   297  			expected: samplesTotal,
   298  			options: []ResolverOption{
   299  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   300  					GoPgo: &typesv1.GoPGO{},
   301  				}),
   302  			},
   303  		},
   304  		{
   305  			name:     "StackTraceSelector GoPGO takes precedence",
   306  			expected: 414,
   307  			options: []ResolverOption{
   308  				WithResolverMaxNodes(10),
   309  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   310  					CallSite: []*typesv1.Location{{Name: "runtime.main"}},
   311  					GoPgo: &typesv1.GoPGO{
   312  						KeepLocations: 5,
   313  					},
   314  				}),
   315  			},
   316  		},
   317  		{
   318  			name:     "GoPGO KeepLocations 5",
   319  			expected: 414,
   320  			options: []ResolverOption{
   321  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   322  					GoPgo: &typesv1.GoPGO{
   323  						KeepLocations: 5,
   324  					},
   325  				}),
   326  			},
   327  		},
   328  		{
   329  			name:     "GoPGO AggregateCallees",
   330  			expected: 442,
   331  			options: []ResolverOption{
   332  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   333  					GoPgo: &typesv1.GoPGO{
   334  						AggregateCallees: true,
   335  					},
   336  				}),
   337  			},
   338  		},
   339  		{
   340  			name:     "GoPGO AggregateCallees KeepLocations 5",
   341  			expected: 316,
   342  			options: []ResolverOption{
   343  				WithResolverStackTraceSelector(&typesv1.StackTraceSelector{
   344  					GoPgo: &typesv1.GoPGO{
   345  						KeepLocations:    5,
   346  						AggregateCallees: true,
   347  					},
   348  				}),
   349  			},
   350  		},
   351  	}
   352  
   353  	for _, tc := range testCases {
   354  		tc := tc
   355  		t.Run(tc.name, func(t *testing.T) {
   356  			r := NewResolver(context.Background(), s.db, tc.options...)
   357  			defer r.Release()
   358  			r.AddSamples(0, samples)
   359  			p, err := r.Pprof()
   360  			require.NoError(t, err)
   361  			assert.Equal(t, tc.expected, len(p.Sample))
   362  
   363  			var sum int64
   364  			for _, x := range p.Sample {
   365  				sum += x.Value[0]
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  // The test examines how strings are copied from the Symbols
   372  // to the Profile at resolve.
   373  //
   374  // We used to have an issue that the first string is not empty,
   375  // as it's required by the pprof format:
   376  // https://github.com/grafana/pyroscope/issues/3199
   377  func Test_Resolver_pprof_strings(t *testing.T) {
   378  	type testCase struct {
   379  		name     string
   380  		symbols  []string
   381  		profile  *googlev1.Profile
   382  		expected *googlev1.Profile
   383  	}
   384  
   385  	testCases := []testCase{
   386  		{
   387  			name:    "normal_sparse",
   388  			symbols: []string{"", "foo", "baz", "bar"},
   389  			profile: &googlev1.Profile{
   390  				Mapping: []*googlev1.Mapping{{
   391  					Filename: 1, // foo
   392  					BuildId:  0, // ""
   393  				}},
   394  				Function: []*googlev1.Function{{
   395  					Name:       3, // bar
   396  					SystemName: 3,
   397  					Filename:   3,
   398  				}},
   399  			},
   400  			expected: &googlev1.Profile{
   401  				StringTable: []string{"", "foo", "bar"},
   402  				Mapping: []*googlev1.Mapping{{
   403  					Filename: 1, // foo
   404  					BuildId:  0,
   405  				}},
   406  				Function: []*googlev1.Function{{
   407  					Name:       2, // bar
   408  					SystemName: 2,
   409  					Filename:   2,
   410  				}},
   411  			},
   412  		},
   413  		{
   414  			name:    "normal_dense",
   415  			symbols: []string{"", "foo", "bar"},
   416  			profile: &googlev1.Profile{
   417  				Mapping: []*googlev1.Mapping{{
   418  					Filename: 1, // foo
   419  					BuildId:  0, // ""
   420  				}},
   421  				Function: []*googlev1.Function{{
   422  					Name:       2, // bar
   423  					SystemName: 2,
   424  					Filename:   2,
   425  				}},
   426  			},
   427  			expected: &googlev1.Profile{
   428  				StringTable: []string{"", "foo", "bar"},
   429  				Mapping: []*googlev1.Mapping{{
   430  					Filename: 1, // foo
   431  					BuildId:  0,
   432  				}},
   433  				Function: []*googlev1.Function{{
   434  					Name:       2, // bar
   435  					SystemName: 2,
   436  					Filename:   2,
   437  				}},
   438  			},
   439  		},
   440  		{
   441  			name:    "no_zero_sparse",
   442  			symbols: []string{"foo", "baz", "fred", "bar"},
   443  			profile: &googlev1.Profile{
   444  				Mapping: []*googlev1.Mapping{{
   445  					Filename: 0, // foo
   446  					BuildId:  1, // baz
   447  				}},
   448  				Function: []*googlev1.Function{{
   449  					Name:       3, // bar
   450  					SystemName: 3,
   451  					Filename:   3,
   452  				}},
   453  			},
   454  			expected: &googlev1.Profile{
   455  				StringTable: []string{"", "foo", "baz", "bar"},
   456  				Mapping: []*googlev1.Mapping{{
   457  					Filename: 1, // foo
   458  					BuildId:  2, // baz
   459  				}},
   460  				Function: []*googlev1.Function{{
   461  					Name:       3, // bar
   462  					SystemName: 3,
   463  					Filename:   3,
   464  				}},
   465  			},
   466  		},
   467  		{
   468  			name:    "no_zero_dense",
   469  			symbols: []string{"foo", "baz", "bar"},
   470  			profile: &googlev1.Profile{
   471  				Mapping: []*googlev1.Mapping{{
   472  					Filename: 0, // foo
   473  					BuildId:  1, // baz
   474  				}},
   475  				Function: []*googlev1.Function{{
   476  					Name:       2, // bar
   477  					SystemName: 2,
   478  					Filename:   2,
   479  				}},
   480  			},
   481  			expected: &googlev1.Profile{
   482  				StringTable: []string{"", "foo", "baz", "bar"},
   483  				Mapping: []*googlev1.Mapping{{
   484  					Filename: 1, // foo
   485  					BuildId:  2, // baz
   486  				}},
   487  				Function: []*googlev1.Function{{
   488  					Name:       3, // bar
   489  					SystemName: 3,
   490  					Filename:   3,
   491  				}},
   492  			},
   493  		},
   494  		{
   495  			name:    "unordered_dense",
   496  			symbols: []string{"foo", "baz", "", "bar"},
   497  			profile: &googlev1.Profile{
   498  				Mapping: []*googlev1.Mapping{{
   499  					Filename: 0, // foo
   500  					BuildId:  2, // ""
   501  				}},
   502  				Function: []*googlev1.Function{{
   503  					Name:       3, // bar
   504  					SystemName: 3,
   505  					Filename:   3,
   506  				}},
   507  			},
   508  			expected: &googlev1.Profile{
   509  				StringTable: []string{"", "foo", "bar"},
   510  				Mapping: []*googlev1.Mapping{{
   511  					Filename: 1, // foo
   512  					BuildId:  0, //
   513  				}},
   514  				Function: []*googlev1.Function{{
   515  					Name:       2, // bar
   516  					SystemName: 2,
   517  					Filename:   2,
   518  				}},
   519  			},
   520  		},
   521  		{
   522  			name:    "unordered_sparse",
   523  			symbols: []string{"foo", "fred", "baz", "", "bar"},
   524  			profile: &googlev1.Profile{
   525  				Mapping: []*googlev1.Mapping{{
   526  					Filename: 0, // foo
   527  					BuildId:  3, // ""
   528  				}},
   529  				Function: []*googlev1.Function{{
   530  					Name:       4, // bar
   531  					SystemName: 4,
   532  					Filename:   4,
   533  				}},
   534  			},
   535  			expected: &googlev1.Profile{
   536  				StringTable: []string{"", "foo", "bar"},
   537  				Mapping: []*googlev1.Mapping{{
   538  					Filename: 1, // foo
   539  					BuildId:  0, //
   540  				}},
   541  				Function: []*googlev1.Function{{
   542  					Name:       2, // bar
   543  					SystemName: 2,
   544  					Filename:   2,
   545  				}},
   546  			},
   547  		},
   548  	}
   549  
   550  	for _, tc := range testCases {
   551  		tc := tc
   552  		t.Run(tc.name, func(t *testing.T) {
   553  			s := &Symbols{Strings: tc.symbols}
   554  			copyStrings(tc.profile, s, nil)
   555  			require.Equal(t, tc.expected, tc.profile)
   556  		})
   557  	}
   558  }