@@ -20,6 +20,7 @@ import (
2020 typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
2121 "github.com/grafana/pyroscope/pkg/block/metadata"
2222 "github.com/grafana/pyroscope/pkg/featureflags"
23+ "github.com/grafana/pyroscope/pkg/pprof"
2324 "github.com/grafana/pyroscope/pkg/tenant"
2425 "github.com/grafana/pyroscope/pkg/test/mocks/mockfrontend"
2526 "github.com/grafana/pyroscope/pkg/test/mocks/mockmetastorev1"
@@ -477,3 +478,225 @@ func Test_QueryFrontend_Series_WithLabelNameFiltering(t *testing.T) {
477478 })
478479 }
479480}
481+
482+ func TestCreateStubsForUnsymbolizedProfiles (t * testing.T ) {
483+ tests := []struct {
484+ name string
485+ profile * profilev1.Profile
486+ queries []* queryv1.Query
487+ expectError bool
488+ validateStubs func (t * testing.T , profile * profilev1.Profile )
489+ }{
490+ {
491+ name : "creates stubs for unsymbolized locations" ,
492+ profile : & profilev1.Profile {
493+ StringTable : []string {"" , "/usr/lib/libjvm.so" },
494+ SampleType : []* profilev1.ValueType {{Type : 1 , Unit : 1 }},
495+ Mapping : []* profilev1.Mapping {
496+ {Id : 1 , Filename : 1 , HasFunctions : false },
497+ },
498+ Location : []* profilev1.Location {
499+ {Id : 1 , MappingId : 1 , Address : 0xcafebabe , Line : nil },
500+ {Id : 2 , MappingId : 1 , Address : 0xdeadbeef , Line : nil },
501+ },
502+ Function : []* profilev1.Function {},
503+ Sample : []* profilev1.Sample {
504+ {LocationId : []uint64 {1 , 2 }, Value : []int64 {100 }},
505+ },
506+ },
507+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
508+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
509+ require .Len (t , profile .Location [0 ].Line , 1 )
510+ require .Len (t , profile .Location [1 ].Line , 1 )
511+ require .Len (t , profile .Function , 2 )
512+ func1Name := profile .StringTable [profile .Function [0 ].Name ]
513+ func2Name := profile .StringTable [profile .Function [1 ].Name ]
514+ assert .Equal (t , "libjvm.so 0xcafebabe" , func1Name )
515+ assert .Equal (t , "libjvm.so 0xdeadbeef" , func2Name )
516+ assert .Equal (t , profile .Function [0 ].Id , profile .Location [0 ].Line [0 ].FunctionId )
517+ assert .Equal (t , profile .Function [1 ].Id , profile .Location [1 ].Line [0 ].FunctionId )
518+ },
519+ },
520+ {
521+ name : "deduplicates stubs by mapping and address combination" ,
522+ profile : & profilev1.Profile {
523+ StringTable : []string {"" , "/lib/libc.so" , "/lib/libm.so" },
524+ SampleType : []* profilev1.ValueType {{Type : 1 , Unit : 1 }},
525+ Mapping : []* profilev1.Mapping {
526+ {Id : 1 , Filename : 1 , HasFunctions : false },
527+ {Id : 2 , Filename : 2 , HasFunctions : false },
528+ },
529+ Location : []* profilev1.Location {
530+ {Id : 1 , MappingId : 1 , Address : 0x1234 , Line : nil },
531+ {Id : 2 , MappingId : 1 , Address : 0x5678 , Line : nil },
532+ {Id : 3 , MappingId : 2 , Address : 0x1234 , Line : nil },
533+ },
534+ Function : []* profilev1.Function {},
535+ Sample : []* profilev1.Sample {
536+ {LocationId : []uint64 {1 , 2 , 3 }, Value : []int64 {50 }},
537+ },
538+ },
539+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
540+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
541+ require .Len (t , profile .Function , 3 )
542+ func1Name := profile .StringTable [profile .Function [0 ].Name ]
543+ func2Name := profile .StringTable [profile .Function [1 ].Name ]
544+ func3Name := profile .StringTable [profile .Function [2 ].Name ]
545+ assert .Equal (t , "libc.so 0x1234" , func1Name )
546+ assert .Equal (t , "libc.so 0x5678" , func2Name )
547+ assert .Equal (t , "libm.so 0x1234" , func3Name )
548+ },
549+ },
550+ {
551+ name : "skips already symbolized locations" ,
552+ profile : & profilev1.Profile {
553+ StringTable : []string {"" , "/usr/bin/app" , "symbolized_func" },
554+ SampleType : []* profilev1.ValueType {{Type : 1 , Unit : 1 }},
555+ Mapping : []* profilev1.Mapping {
556+ {Id : 1 , Filename : 1 , HasFunctions : true },
557+ },
558+ Location : []* profilev1.Location {
559+ {Id : 1 , MappingId : 1 , Address : 0x1000 , Line : []* profilev1.Line {{FunctionId : 1 }}},
560+ {Id : 2 , MappingId : 1 , Address : 0x2000 , Line : nil },
561+ },
562+ Function : []* profilev1.Function {
563+ {Id : 1 , Name : 2 },
564+ },
565+ Sample : []* profilev1.Sample {
566+ {LocationId : []uint64 {1 , 2 }, Value : []int64 {25 }},
567+ },
568+ },
569+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
570+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
571+ require .Len (t , profile .Location [0 ].Line , 1 )
572+ assert .Equal (t , uint64 (1 ), profile .Location [0 ].Line [0 ].FunctionId )
573+ require .Len (t , profile .Location [1 ].Line , 1 )
574+ require .Len (t , profile .Function , 2 )
575+ stubFunc := profile .Function [1 ]
576+ stubName := profile .StringTable [stubFunc .Name ]
577+ assert .Equal (t , "app 0x2000" , stubName )
578+ },
579+ },
580+ {
581+ name : "handles multiple mappings" ,
582+ profile : & profilev1.Profile {
583+ StringTable : []string {"" , "/lib/liba.so" , "/lib/libb.so" },
584+ SampleType : []* profilev1.ValueType {{Type : 1 , Unit : 1 }},
585+ Mapping : []* profilev1.Mapping {
586+ {Id : 1 , Filename : 1 , HasFunctions : false },
587+ {Id : 2 , Filename : 2 , HasFunctions : false },
588+ },
589+ Location : []* profilev1.Location {
590+ {Id : 1 , MappingId : 1 , Address : 0x100 , Line : nil },
591+ {Id : 2 , MappingId : 2 , Address : 0x200 , Line : nil },
592+ },
593+ Function : []* profilev1.Function {},
594+ Sample : []* profilev1.Sample {
595+ {LocationId : []uint64 {1 , 2 }, Value : []int64 {10 }},
596+ },
597+ },
598+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
599+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
600+ require .Len (t , profile .Function , 2 )
601+ func1Name := profile .StringTable [profile .Function [0 ].Name ]
602+ func2Name := profile .StringTable [profile .Function [1 ].Name ]
603+ assert .Equal (t , "liba.so 0x100" , func1Name )
604+ assert .Equal (t , "libb.so 0x200" , func2Name )
605+ },
606+ },
607+ {
608+ name : "handles unknown binary name" ,
609+ profile : & profilev1.Profile {
610+ StringTable : []string {"" },
611+ SampleType : []* profilev1.ValueType {{Type : 0 , Unit : 0 }},
612+ Mapping : []* profilev1.Mapping {
613+ {Id : 1 , Filename : 0 , HasFunctions : false },
614+ },
615+ Location : []* profilev1.Location {
616+ {Id : 1 , MappingId : 1 , Address : 0xabc , Line : nil },
617+ },
618+ Function : []* profilev1.Function {},
619+ Sample : []* profilev1.Sample {
620+ {LocationId : []uint64 {1 }, Value : []int64 {5 }},
621+ },
622+ },
623+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
624+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
625+ require .Len (t , profile .Function , 1 )
626+ funcName := profile .StringTable [profile .Function [0 ].Name ]
627+ assert .Equal (t , "unknown 0xabc" , funcName )
628+ },
629+ },
630+ {
631+ name : "query/report count mismatch returns error" ,
632+ profile : & profilev1.Profile {
633+ StringTable : []string {"" },
634+ Location : []* profilev1.Location {},
635+ Function : []* profilev1.Function {},
636+ Sample : []* profilev1.Sample {},
637+ },
638+ queries : []* queryv1.Query {
639+ {QueryType : queryv1 .QueryType_QUERY_PPROF },
640+ {QueryType : queryv1 .QueryType_QUERY_PPROF },
641+ },
642+ expectError : true ,
643+ },
644+ {
645+ name : "no changes needed returns early" ,
646+ profile : & profilev1.Profile {
647+ StringTable : []string {"" , "/usr/bin/app" , "func1" },
648+ Mapping : []* profilev1.Mapping {
649+ {Id : 1 , Filename : 1 , HasFunctions : true },
650+ },
651+ Location : []* profilev1.Location {
652+ {Id : 1 , MappingId : 1 , Address : 0x1000 , Line : []* profilev1.Line {{FunctionId : 1 }}},
653+ },
654+ Function : []* profilev1.Function {
655+ {Id : 1 , Name : 2 },
656+ },
657+ Sample : []* profilev1.Sample {
658+ {LocationId : []uint64 {1 }, Value : []int64 {10 }},
659+ },
660+ },
661+ queries : []* queryv1.Query {{QueryType : queryv1 .QueryType_QUERY_PPROF }},
662+ validateStubs : func (t * testing.T , profile * profilev1.Profile ) {
663+ require .Len (t , profile .Function , 1 , "should not add functions" )
664+ assert .Equal (t , "func1" , profile .StringTable [2 ], "should not modify string table" )
665+ },
666+ },
667+ }
668+
669+ for _ , tt := range tests {
670+ t .Run (tt .name , func (t * testing.T ) {
671+ profileBytes , err := pprof .Marshal (tt .profile , true )
672+ require .NoError (t , err )
673+
674+ resp := & queryv1.InvokeResponse {
675+ Reports : []* queryv1.Report {
676+ {
677+ Pprof : & queryv1.PprofReport {Pprof : profileBytes },
678+ ReportType : queryv1 .ReportType_REPORT_PPROF ,
679+ },
680+ },
681+ }
682+
683+ qf := & QueryFrontend {}
684+ err = qf .createStubsForUnsymbolizedProfiles (context .Background (), resp , tt .queries )
685+
686+ if tt .expectError {
687+ require .Error (t , err )
688+ return
689+ }
690+
691+ require .NoError (t , err )
692+
693+ var resultProfile profilev1.Profile
694+ err = pprof .Unmarshal (resp .Reports [0 ].Pprof .Pprof , & resultProfile )
695+ require .NoError (t , err )
696+
697+ if tt .validateStubs != nil {
698+ tt .validateStubs (t , & resultProfile )
699+ }
700+ })
701+ }
702+ }
0 commit comments