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 }