Skip to content

Commit f1f92dd

Browse files
authored
feat: Support line numbers in pprof (#22)
Fixes #21
1 parent 3cad799 commit f1f92dd

File tree

6 files changed

+306
-211
lines changed

6 files changed

+306
-211
lines changed

fgprof.go

+192-37
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,26 @@
44
package fgprof
55

66
import (
7+
"fmt"
78
"io"
89
"runtime"
10+
"sort"
911
"strings"
1012
"time"
13+
14+
"github.com/google/pprof/profile"
15+
)
16+
17+
// Format decides how the output is rendered to the user.
18+
type Format string
19+
20+
const (
21+
// FormatFolded is used by Brendan Gregg's FlameGraph utility, see
22+
// https://github.com/brendangregg/FlameGraph#2-fold-stacks.
23+
FormatFolded Format = "folded"
24+
// FormatPprof is used by Google's pprof utility, see
25+
// https://github.com/google/pprof/blob/master/proto/README.md.
26+
FormatPprof Format = "pprof"
1127
)
1228

1329
// Start begins profiling the goroutines of the program and returns a function
@@ -23,7 +39,7 @@ func Start(w io.Writer, format Format) func() error {
2339
stopCh := make(chan struct{})
2440

2541
prof := &profiler{}
26-
stackCounts := stackCounter{}
42+
profile := newWallclockProfile()
2743

2844
go func() {
2945
defer ticker.Stop()
@@ -32,7 +48,7 @@ func Start(w io.Writer, format Format) func() error {
3248
select {
3349
case <-ticker.C:
3450
stacks := prof.GoroutineProfile()
35-
stackCounts.Update(stacks)
51+
profile.Add(stacks)
3652
case <-stopCh:
3753
return
3854
}
@@ -42,14 +58,8 @@ func Start(w io.Writer, format Format) func() error {
4258
return func() error {
4359
stopCh <- struct{}{}
4460
endTime := time.Now()
45-
return writeFormat(
46-
w,
47-
stackCounts.HumanMap(prof.SelfFrame()),
48-
format,
49-
hz,
50-
startTime,
51-
endTime,
52-
)
61+
profile.Ignore(prof.SelfFrames()...)
62+
return profile.Export(w, format, hz, startTime, endTime)
5363
}
5464
}
5565

@@ -95,58 +105,203 @@ func (p *profiler) GoroutineProfile() []runtime.StackRecord {
95105
}
96106
}
97107

98-
func (p *profiler) SelfFrame() *runtime.Frame {
99-
return p.selfFrame
108+
// SelfFrames returns frames that belong to the profiler so that we can ignore
109+
// them when exporting the final profile.
110+
func (p *profiler) SelfFrames() []*runtime.Frame {
111+
if p.selfFrame != nil {
112+
return []*runtime.Frame{p.selfFrame}
113+
}
114+
return nil
100115
}
101116

102-
type stringStackCounter map[string]int
117+
func newWallclockProfile() *wallclockProfile {
118+
return &wallclockProfile{stacks: map[[32]uintptr]*wallclockStack{}}
119+
}
103120

104-
func (s stringStackCounter) Update(p []runtime.StackRecord) {
105-
for _, pp := range p {
106-
frames := runtime.CallersFrames(pp.Stack())
121+
// wallclockProfile holds a wallclock profile that can be exported in different
122+
// formats.
123+
type wallclockProfile struct {
124+
stacks map[[32]uintptr]*wallclockStack
125+
ignore []*runtime.Frame
126+
}
107127

108-
var stack []string
109-
for {
110-
frame, more := frames.Next()
111-
stack = append([]string{frame.Function}, stack...)
112-
if !more {
113-
break
128+
// wallclockStack holds the symbolized frames of a stack trace and the number
129+
// of times it has been seen.
130+
type wallclockStack struct {
131+
frames []*runtime.Frame
132+
count int
133+
}
134+
135+
// Ignore sets a list of frames that should be ignored when exporting the
136+
// profile.
137+
func (p *wallclockProfile) Ignore(frames ...*runtime.Frame) {
138+
p.ignore = frames
139+
}
140+
141+
// Add adds the given stack traces to the profile.
142+
func (p *wallclockProfile) Add(stackRecords []runtime.StackRecord) {
143+
for _, stackRecord := range stackRecords {
144+
if _, ok := p.stacks[stackRecord.Stack0]; !ok {
145+
ws := &wallclockStack{}
146+
// symbolize pcs into frames
147+
frames := runtime.CallersFrames(stackRecord.Stack())
148+
for {
149+
frame, more := frames.Next()
150+
ws.frames = append(ws.frames, &frame)
151+
if !more {
152+
break
153+
}
114154
}
155+
p.stacks[stackRecord.Stack0] = ws
115156
}
116-
key := strings.Join(stack, ";")
117-
s[key]++
157+
p.stacks[stackRecord.Stack0].count++
118158
}
119159
}
120160

121-
type stackCounter map[[32]uintptr]int
161+
func (p *wallclockProfile) Export(w io.Writer, f Format, hz int, startTime, endTime time.Time) error {
162+
switch f {
163+
case FormatFolded:
164+
return p.exportFolded(w)
165+
case FormatPprof:
166+
return p.exportPprof(hz, startTime, endTime).Write(w)
167+
default:
168+
return fmt.Errorf("unknown format: %q", f)
169+
}
170+
}
122171

123-
func (s stackCounter) Update(p []runtime.StackRecord) {
124-
for _, pp := range p {
125-
s[pp.Stack0]++
172+
// exportStacks returns the stacks in this profile except those that have been
173+
// set to Ignore().
174+
func (p *wallclockProfile) exportStacks() []*wallclockStack {
175+
stacks := make([]*wallclockStack, 0, len(p.stacks))
176+
nextStack:
177+
for _, ws := range p.stacks {
178+
for _, f := range ws.frames {
179+
for _, igf := range p.ignore {
180+
if f.Entry == igf.Entry {
181+
continue nextStack
182+
}
183+
}
184+
}
185+
stacks = append(stacks, ws)
126186
}
187+
return stacks
127188
}
128189

129-
// @TODO(fg) create a better interface that avoids the pprof output having to
130-
// split the stacks using the `;` separator.
131-
func (s stackCounter) HumanMap(exclude *runtime.Frame) map[string]int {
132-
m := map[string]int{}
190+
func (p *wallclockProfile) exportFolded(w io.Writer) error {
191+
var lines []string
192+
stacks := p.exportStacks()
193+
for _, ws := range stacks {
194+
var foldedStack []string
195+
for _, f := range ws.frames {
196+
foldedStack = append(foldedStack, f.Function)
197+
}
198+
line := fmt.Sprintf("%s %d", strings.Join(foldedStack, ";"), ws.count)
199+
lines = append(lines, line)
200+
}
201+
sort.Strings(lines)
202+
_, err := io.WriteString(w, strings.Join(lines, "\n")+"\n")
203+
return err
204+
}
205+
206+
func (p *wallclockProfile) exportPprof(hz int, startTime, endTime time.Time) *profile.Profile {
207+
prof := &profile.Profile{}
208+
m := &profile.Mapping{ID: 1, HasFunctions: true}
209+
prof.Period = int64(1e9 / hz) // Number of nanoseconds between samples.
210+
prof.TimeNanos = startTime.UnixNano()
211+
prof.DurationNanos = int64(endTime.Sub(startTime))
212+
prof.Mapping = []*profile.Mapping{m}
213+
prof.SampleType = []*profile.ValueType{
214+
{
215+
Type: "samples",
216+
Unit: "count",
217+
},
218+
{
219+
Type: "time",
220+
Unit: "nanoseconds",
221+
},
222+
}
223+
prof.PeriodType = &profile.ValueType{
224+
Type: "wallclock",
225+
Unit: "nanoseconds",
226+
}
227+
228+
type functionKey struct {
229+
Name string
230+
Filename string
231+
}
232+
funcIdx := map[functionKey]*profile.Function{}
233+
234+
type locationKey struct {
235+
Function functionKey
236+
Line int
237+
}
238+
locationIdx := map[locationKey]*profile.Location{}
239+
for _, ws := range p.exportStacks() {
240+
sample := &profile.Sample{
241+
Value: []int64{
242+
int64(ws.count),
243+
int64(1000 * 1000 * 1000 / hz * ws.count),
244+
},
245+
}
246+
247+
for _, frame := range ws.frames {
248+
fnKey := functionKey{Name: frame.Function, Filename: frame.File}
249+
function, ok := funcIdx[fnKey]
250+
if !ok {
251+
function = &profile.Function{
252+
ID: uint64(len(prof.Function)) + 1,
253+
Name: frame.Function,
254+
SystemName: frame.Function,
255+
Filename: frame.File,
256+
}
257+
funcIdx[fnKey] = function
258+
prof.Function = append(prof.Function, function)
259+
}
260+
261+
locKey := locationKey{Function: fnKey, Line: frame.Line}
262+
location, ok := locationIdx[locKey]
263+
if !ok {
264+
location = &profile.Location{
265+
ID: uint64(len(prof.Location)) + 1,
266+
Mapping: m,
267+
Line: []profile.Line{{
268+
Function: function,
269+
Line: int64(frame.Line),
270+
}},
271+
}
272+
locationIdx[locKey] = location
273+
prof.Location = append(prof.Location, location)
274+
}
275+
sample.Location = append(sample.Location, location)
276+
}
277+
prof.Sample = append(prof.Sample, sample)
278+
}
279+
return prof
280+
}
281+
282+
type symbolizedStacks map[[32]uintptr][]frameCount
283+
284+
func (w wallclockProfile) Symbolize(exclude *runtime.Frame) symbolizedStacks {
285+
m := make(symbolizedStacks)
133286
outer:
134-
for stack0, count := range s {
287+
for stack0, ws := range w.stacks {
135288
frames := runtime.CallersFrames((&runtime.StackRecord{Stack0: stack0}).Stack())
136289

137-
var stack []string
138290
for {
139291
frame, more := frames.Next()
140292
if frame.Entry == exclude.Entry {
141293
continue outer
142294
}
143-
stack = append([]string{frame.Function}, stack...)
295+
m[stack0] = append(m[stack0], frameCount{Frame: &frame, Count: ws.count})
144296
if !more {
145297
break
146298
}
147299
}
148-
key := strings.Join(stack, ";")
149-
m[key] = count
150300
}
151301
return m
152302
}
303+
304+
type frameCount struct {
305+
*runtime.Frame
306+
Count int
307+
}

0 commit comments

Comments
 (0)