1
2
3
4
5
6
7
8 package pgo
9
10 import (
11 "errors"
12 "fmt"
13 "internal/profile"
14 "io"
15 "sort"
16 )
17
18
19 func FromPProf(r io.Reader) (*Profile, error) {
20 p, err := profile.Parse(r)
21 if errors.Is(err, profile.ErrNoData) {
22
23
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
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
45
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
56 }
57
58 return &Profile{
59 TotalWeight: totalWeight,
60 NamedEdgeMap: namedEdgeMap,
61 }, nil
62 }
63
64
65
66
67
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
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
82
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
105
106
107
108 func createNamedEdgeMap(g *profile.Graph) (edgeMap NamedEdgeMap, totalWeight int64, err error) {
109 seenStartLine := false
110
111
112
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
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
128 weight[namedEdge] += e.WeightValue()
129 }
130 }
131
132 if !seenStartLine {
133
134
135
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
146 }
147
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
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