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 }