// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package pgo contains the compiler-agnostic portions of PGO profile handling. // Notably, parsing pprof profiles and serializing/deserializing from a custom // intermediate representation. package pgo import ( "errors" "fmt" "internal/profile" "io" "sort" ) // FromPProf parses Profile from a pprof profile. func FromPProf(r io.Reader) (*Profile, error) { p, err := profile.Parse(r) if errors.Is(err, profile.ErrNoData) { // Treat a completely empty file the same as a profile with no // samples: nothing to do. return emptyProfile(), nil } else if err != nil { return nil, fmt.Errorf("error parsing profile: %w", err) } if len(p.Sample) == 0 { // We accept empty profiles, but there is nothing to do. return emptyProfile(), nil } valueIndex := -1 for i, s := range p.SampleType { // Samples count is the raw data collected, and CPU nanoseconds is just // a scaled version of it, so either one we can find is fine. if (s.Type == "samples" && s.Unit == "count") || (s.Type == "cpu" && s.Unit == "nanoseconds") { valueIndex = i break } } if valueIndex == -1 { return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`) } g := profile.NewGraph(p, &profile.Options{ SampleValue: func(v []int64) int64 { return v[valueIndex] }, }) namedEdgeMap, totalWeight, err := createNamedEdgeMap(g) if err != nil { return nil, err } if totalWeight == 0 { return emptyProfile(), nil // accept but ignore profile with no samples. } return &Profile{ TotalWeight: totalWeight, NamedEdgeMap: namedEdgeMap, }, nil } // createNamedEdgeMap builds a map of callsite-callee edge weights from the // profile-graph. // // Caller should ignore the profile if totalWeight == 0. func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) { seenStartLine := false // Process graph and build various node and edge maps which will // be consumed by AST walk. weight := make(map[NamedCallEdge]int64) for _, n := range g.Nodes { seenStartLine = seenStartLine || n.Info.StartLine != 0 canonicalName := n.Info.Name // Create the key to the nodeMapKey. namedEdge := NamedCallEdge{ CallerName: canonicalName, CallSiteOffset: n.Info.Lineno - n.Info.StartLine, } for _, e := range n.Out { totalWeight += e.WeightValue() namedEdge.CalleeName = e.Dest.Info.Name // Create new entry or increment existing entry. weight[namedEdge] += e.WeightValue() } } if !seenStartLine { // TODO(prattmic): If Function.start_line is missing we could // fall back to using absolute line numbers, which is better // than nothing. 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)") } return postProcessNamedEdgeMap(weight, totalWeight) } func sortByWeight(edges []NamedCallEdge, weight map[NamedCallEdge]int64) { sort.Slice(edges, func(i, j int) bool { ei, ej := edges[i], edges[j] if wi, wj := weight[ei], weight[ej]; wi != wj { return wi > wj // want larger weight first } // same weight, order by name/line number if ei.CallerName != ej.CallerName { return ei.CallerName < ej.CallerName } if ei.CalleeName != ej.CalleeName { return ei.CalleeName < ej.CalleeName } return ei.CallSiteOffset < ej.CallSiteOffset }) } func postProcessNamedEdgeMap(weight map[NamedCallEdge]int64, weightVal int64) (edgeMap NamedEdgeMap, totalWeight int64, err error) { if weightVal == 0 { return NamedEdgeMap{}, 0, nil // accept but ignore profile with no samples. } byWeight := make([]NamedCallEdge, 0, len(weight)) for namedEdge := range weight { byWeight = append(byWeight, namedEdge) } sortByWeight(byWeight, weight) edgeMap = NamedEdgeMap{ Weight: weight, ByWeight: byWeight, } totalWeight = weightVal return edgeMap, totalWeight, nil }