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

     1  package symdb
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/require"
     9  
    10  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    11  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    12  	v1 "github.com/grafana/pyroscope/pkg/phlaredb/schemas/v1"
    13  )
    14  
    15  func Test_memory_Resolver_ResolveTree(t *testing.T) {
    16  	s := newMemSuite(t, [][]string{{"testdata/profile.pb.gz"}})
    17  	expectedFingerprint := pprofFingerprint(s.profiles[0], 0)
    18  
    19  	t.Run("default", func(t *testing.T) {
    20  		r := NewResolver(context.Background(), s.db)
    21  		defer r.Release()
    22  		r.AddSamples(0, s.indexed[0][0].Samples)
    23  		resolved, err := r.Tree()
    24  		require.NoError(t, err)
    25  		require.Equal(t, expectedFingerprint, treeFingerprint(resolved))
    26  	})
    27  
    28  	for _, tc := range []struct {
    29  		name            string
    30  		callsite        []string
    31  		stacktraceCount int
    32  		total           int
    33  	}{
    34  		{
    35  			name: "multiple stacks",
    36  			callsite: []string{
    37  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*scrapeLoop).run",
    38  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*Target).report",
    39  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*scrapeLoop).scrape",
    40  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*pprofWriter).writeProfile",
    41  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*cache).writeProfiles",
    42  				"github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie.(*Trie).Insert",
    43  			},
    44  			stacktraceCount: 4,
    45  			total:           2752628,
    46  		},
    47  		{
    48  			name: "single stack",
    49  			callsite: []string{
    50  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*scrapeLoop).run",
    51  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*Target).report",
    52  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*scrapeLoop).scrape",
    53  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*pprofWriter).writeProfile",
    54  				"github.com/pyroscope-io/pyroscope/pkg/scrape.(*cache).writeProfiles",
    55  				"github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie.(*Trie).Insert",
    56  				"github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie.(*trieNode).findNodeAt",
    57  				"github.com/pyroscope-io/pyroscope/pkg/structs/transporttrie.newTrieNode",
    58  			},
    59  			stacktraceCount: 1,
    60  			total:           417817,
    61  		},
    62  		{
    63  			name: "no match",
    64  			callsite: []string{
    65  				"github.com/no-match/no-match.main",
    66  			},
    67  			stacktraceCount: 0,
    68  			total:           0,
    69  		},
    70  	} {
    71  		t.Run("with stack trace selector/"+tc.name, func(t *testing.T) {
    72  			sts := &typesv1.StackTraceSelector{
    73  				CallSite: make([]*typesv1.Location, len(tc.callsite)),
    74  			}
    75  			for i, name := range tc.callsite {
    76  				sts.CallSite[i] = &typesv1.Location{
    77  					Name: name,
    78  				}
    79  			}
    80  
    81  			r := NewResolver(context.Background(), s.db, WithResolverStackTraceSelector(sts), WithResolverMaxNodes(10))
    82  			defer r.Release()
    83  			r.AddSamples(0, s.indexed[0][0].Samples)
    84  			resolved, err := r.Tree()
    85  			require.NoError(t, err)
    86  
    87  			stacktraceCount := 0
    88  			total := 0
    89  
    90  			resolved.IterateStacks(func(name string, self int64, stack []string) {
    91  				stacktraceCount++
    92  				total += int(self)
    93  
    94  				prefix := make([]string, len(tc.callsite))
    95  				for i := range prefix {
    96  					prefix[i] = stack[len(stack)-1-i]
    97  				}
    98  				require.Equal(t, tc.callsite, prefix, "stack prefix doesn't match")
    99  			})
   100  			assert.Equal(t, tc.stacktraceCount, stacktraceCount)
   101  			assert.Equal(t, tc.total, total)
   102  		})
   103  	}
   104  }
   105  
   106  func Test_block_Resolver_ResolveTree(t *testing.T) {
   107  	s := newBlockSuite(t, [][]string{{"testdata/profile.pb.gz"}})
   108  	defer s.teardown()
   109  	expectedFingerprint := pprofFingerprint(s.profiles[0], 1)
   110  	r := NewResolver(context.Background(), s.reader)
   111  	defer r.Release()
   112  	r.AddSamples(0, s.indexed[0][1].Samples)
   113  	resolved, err := r.Tree()
   114  	require.NoError(t, err)
   115  	require.Equal(t, expectedFingerprint, treeFingerprint(resolved))
   116  }
   117  
   118  func Benchmark_Resolver_ResolveTree_Small(b *testing.B) {
   119  	s := newMemSuite(b, [][]string{{"testdata/profile.pb.gz"}})
   120  	samples := s.indexed[0][0].Samples
   121  	b.Run("0", benchmarkResolverResolveTree(s.db, samples, 0))
   122  	b.Run("1K", benchmarkResolverResolveTree(s.db, samples, 1<<10))
   123  	b.Run("8K", benchmarkResolverResolveTree(s.db, samples, 8<<10))
   124  }
   125  
   126  func Benchmark_Resolver_ResolveTree_Big(b *testing.B) {
   127  	s := newMemSuite(b, [][]string{{"testdata/big-profile.pb.gz"}})
   128  	samples := s.indexed[0][0].Samples
   129  	b.Run("0", benchmarkResolverResolveTree(s.db, samples, 0))
   130  	b.Run("8K", benchmarkResolverResolveTree(s.db, samples, 8<<10))
   131  	b.Run("16K", benchmarkResolverResolveTree(s.db, samples, 16<<10))
   132  	b.Run("32K", benchmarkResolverResolveTree(s.db, samples, 32<<10))
   133  	b.Run("64K", benchmarkResolverResolveTree(s.db, samples, 64<<10))
   134  }
   135  
   136  func benchmarkResolverResolveTree(sym SymbolsReader, samples v1.Samples, n int64) func(b *testing.B) {
   137  	return func(b *testing.B) {
   138  		b.ResetTimer()
   139  		b.ReportAllocs()
   140  		for i := 0; i < b.N; i++ {
   141  			r := NewResolver(context.Background(), sym, WithResolverMaxNodes(n))
   142  			r.AddSamples(0, samples)
   143  			_, _ = r.Tree()
   144  		}
   145  	}
   146  }
   147  
   148  func Test_memory_Resolver_ResolveTree_copied_nodes(t *testing.T) {
   149  	s := newMemSuite(t, [][]string{{"testdata/big-profile.pb.gz"}})
   150  	samples := s.indexed[0][0].Samples
   151  
   152  	resolve := func(options ...ResolverOption) (nodes, total int64) {
   153  		r := NewResolver(context.Background(), s.db, options...)
   154  		defer r.Release()
   155  		r.AddSamples(0, samples)
   156  		resolved, err := r.Tree()
   157  		require.NoError(t, err)
   158  		resolved.FormatNodeNames(func(s string) string {
   159  			nodes++
   160  			return s
   161  		})
   162  		return nodes, resolved.Total()
   163  	}
   164  
   165  	const maxNodes int64 = 16 << 10
   166  	nodesFull, totalFull := resolve()
   167  	nodesTrunc, totalTrunc := resolve(WithResolverMaxNodes(maxNodes))
   168  	// The only reason we perform this assertion is to make sure that
   169  	// truncation did take place, and the number of nodes is close to
   170  	// the target (we actually keep all nodes with top 16K values).
   171  	assert.Equal(t, int64(1585462), nodesFull)
   172  	assert.Equal(t, int64(22461), nodesTrunc)
   173  	require.Equal(t, totalFull, totalTrunc)
   174  }
   175  
   176  func Test_buildTreeFromParentPointerTrees(t *testing.T) {
   177  	// The profile has the following samples:
   178  	//
   179  	//	a b c f f1 f2 f3 f4 f5
   180  	//	1 2 3 4 5  6  7  8  9
   181  	//
   182  	//	4: a b c f
   183  	//	5: a b c f1
   184  	//	6: a b c f1 f2
   185  	//	8: a b c f3 f4
   186  	//	9: a b c f3 f4 f5
   187  	//
   188  	expectedSamples := v1.Samples{
   189  		StacktraceIDs: []uint32{4, 5, 6, 8, 9},
   190  		Values:        []uint64{1, 1, 1, 1, 1},
   191  	}
   192  
   193  	// After the truncation, we expect to see the following tree
   194  	// (function f, f2, and f5 are replaced with "other"):
   195  	const maxNodes = 6
   196  	expectedTruncatedTree := `.
   197  └── a: self 0 total 5
   198      └── b: self 0 total 5
   199          └── c: self 0 total 5
   200              ├── f1: self 1 total 2
   201              │   └── other: self 1 total 1
   202              ├── f3: self 0 total 2
   203              │   └── f4: self 1 total 2
   204              │       └── other: self 1 total 1
   205              └── other: self 1 total 1
   206  `
   207  
   208  	p := &profilev1.Profile{
   209  		Sample: []*profilev1.Sample{
   210  			{LocationId: []uint64{4, 3, 2, 1}, Value: []int64{1}},
   211  			{LocationId: []uint64{5, 3, 2, 1}, Value: []int64{1}},
   212  			{LocationId: []uint64{6, 5, 3, 2, 1}, Value: []int64{1}},
   213  			{LocationId: []uint64{8, 7, 3, 2, 1}, Value: []int64{1}},
   214  			{LocationId: []uint64{9, 8, 7, 3, 2, 1}, Value: []int64{1}},
   215  		},
   216  		StringTable: []string{
   217  			"", "a", "b", "c", "f", "f1", "f2", "f3", "f4", "f5",
   218  		},
   219  	}
   220  
   221  	names := uint64(len(p.StringTable))
   222  	for i := uint64(1); i < names; i++ {
   223  		p.Location = append(p.Location, &profilev1.Location{
   224  			Id: i, Line: []*profilev1.Line{{FunctionId: i}},
   225  		})
   226  		p.Function = append(p.Function, &profilev1.Function{
   227  			Id: i, Name: int64(i),
   228  		})
   229  	}
   230  
   231  	s := newMemSuite(t, nil)
   232  	const partition = 0
   233  	indexed := s.db.WriteProfileSymbols(partition, p)
   234  	assert.Equal(t, expectedSamples, indexed[partition].Samples)
   235  	b := blockSuite{memSuite: s}
   236  	b.flush()
   237  	pr, err := b.reader.Partition(context.Background(), partition)
   238  	require.NoError(t, err)
   239  	symbols := pr.Symbols()
   240  	iterator, ok := symbols.Stacktraces.(StacktraceIDRangeIterator)
   241  	require.True(t, ok)
   242  
   243  	for _, tc := range []struct {
   244  		name     string
   245  		selector *typesv1.StackTraceSelector
   246  		expected string
   247  	}{
   248  		{
   249  			name:     "without selection",
   250  			selector: nil,
   251  			expected: expectedTruncatedTree,
   252  		},
   253  		{
   254  			name: "with common prefix selection",
   255  			selector: &typesv1.StackTraceSelector{
   256  				CallSite: []*typesv1.Location{
   257  					{Name: "a"},
   258  					{Name: "b"},
   259  					{Name: "c"},
   260  				},
   261  			},
   262  			expected: expectedTruncatedTree,
   263  		},
   264  		{
   265  			name: "with focus on truncated callsite last shown",
   266  			selector: &typesv1.StackTraceSelector{
   267  				CallSite: []*typesv1.Location{
   268  					{Name: "a"},
   269  					{Name: "b"},
   270  					{Name: "c"},
   271  					{Name: "f1"},
   272  				},
   273  			},
   274  			expected: `.
   275  └── a: self 0 total 2
   276      └── b: self 0 total 2
   277          └── c: self 0 total 2
   278              └── f1: self 1 total 2
   279                  └── f2: self 1 total 1
   280  `,
   281  		},
   282  		{
   283  			name: "with focus on truncated callsite",
   284  			selector: &typesv1.StackTraceSelector{
   285  				CallSite: []*typesv1.Location{
   286  					{Name: "a"},
   287  					{Name: "b"},
   288  					{Name: "c"},
   289  					{Name: "f1"},
   290  					{Name: "f2"},
   291  				},
   292  			},
   293  			expected: `.
   294  └── a: self 0 total 1
   295      └── b: self 0 total 1
   296          └── c: self 0 total 1
   297              └── f1: self 0 total 1
   298                  └── f2: self 1 total 1
   299  `,
   300  		},
   301  	} {
   302  		t.Run(tc.name, func(t *testing.T) {
   303  			appender := NewSampleAppender()
   304  			appender.AppendMany(expectedSamples.StacktraceIDs, expectedSamples.Values)
   305  			ranges := iterator.SplitStacktraceIDRanges(appender)
   306  			resolved, err := buildTreeFromParentPointerTrees(context.Background(), ranges, symbols, maxNodes, SelectStackTraces(symbols, tc.selector))
   307  			require.NoError(t, err)
   308  
   309  			require.Equal(t, tc.expected, resolved.String())
   310  		})
   311  	}
   312  }