| |
| |
| |
|
|
| |
| |
| |
| package pgo |
|
|
| import ( |
| "errors" |
| "fmt" |
| "internal/profile" |
| "io" |
| "sort" |
| ) |
|
|
| |
| func FromPProf(r io.Reader) (*Profile, error) { |
| p, err := profile.Parse(r) |
| if errors.Is(err, profile.ErrNoData) { |
| |
| |
| return emptyProfile(), nil |
| } else if err != nil { |
| return nil, fmt.Errorf("error parsing profile: %w", err) |
| } |
|
|
| if len(p.Sample) == 0 { |
| |
| return emptyProfile(), nil |
| } |
|
|
| valueIndex := -1 |
| for i, s := range p.SampleType { |
| |
| |
| 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 |
| } |
|
|
| return &Profile{ |
| TotalWeight: totalWeight, |
| NamedEdgeMap: namedEdgeMap, |
| }, nil |
| } |
|
|
| |
| |
| |
| |
| func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) { |
| seenStartLine := false |
|
|
| |
| |
| weight := make(map[NamedCallEdge]int64) |
| for _, n := range g.Nodes { |
| seenStartLine = seenStartLine || n.Info.StartLine != 0 |
|
|
| canonicalName := n.Info.Name |
| |
| 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 |
| |
| weight[namedEdge] += e.WeightValue() |
| } |
| } |
|
|
| if !seenStartLine { |
| |
| |
| |
| 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 |
| } |
| |
| 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 |
| } |
| 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 |
| } |
|
|