github.com/grafana/pyroscope@v1.18.0/pkg/pprof/merge_test.go (about) 1 package pprof 2 3 import ( 4 "crypto/md5" 5 "encoding/hex" 6 "fmt" 7 "net" 8 "os" 9 "path/filepath" 10 "sort" 11 "strings" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/google/pprof/profile" 17 "github.com/stretchr/testify/require" 18 19 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 20 "github.com/grafana/pyroscope/pkg/testhelper" 21 ) 22 23 func Test_Merge_Single(t *testing.T) { 24 p, err := OpenFile("testdata/go.cpu.labels.pprof") 25 require.NoError(t, err) 26 var m ProfileMerge 27 require.NoError(t, m.Merge(p.CloneVT(), true)) 28 sortLabels(p.Profile) 29 act := m.Profile() 30 exp := p.Profile 31 testhelper.EqualProto(t, exp, act) 32 } 33 34 func sortLabels(p *profilev1.Profile) { 35 for _, s := range p.Sample { 36 sort.Sort(LabelsByKeyValue(s.Label)) 37 } 38 } 39 40 type fuzzEvent byte 41 42 const ( 43 fuzzEventUnknown fuzzEvent = iota 44 fuzzEventPostDecode 45 fuzzEventPostMerge 46 ) 47 48 type eventSocket struct { 49 lck sync.Mutex 50 fMap map[string]net.Conn 51 } 52 53 var eventWriter = &eventSocket{ 54 fMap: make(map[string]net.Conn), 55 } 56 57 func eventWrite(t *testing.T, msg []byte) { 58 eventWriter.lck.Lock() 59 c, ok := eventWriter.fMap[eventName(t)] 60 if !ok { 61 var err error 62 c, err = net.Dial("unix", eventPath(t)) 63 if err != nil { 64 eventWriter.lck.Unlock() 65 t.Fatalf("error connecting: %v", err) 66 return 67 } 68 eventWriter.fMap[eventName(t)] = c 69 } 70 eventWriter.lck.Unlock() 71 _, err := c.Write(msg) 72 73 require.NoError(t, err) 74 } 75 76 func eventName(t testing.TB) string { 77 return strings.Split(t.Name(), "/")[0] 78 } 79 80 func eventPath(t testing.TB) string { 81 hash := md5.Sum([]byte(eventName(t))) 82 p := filepath.Join(os.TempDir(), hex.EncodeToString(hash[:])+"-fuzz-events.sock") 83 return p 84 } 85 86 func isFuzzWorker() bool { 87 for _, arg := range os.Args { 88 if arg == "-test.fuzzworker" { 89 return true 90 } 91 if arg == "-fuzzworker" { 92 return true 93 } 94 } 95 return false 96 } 97 98 // runEventsGatherer starts a server that listens for events from the fuzzing worker processes. This allows us to gather additional metrics on how successful the fuzzing is with finding valid profiles. 99 func runEventsGatherer(t testing.TB) { 100 fPath := eventPath(t) 101 _ = os.Remove(fPath) 102 socket, err := net.Listen("unix", fPath) 103 require.NoError(t, err) 104 t.Cleanup(func() { 105 socket.Close() 106 _ = os.Remove(fPath) 107 }) 108 109 eventCh := make(chan fuzzEvent, 32) 110 go func() { 111 for { 112 conn, err := socket.Accept() 113 if err != nil { 114 return 115 } 116 go func(conn net.Conn) { 117 defer conn.Close() 118 buf := make([]byte, 1024) 119 for { 120 n, err := conn.Read(buf) 121 if err != nil { 122 return 123 } 124 for _, b := range buf[:n] { 125 eventCh <- fuzzEvent(b) 126 } 127 } 128 }(conn) 129 } 130 }() 131 132 go func() { 133 ticker := time.NewTicker(3 * time.Second) 134 stdout := os.Stdout 135 defer ticker.Stop() 136 var totalPostDecode, totalPostMerge int64 137 var lastPostDecode, lastPostMerge int64 138 for { 139 select { 140 case <-ticker.C: 141 fmt.Fprintf(stdout, "postDecode: %d/%d (last 3s, total) postMerge %d/%d (last 3s, total)\n", totalPostDecode-lastPostDecode, totalPostDecode, totalPostMerge-lastPostMerge, totalPostMerge) 142 lastPostDecode = totalPostDecode 143 lastPostMerge = totalPostMerge 144 case event := <-eventCh: 145 switch event { 146 case fuzzEventPostDecode: 147 totalPostDecode += 1 148 case fuzzEventPostMerge: 149 totalPostMerge += 1 150 } 151 } 152 } 153 }() 154 } 155 156 func Fuzz_Merge_Single(f *testing.F) { 157 // setup event handler (only in main process) 158 if !isFuzzWorker() { 159 runEventsGatherer(f) 160 } 161 162 for _, file := range []string{ 163 "testdata/go.cpu.labels.pprof", 164 "testdata/heap", 165 "testdata/profile_java", 166 "testdata/profile_rust", 167 } { 168 raw, err := OpenFile(file) 169 require.NoError(f, err) 170 data, err := raw.MarshalVT() 171 require.NoError(f, err) 172 f.Add(data) 173 } 174 175 f.Fuzz(func(t *testing.T, data []byte) { 176 var p profilev1.Profile 177 err := p.UnmarshalVT(data) 178 if err != nil { 179 return 180 } 181 182 eventWrite(t, []byte{byte(fuzzEventPostDecode)}) 183 var m ProfileMerge 184 err = m.Merge(&p, true) 185 if err != nil { 186 return 187 } 188 eventWrite(t, []byte{byte(fuzzEventPostMerge)}) 189 }) 190 } 191 192 func Test_Merge_Self(t *testing.T) { 193 p, err := OpenFile("testdata/go.cpu.labels.pprof") 194 require.NoError(t, err) 195 var m ProfileMerge 196 require.NoError(t, m.Merge(p.CloneVT(), true)) 197 require.NoError(t, m.Merge(p.CloneVT(), true)) 198 for i := range p.Sample { 199 s := p.Sample[i] 200 for j := range s.Value { 201 s.Value[j] *= 2 202 } 203 } 204 p.DurationNanos *= 2 205 sortLabels(p.Profile) 206 testhelper.EqualProto(t, p.Profile, m.Profile()) 207 } 208 209 func Test_Merge_Halves(t *testing.T) { 210 p, err := OpenFile("testdata/go.cpu.labels.pprof") 211 require.NoError(t, err) 212 213 a := p.CloneVT() 214 b := p.CloneVT() 215 n := len(p.Sample) / 2 216 a.Sample = a.Sample[:n] 217 b.Sample = b.Sample[n:] 218 219 var m ProfileMerge 220 require.NoError(t, m.Merge(a, true)) 221 require.NoError(t, m.Merge(b, true)) 222 223 // Merge with self for normalisation. 224 var sm ProfileMerge 225 require.NoError(t, sm.Merge(p.CloneVT(), true)) 226 p.DurationNanos *= 2 227 228 sortLabels(p.Profile) 229 testhelper.EqualProto(t, p.Profile, m.Profile()) 230 } 231 232 func Test_Merge_Sample(t *testing.T) { 233 stringTable := []string{ 234 "", 235 "samples", 236 "count", 237 "cpu", 238 "nanoseconds", 239 "foo", 240 "bar", 241 "profile_id", 242 "c717c11b87121639", 243 "function", 244 "slow", 245 "8c946fa4ae322f7f", 246 "fast", 247 "main.work", 248 "/Users/kolesnikovae/Documents/src/pyroscope/examples/golang-push/simple/main.go", 249 "main.slowFunction.func1", 250 "runtime/pprof.Do", 251 "/usr/local/go/src/runtime/pprof/runtime.go", 252 "main.slowFunction", 253 "main.main.func2", 254 "github.com/pyroscope-io/client/pyroscope.TagWrapper.func1", 255 "/Users/kolesnikovae/go/pkg/mod/github.com/pyroscope-io/client@v0.2.4-0.20220607180407-0ba26860ce5b/pyroscope/api.go", 256 "github.com/pyroscope-io/client/pyroscope.TagWrapper", 257 "main.main", 258 "runtime.main", 259 "/usr/local/go/src/runtime/proc.go", 260 "main.fastFunction.func1", 261 "main.fastFunction", 262 } 263 264 a := &profilev1.Profile{ 265 SampleType: []*profilev1.ValueType{ 266 { 267 Type: 1, 268 Unit: 2, 269 }, 270 { 271 Type: 3, 272 Unit: 4, 273 }, 274 }, 275 Sample: []*profilev1.Sample{ 276 { 277 LocationId: []uint64{1, 2, 3}, 278 Value: []int64{1, 10000000}, 279 Label: []*profilev1.Label{ 280 {Key: 5, Str: 6}, 281 {Key: 7, Str: 8}, 282 {Key: 9, Str: 10}, 283 }, 284 }, 285 }, 286 Mapping: []*profilev1.Mapping{ 287 { 288 Id: 1, 289 HasFunctions: true, 290 }, 291 }, 292 Location: []*profilev1.Location{ 293 { 294 Id: 1, 295 MappingId: 1, 296 Address: 19497668, 297 Line: []*profilev1.Line{{FunctionId: 1, Line: 19}}, 298 }, 299 { 300 Id: 2, 301 MappingId: 1, 302 Address: 19498429, 303 Line: []*profilev1.Line{{FunctionId: 2, Line: 43}}, 304 }, 305 { 306 Id: 3, 307 MappingId: 1, 308 Address: 19267106, 309 Line: []*profilev1.Line{{FunctionId: 3, Line: 40}}, 310 }, 311 }, 312 Function: []*profilev1.Function{ 313 { 314 Id: 1, 315 Name: 13, 316 SystemName: 13, 317 Filename: 14, 318 }, 319 { 320 Id: 2, 321 Name: 15, 322 SystemName: 15, 323 Filename: 14, 324 }, 325 { 326 Id: 3, 327 Name: 16, 328 SystemName: 16, 329 Filename: 17, 330 }, 331 }, 332 StringTable: stringTable, 333 TimeNanos: 1654798932062349000, 334 DurationNanos: 10123363553, 335 PeriodType: &profilev1.ValueType{ 336 Type: 3, 337 Unit: 4, 338 }, 339 Period: 10000000, 340 } 341 342 b := &profilev1.Profile{ 343 SampleType: []*profilev1.ValueType{ 344 { 345 Type: 1, 346 Unit: 2, 347 }, 348 { 349 Type: 3, 350 Unit: 4, 351 }, 352 }, 353 Sample: []*profilev1.Sample{ 354 { 355 LocationId: []uint64{1}, 356 Value: []int64{1, 10000000}, 357 Label: []*profilev1.Label{ 358 {Key: 5, Str: 6}, 359 {Key: 7, Str: 11}, 360 {Key: 9, Str: 12}, 361 }, 362 }, 363 { 364 LocationId: []uint64{2, 3, 4}, // Same 365 Value: []int64{1, 10000000}, 366 Label: []*profilev1.Label{ 367 {Key: 5, Str: 6}, 368 {Key: 7, Str: 8}, 369 {Key: 9, Str: 10}, 370 }, 371 }, 372 }, 373 Mapping: []*profilev1.Mapping{ 374 { 375 Id: 1, 376 HasFunctions: true, 377 }, 378 }, 379 Location: []*profilev1.Location{ 380 { 381 Id: 1, 382 MappingId: 1, 383 Address: 19499013, 384 Line: []*profilev1.Line{{FunctionId: 1, Line: 42}}, 385 }, 386 { 387 Id: 2, 388 MappingId: 1, 389 Address: 19497668, 390 Line: []*profilev1.Line{{FunctionId: 2, Line: 19}}, 391 }, 392 { 393 Id: 3, 394 MappingId: 1, 395 Address: 19498429, 396 Line: []*profilev1.Line{{FunctionId: 3, Line: 43}}, 397 }, 398 { 399 Id: 4, 400 MappingId: 1, 401 Address: 19267106, 402 Line: []*profilev1.Line{{FunctionId: 4, Line: 40}}, 403 }, 404 }, 405 Function: []*profilev1.Function{ 406 { 407 Id: 1, 408 Name: 18, 409 SystemName: 18, 410 Filename: 14, 411 }, 412 { 413 Id: 2, 414 Name: 13, 415 SystemName: 13, 416 Filename: 14, 417 }, 418 { 419 Id: 3, 420 Name: 15, 421 SystemName: 15, 422 Filename: 14, 423 }, 424 { 425 Id: 4, 426 Name: 16, 427 SystemName: 16, 428 Filename: 17, 429 }, 430 }, 431 StringTable: stringTable, 432 TimeNanos: 1654798932062349000, 433 DurationNanos: 10123363553, 434 PeriodType: &profilev1.ValueType{ 435 Type: 3, 436 Unit: 4, 437 }, 438 Period: 10000000, 439 } 440 441 expected := &profilev1.Profile{ 442 SampleType: []*profilev1.ValueType{ 443 { 444 Type: 1, 445 Unit: 2, 446 }, 447 { 448 Type: 3, 449 Unit: 4, 450 }, 451 }, 452 Sample: []*profilev1.Sample{ 453 { 454 LocationId: []uint64{1, 2, 3}, 455 Value: []int64{2, 20000000}, 456 Label: []*profilev1.Label{ 457 {Key: 5, Str: 6}, 458 {Key: 7, Str: 8}, 459 {Key: 9, Str: 10}, 460 }, 461 }, 462 { 463 LocationId: []uint64{4}, 464 Value: []int64{1, 10000000}, 465 Label: []*profilev1.Label{ 466 {Key: 5, Str: 6}, 467 {Key: 7, Str: 11}, 468 {Key: 9, Str: 12}, 469 }, 470 }, 471 }, 472 Mapping: []*profilev1.Mapping{ 473 { 474 Id: 1, 475 HasFunctions: true, 476 }, 477 }, 478 Location: []*profilev1.Location{ 479 { 480 Id: 1, 481 MappingId: 1, 482 Address: 19497668, 483 Line: []*profilev1.Line{{FunctionId: 1, Line: 19}}, 484 }, 485 { 486 Id: 2, 487 MappingId: 1, 488 Address: 19498429, 489 Line: []*profilev1.Line{{FunctionId: 2, Line: 43}}, 490 }, 491 { 492 Id: 3, 493 MappingId: 1, 494 Address: 19267106, 495 Line: []*profilev1.Line{{FunctionId: 3, Line: 40}}, 496 }, 497 { 498 Id: 4, 499 MappingId: 1, 500 Address: 19499013, 501 Line: []*profilev1.Line{{FunctionId: 4, Line: 42}}, 502 }, 503 }, 504 Function: []*profilev1.Function{ 505 { 506 Id: 1, 507 Name: 13, 508 SystemName: 13, 509 Filename: 14, 510 }, 511 { 512 Id: 2, 513 Name: 15, 514 SystemName: 15, 515 Filename: 14, 516 }, 517 { 518 Id: 3, 519 Name: 16, 520 SystemName: 16, 521 Filename: 17, 522 }, 523 { 524 Id: 4, 525 Name: 18, 526 SystemName: 18, 527 Filename: 14, 528 }, 529 }, 530 StringTable: stringTable, 531 TimeNanos: 1654798932062349000, 532 DurationNanos: 20246727106, 533 PeriodType: &profilev1.ValueType{ 534 Type: 3, 535 Unit: 4, 536 }, 537 Period: 10000000, 538 } 539 540 var m ProfileMerge 541 require.NoError(t, m.Merge(a, true)) 542 require.NoError(t, m.Merge(b, true)) 543 544 testhelper.EqualProto(t, expected, m.Profile()) 545 } 546 547 func TestMergeEmpty(t *testing.T) { 548 var m ProfileMerge 549 550 err := m.Merge(&profilev1.Profile{ 551 SampleType: []*profilev1.ValueType{ 552 { 553 Type: 2, 554 Unit: 1, 555 }, 556 }, 557 PeriodType: &profilev1.ValueType{ 558 Type: 2, 559 Unit: 1, 560 }, 561 StringTable: []string{"", "nanoseconds", "cpu"}, 562 }, true) 563 require.NoError(t, err) 564 err = m.Merge(&profilev1.Profile{ 565 Sample: []*profilev1.Sample{ 566 { 567 LocationId: []uint64{1}, 568 Value: []int64{1}, 569 }, 570 }, 571 Location: []*profilev1.Location{ 572 { 573 Id: 1, 574 MappingId: 1, 575 Line: []*profilev1.Line{{FunctionId: 1, Line: 1}}, 576 }, 577 }, 578 Function: []*profilev1.Function{ 579 { 580 Id: 1, 581 Name: 1, 582 }, 583 }, 584 SampleType: []*profilev1.ValueType{ 585 { 586 Type: 3, 587 Unit: 2, 588 }, 589 }, 590 PeriodType: &profilev1.ValueType{ 591 Type: 3, 592 Unit: 2, 593 }, 594 Mapping: []*profilev1.Mapping{ 595 { 596 Id: 1, 597 }, 598 }, 599 StringTable: []string{"", "bar", "nanoseconds", "cpu"}, 600 }, true) 601 require.NoError(t, err) 602 } 603 604 // Benchmark_Merge_self/pprof.Merge-10 2722 421419 ns/op 605 // Benchmark_Merge_self/profile.Merge-10 802 1417907 ns/op 606 func Benchmark_Merge_self(b *testing.B) { 607 d, err := os.ReadFile("testdata/go.cpu.labels.pprof") 608 require.NoError(b, err) 609 610 b.Run("pprof.Merge", func(b *testing.B) { 611 p, err := RawFromBytes(d) 612 require.NoError(b, err) 613 b.ResetTimer() 614 for i := 0; i < b.N; i++ { 615 var m ProfileMerge 616 require.NoError(b, m.Merge(p.CloneVT(), true)) 617 } 618 }) 619 620 b.Run("profile.Merge", func(b *testing.B) { 621 p, err := profile.ParseData(d) 622 require.NoError(b, err) 623 b.ResetTimer() 624 for i := 0; i < b.N; i++ { 625 _, err = profile.Merge([]*profile.Profile{p.Copy()}) 626 require.NoError(b, err) 627 } 628 }) 629 }