Help us shape the future of Go! Take the 2026 Go Developer Survey

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, err := sampleValueIndex(p)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	g := profile.NewGraph(p, &profile.Options{
    40  		SampleValue: func(v []int64) int64 { return v[valueIndex] },
    41  	})
    42  
    43  	if len(g.Nodes) == 0 {
    44  		// If all sample values are 0, the graph will have no nodes.
    45  		// In this case, treat it as an empty profile.
    46  		return emptyProfile(), nil
    47  	}
    48  
    49  	namedEdgeMap, totalWeight, err := createNamedEdgeMap(g)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	if totalWeight == 0 {
    55  		return emptyProfile(), nil // accept but ignore profile with no samples.
    56  	}
    57  
    58  	return &Profile{
    59  		TotalWeight:  totalWeight,
    60  		NamedEdgeMap: namedEdgeMap,
    61  	}, nil
    62  }
    63  
    64  // sampleValueIndex returns the index of the sample value to use as the PGO
    65  // weight. Non-Go CPU profiles (e.g., from Linux perf) use arbitrary sample
    66  // types, so we accept the default sample type unless it indicates a Go
    67  // non-CPU profile (heap, mutex, block, or goroutine) passed by mistake.
    68  func sampleValueIndex(p *profile.Profile) (int, error) {
    69  	if len(p.SampleType) == 0 {
    70  		return 0, fmt.Errorf("profile has no sample types")
    71  	}
    72  
    73  	// Prefer runtime/pprof's CPU profile sample types.
    74  	for i, s := range p.SampleType {
    75  		if (s.Type == "samples" && s.Unit == "count") ||
    76  			(s.Type == "cpu" && s.Unit == "nanoseconds") {
    77  			return i, nil
    78  		}
    79  	}
    80  
    81  	// Use the default sample type, or the last sample type if there is no
    82  	// default, as pprof does.
    83  	index := len(p.SampleType) - 1
    84  	if d := p.DefaultSampleType; d != "" {
    85  		for i, s := range p.SampleType {
    86  			if s.Type == d {
    87  				index = i
    88  				break
    89  			}
    90  		}
    91  	}
    92  
    93  	switch t := p.SampleType[index].Type; t {
    94  	case "alloc_objects", "alloc_space", "inuse_objects", "inuse_space":
    95  		return 0, fmt.Errorf("profile must be a CPU profile (default sample type %q indicates that this is a Go heap profile)", t)
    96  	case "contentions", "delay":
    97  		return 0, fmt.Errorf("profile must be a CPU profile (default sample type %q indicates that this is a Go mutex or block profile)", t)
    98  	case "goroutine":
    99  		return 0, fmt.Errorf("profile must be a CPU profile (default sample type %q indicates that this is a Go goroutine profile)", t)
   100  	}
   101  	return index, nil
   102  }
   103  
   104  // createNamedEdgeMap builds a map of callsite-callee edge weights from the
   105  // profile-graph.
   106  //
   107  // Caller should ignore the profile if totalWeight == 0.
   108  func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
   109  	seenStartLine := false
   110  
   111  	// Process graph and build various node and edge maps which will
   112  	// be consumed by AST walk.
   113  	weight := make(map[NamedCallEdge]int64)
   114  	for _, n := range g.Nodes {
   115  		seenStartLine = seenStartLine || n.Info.StartLine != 0
   116  
   117  		canonicalName := n.Info.Name
   118  		// Create the key to the nodeMapKey.
   119  		namedEdge := NamedCallEdge{
   120  			CallerName:     canonicalName,
   121  			CallSiteOffset: n.Info.Lineno - n.Info.StartLine,
   122  		}
   123  
   124  		for _, e := range n.Out {
   125  			totalWeight += e.WeightValue()
   126  			namedEdge.CalleeName = e.Dest.Info.Name
   127  			// Create new entry or increment existing entry.
   128  			weight[namedEdge] += e.WeightValue()
   129  		}
   130  	}
   131  
   132  	if !seenStartLine {
   133  		// TODO(prattmic): If Function.start_line is missing we could
   134  		// fall back to using absolute line numbers, which is better
   135  		// than nothing.
   136  		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)")
   137  	}
   138  	return postProcessNamedEdgeMap(weight, totalWeight)
   139  }
   140  
   141  func sortByWeight(edges []NamedCallEdge, weight map[NamedCallEdge]int64) {
   142  	sort.Slice(edges, func(i, j int) bool {
   143  		ei, ej := edges[i], edges[j]
   144  		if wi, wj := weight[ei], weight[ej]; wi != wj {
   145  			return wi > wj // want larger weight first
   146  		}
   147  		// same weight, order by name/line number
   148  		if ei.CallerName != ej.CallerName {
   149  			return ei.CallerName < ej.CallerName
   150  		}
   151  		if ei.CalleeName != ej.CalleeName {
   152  			return ei.CalleeName < ej.CalleeName
   153  		}
   154  		return ei.CallSiteOffset < ej.CallSiteOffset
   155  	})
   156  }
   157  
   158  func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
   159  	if weightVal == 0 {
   160  		return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples.
   161  	}
   162  	byWeight := make([]NamedCallEdge, 0, len(weight))
   163  	for namedEdge := range weight {
   164  		byWeight = append(byWeight, namedEdge)
   165  	}
   166  	sortByWeight(byWeight, weight)
   167  
   168  	edgeMap = NamedEdgeMap{
   169  		Weight:   weight,
   170  		ByWeight: byWeight,
   171  	}
   172  
   173  	totalWeight = weightVal
   174  
   175  	return edgeMap, totalWeight, nil
   176  }
   177  

View as plain text