Source file src/cmd/internal/pgo/pprof.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package pgo contains the compiler-agnostic portions of PGO profile handling.
     6  // Notably, parsing pprof profiles and serializing/deserializing from a custom
     7  // intermediate representation.
     8  package pgo
     9  
    10  import (
    11  	"errors"
    12  	"fmt"
    13  	"internal/profile"
    14  	"io"
    15  	"sort"
    16  )
    17  
    18  // FromPProf parses Profile from a pprof profile.
    19  func FromPProf(r io.Reader) (*Profile, error) {
    20  	p, err := profile.Parse(r)
    21  	if errors.Is(err, profile.ErrNoData) {
    22  		// Treat a completely empty file the same as a profile with no
    23  		// samples: nothing to do.
    24  		return emptyProfile(), nil
    25  	} else if err != nil {
    26  		return nil, fmt.Errorf("error parsing profile: %w", err)
    27  	}
    28  
    29  	if len(p.Sample) == 0 {
    30  		// We accept empty profiles, but there is nothing to do.
    31  		return emptyProfile(), nil
    32  	}
    33  
    34  	valueIndex := -1
    35  	for i, s := range p.SampleType {
    36  		// Samples count is the raw data collected, and CPU nanoseconds is just
    37  		// a scaled version of it, so either one we can find is fine.
    38  		if (s.Type == "samples" && s.Unit == "count") ||
    39  			(s.Type == "cpu" && s.Unit == "nanoseconds") {
    40  			valueIndex = i
    41  			break
    42  		}
    43  	}
    44  
    45  	if valueIndex == -1 {
    46  		return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
    47  	}
    48  
    49  	g := profile.NewGraph(p, &profile.Options{
    50  		SampleValue: func(v []int64) int64 { return v[valueIndex] },
    51  	})
    52  
    53  	namedEdgeMap, totalWeight, err := createNamedEdgeMap(g)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	if totalWeight == 0 {
    59  		return emptyProfile(), nil // accept but ignore profile with no samples.
    60  	}
    61  
    62  	return &Profile{
    63  		TotalWeight:  totalWeight,
    64  		NamedEdgeMap: namedEdgeMap,
    65  	}, nil
    66  }
    67  
    68  // createNamedEdgeMap builds a map of callsite-callee edge weights from the
    69  // profile-graph.
    70  //
    71  // Caller should ignore the profile if totalWeight == 0.
    72  func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
    73  	seenStartLine := false
    74  
    75  	// Process graph and build various node and edge maps which will
    76  	// be consumed by AST walk.
    77  	weight := make(map[NamedCallEdge]int64)
    78  	for _, n := range g.Nodes {
    79  		seenStartLine = seenStartLine || n.Info.StartLine != 0
    80  
    81  		canonicalName := n.Info.Name
    82  		// Create the key to the nodeMapKey.
    83  		namedEdge := NamedCallEdge{
    84  			CallerName:     canonicalName,
    85  			CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
    86  		}
    87  
    88  		for _, e := range n.Out {
    89  			totalWeight += e.WeightValue()
    90  			namedEdge.CalleeName = e.Dest.Info.Name
    91  			// Create new entry or increment existing entry.
    92  			weight[namedEdge] += e.WeightValue()
    93  		}
    94  	}
    95  
    96  	if !seenStartLine {
    97  		// TODO(prattmic): If Function.start_line is missing we could
    98  		// fall back to using absolute line numbers, which is better
    99  		// than nothing.
   100  		return NamedEdgeMap{}, 0, fmt.Errorf("profile missing Function.start_line data (Go version of profiled application too old? Go 1.20+ automatically adds this to profiles)")
   101  	}
   102  	return postProcessNamedEdgeMap(weight, totalWeight)
   103  }
   104  
   105  func sortByWeight(edges []NamedCallEdge, weight map[NamedCallEdge]int64) {
   106  	sort.Slice(edges, func(i, j int) bool {
   107  		ei, ej := edges[i], edges[j]
   108  		if wi, wj := weight[ei], weight[ej]; wi != wj {
   109  			return wi > wj // want larger weight first
   110  		}
   111  		// same weight, order by name/line number
   112  		if ei.CallerName != ej.CallerName {
   113  			return ei.CallerName < ej.CallerName
   114  		}
   115  		if ei.CalleeName != ej.CalleeName {
   116  			return ei.CalleeName < ej.CalleeName
   117  		}
   118  		return ei.CallSiteOffset < ej.CallSiteOffset
   119  	})
   120  }
   121  
   122  func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
   123  	if weightVal == 0 {
   124  		return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
   125  	}
   126  	byWeight := make([]NamedCallEdge, 0, len(weight))
   127  	for namedEdge := range weight {
   128  		byWeight = append(byWeight, namedEdge)
   129  	}
   130  	sortByWeight(byWeight, weight)
   131  
   132  	edgeMap = NamedEdgeMap{
   133  		Weight:   weight,
   134  		ByWeight: byWeight,
   135  	}
   136  
   137  	totalWeight = weightVal
   138  
   139  	return edgeMap, totalWeight, nil
   140  }
   141  

View as plain text