@@ -14,7 +14,6 @@ import (
1414 "path/filepath"
1515 "regexp"
1616 "sort"
17- "strconv"
1817 "strings"
1918
2019 "github.com/google/pprof/profile"
@@ -221,11 +220,17 @@ func containsStr(s []string, v string) bool {
221220// entry instead of producing a separate JSON line each.
222221var captureKeysToIgnore = []string {
223222 "thread native id" ,
224- "thread id" ,
225- "process_id" ,
223+ LabelThreadID ,
224+ LabelProcessID ,
226225 "end_timestamp_ns" ,
227- "span id" ,
228- "local root span id" ,
226+ LabelTraceID ,
227+ LabelSpanID ,
228+ LabelLocalRootSID ,
229+ // OTLP resource/sample attributes that vary per sample or per run and would
230+ // otherwise split otherwise-identical stacks in the bootstrap JSON.
231+ "cpu.logical_number" , // per-sample: which CPU the sample hit
232+ "container.id" , // per-run
233+ "process.context.label.check_id" , // per-run
229234}
230235
231236// labelsKey produces a stable string key from a label set (which has already
@@ -240,7 +245,7 @@ func labelsKey(labels []Labels) string {
240245 for _ , l := range labels {
241246 b .WriteString (l .Key )
242247 b .WriteByte ('=' )
243- for _ , v := range l .Values { // Values is already sorted by getProfileType
248+ for _ , v := range l .Values { // Values is already sorted by the format adapter
244249 b .WriteString (v )
245250 b .WriteByte (',' )
246251 }
@@ -249,16 +254,20 @@ func labelsKey(labels []Labels) string {
249254 return b .String ()
250255}
251256
252- func captureProfData (r Reporter , prof * profile. Profile , path string , testName string , profileDuration float64 ) {
257+ func captureProfData (r Reporter , ps * ProfileSet , path string , testName string ) {
253258 var capturedData StackTestData
254259 capturedData .TestName = testName
255260
256- for _ , sampleType := range prof . SampleType {
261+ for _ , sampleType := range ps . SampleTypes () {
257262 var typedStack TypedStacks
258- typedStack .ProfileType = sampleType . Type
263+ typedStack .ProfileType = sampleType
259264 typedStack .ErrorMargin = 1
260265
261- typedProf := getProfileType (r , prof , sampleType .Type )
266+ // Rate-scale each type by its own duration (a file may mix, e.g., a
267+ // 10s allocation profile with a 60s CPU profile).
268+ profileDuration := ps .Duration (sampleType )
269+
270+ typedProf , _ := ps .Samples (sampleType )
262271
263272 // Group samples by (stack, kept-labels) and sum their values. Without
264273 // this, ephemeral labels like end_timestamp_ns produce one entry per
@@ -324,7 +333,7 @@ func captureProfData(r Reporter, prof *profile.Profile, path string, testName st
324333 capturedData .Stacks = append (capturedData .Stacks , typedStack )
325334 }
326335
327- jsonPath := filepath . Join ( filepath . Dir ( path ), fileNameWithoutExt ( filepath . Base ( path ))) + ".json"
336+ jsonPath := captureJSONPath ( path )
328337
329338 err := writeToJSONFile (capturedData , jsonPath )
330339 if err != nil {
@@ -334,57 +343,19 @@ func captureProfData(r Reporter, prof *profile.Profile, path string, testName st
334343 }
335344}
336345
337- func getProfileType (r Reporter , prof * profile.Profile , type_ string ) []StackSample {
338- typeIdx := - 1
339- for i , sampleType := range prof .SampleType {
340- if sampleType .Type == type_ {
341- typeIdx = i
342- }
343- }
344- if typeIdx == - 1 {
345- r .Fatalf ("Couldn't find sample type %s" , type_ )
346- }
347-
348- if err := prof .Aggregate (true , true , false , false , false , false ); err != nil {
349- r .Fatalf ("Error aggregating profile samples: %v" , err )
350- }
351- prof = prof .Compact ()
352- sort .Slice (prof .Sample , func (i , j int ) bool {
353- return prof .Sample [i ].Value [0 ] > prof .Sample [j ].Value [0 ]
354- })
355-
356- var out []StackSample
357- for _ , sample := range prof .Sample {
358- var frames []string
359- for i := range sample .Location {
360- loc := sample .Location [len (sample .Location )- i - 1 ]
361- for j := range loc .Line {
362- line := loc .Line [len (loc .Line )- j - 1 ]
363- name := line .Function .Name
364- frames = append (frames , name )
365- }
366- }
367- labels := make (map [string ][]string )
368- for k , v := range sample .Label {
369- // ease the comparison by sorting string values
370- sort .Strings (v )
371- labels [k ] = v
372- }
373- for k , v := range sample .NumLabel {
374- for _ , i := range v {
375- labels [k ] = append (labels [k ], strconv .FormatInt (i , 10 ))
376- }
377- sort .Strings (labels [k ])
378- }
379-
380- ss := StackSample {
381- Stack : strings .Join (frames , ";" ),
382- Val : sample .Value [typeIdx ],
383- Labels : labels ,
384- }
385- out = append (out , ss )
386- }
387- return out
346+ // captureJSONPath is where captureProfData writes the observed-stacks JSON: the
347+ // profile's basename with its extension replaced by .json. It guards against
348+ // clobbering the source: inputs whose own extension is already .json (e.g.
349+ // foo.otlp.json) would otherwise resolve back to the input path, so a
350+ // .capture.json variant is used instead.
351+ func captureJSONPath (path string ) string {
352+ dir := filepath .Dir (path )
353+ base := filepath .Base (path )
354+ jsonPath := filepath .Join (dir , fileNameWithoutExt (base )+ ".json" )
355+ if jsonPath == path {
356+ jsonPath = filepath .Join (dir , fileNameWithoutExt (base )+ ".capture.json" )
357+ }
358+ return jsonPath
388359}
389360
390361func checkLabels (r Reporter , labels map [string ][]string , expectedLabels []Labels ) bool {
@@ -608,15 +579,10 @@ func getMatchingFiles(folder string, filenameRegex *regexp.Regexp) ([]string, er
608579 return matchingFiles , nil
609580}
610581
611- // ReadPprofFile reads a pprof file from disk, transparently decompressing lz4
612- // or zstd frames if present, and returns the parsed profile.
613- func ReadPprofFile (pprofFile string ) (* profile.Profile , error ) {
614- file , err := os .Open (pprofFile )
615- if err != nil {
616- return nil , err
617- }
618- defer file .Close ()
619- content , err := io .ReadAll (file )
582+ // readAndDecompress reads a profile file, transparently decompressing lz4 or
583+ // zstd frames if present, and returns the raw payload bytes.
584+ func readAndDecompress (path string ) ([]byte , error ) {
585+ content , err := os .ReadFile (path )
620586 if err != nil {
621587 return nil , err
622588 }
@@ -651,36 +617,46 @@ func ReadPprofFile(pprofFile string) (*profile.Profile, error) {
651617 content = decompressed
652618 }
653619 }
654- prof , err := profile .ParseData (content )
620+ return content , nil
621+ }
622+
623+ // ReadPprofFile reads a pprof file from disk (decompressing lz4/zstd if needed)
624+ // and returns the parsed google/pprof profile. Retained for consumers that
625+ // want the raw pprof model; the analyzer itself uses LoadProfileSet.
626+ func ReadPprofFile (pprofFile string ) (* profile.Profile , error ) {
627+ content , err := readAndDecompress (pprofFile )
655628 if err != nil {
656629 return nil , err
657630 }
658- return prof , nil
631+ return profile . ParseData ( content )
659632}
660633
661634// AnalyzePprofFile reads a single pprof file and asserts the given typedStacks
662635// expectations against it. If captureData is true, a JSON dump of the actual
663636// stacks observed in the profile is written next to the pprof file (useful to
664637// bootstrap an expected_profile.json).
665638func AnalyzePprofFile (r Reporter , pprofFile string , typedStacks TypedStacks , testName string , captureData bool , scaleByDuration bool , allowFailure bool ) {
666- prof , err := ReadPprofFile (pprofFile )
639+ ps , err := LoadProfileSet (pprofFile )
667640 if err != nil {
668- r .Fatalf ("Error reading file %s" , pprofFile )
641+ r .Fatalf ("Error reading file %s: %v " , pprofFile , err )
669642 }
670643 r .Logf ("Analyzing results in %s for profile type %s" , pprofFile , typedStacks .ProfileType )
671644
672- profileDuration := float64 ( prof . DurationNanos ) / 1000000000.0
645+ profileDuration := ps . Duration ( typedStacks . ProfileType )
673646 r .Logf ("Found a profile duration of %.1f seconds (in %s)" , profileDuration , filepath .Base (pprofFile ))
674647
675648 // Store current data in a json file to help users create their tests
676649 if captureData {
677- captureProfData (r , prof , pprofFile , testName , profileDuration )
650+ captureProfData (r , ps , pprofFile , testName )
678651 }
679652 if ! scaleByDuration {
680653 // ignore duration, values can be considered absolute
681654 profileDuration = 0
682655 }
683- typedProf := getProfileType (r , prof , typedStacks .ProfileType )
656+ typedProf , ok := ps .Samples (typedStacks .ProfileType )
657+ if ! ok {
658+ r .Fatalf ("Couldn't find sample type %s" , typedStacks .ProfileType )
659+ }
684660 analyzeProfDataWithFailureHandling (r , typedProf , typedStacks , profileDuration , allowFailure )
685661}
686662
0 commit comments