fgprof.go raw
1 // fgprof is a sampling Go profiler that allows you to analyze On-CPU as well
2 // as [Off-CPU](http://www.brendangregg.com/offcpuanalysis.html) (e.g. I/O)
3 // time together.
4 package fgprof
5
6 import (
7 "fmt"
8 "io"
9 "math"
10 "runtime"
11 "sort"
12 "strings"
13 "time"
14
15 "github.com/google/pprof/profile"
16 )
17
18 // Format decides how the output is rendered to the user.
19 type Format string
20
21 const (
22 // FormatFolded is used by Brendan Gregg's FlameGraph utility, see
23 // https://github.com/brendangregg/FlameGraph#2-fold-stacks.
24 FormatFolded Format = "folded"
25 // FormatPprof is used by Google's pprof utility, see
26 // https://github.com/google/pprof/blob/master/proto/README.md.
27 FormatPprof Format = "pprof"
28 )
29
30 // Start begins profiling the goroutines of the program and returns a function
31 // that needs to be invoked by the caller to stop the profiling and write the
32 // results to w using the given format.
33 func Start(w io.Writer, format Format) func() error {
34 startTime := time.Now()
35
36 // Go's CPU profiler uses 100hz, but 99hz might be less likely to result in
37 // accidental synchronization with the program we're profiling.
38 const hz = 99
39 ticker := time.NewTicker(time.Second / hz)
40 stopCh := make(chan struct{})
41 prof := &profiler{}
42 profile := newWallclockProfile()
43
44 var sampleCount int64
45
46 go func() {
47 defer ticker.Stop()
48
49 for {
50 select {
51 case <-ticker.C:
52 sampleCount++
53
54 stacks := prof.GoroutineProfile()
55 profile.Add(stacks)
56 case <-stopCh:
57 return
58 }
59 }
60 }()
61
62 return func() error {
63 stopCh <- struct{}{}
64 endTime := time.Now()
65 profile.Ignore(prof.SelfFrames()...)
66
67 // Compute actual sample rate in case, due to performance issues, we
68 // were not actually able to sample at the given hz. Converting
69 // everything to float avoids integers being rounded in the wrong
70 // direction and improves the correctness of times in profiles.
71 duration := endTime.Sub(startTime)
72 actualHz := float64(sampleCount) / (float64(duration) / 1e9)
73 return profile.Export(w, format, int(math.Round(actualHz)), startTime, endTime)
74 }
75 }
76
77 // profiler provides a convenient and performant way to access
78 // runtime.GoroutineProfile().
79 type profiler struct {
80 stacks []runtime.StackRecord
81 selfFrame *runtime.Frame
82 }
83
84 // nullTerminationWorkaround deals with a regression in go1.23, see:
85 // - https://github.com/felixge/fgprof/issues/33
86 // - https://go-review.googlesource.com/c/go/+/609815
87 var nullTerminationWorkaround = runtime.Version() == "go1.23.0"
88
89 // GoroutineProfile returns the stacks of all goroutines currently managed by
90 // the scheduler. This includes both goroutines that are currently running
91 // (On-CPU), as well as waiting (Off-CPU).
92 func (p *profiler) GoroutineProfile() []runtime.StackRecord {
93 if p.selfFrame == nil {
94 // Determine the runtime.Frame of this func so we can hide it from our
95 // profiling output.
96 rpc := make([]uintptr, 1)
97 n := runtime.Callers(1, rpc)
98 if n < 1 {
99 panic("could not determine selfFrame")
100 }
101 selfFrame, _ := runtime.CallersFrames(rpc).Next()
102 p.selfFrame = &selfFrame
103 }
104
105 // We don't know how many goroutines exist, so we have to grow p.stacks
106 // dynamically. We overshoot by 10% since it's possible that more goroutines
107 // are launched in between two calls to GoroutineProfile. Once p.stacks
108 // reaches the maximum number of goroutines used by the program, it will get
109 // reused indefinitely, eliminating GoroutineProfile calls and allocations.
110 //
111 // TODO(fg) There might be workloads where it would be nice to shrink
112 // p.stacks dynamically as well, but let's not over-engineer this until we
113 // understand those cases better.
114 for {
115 if nullTerminationWorkaround {
116 for i := range p.stacks {
117 p.stacks[i].Stack0 = [32]uintptr{}
118 }
119 }
120 n, ok := runtime.GoroutineProfile(p.stacks)
121 if !ok {
122 p.stacks = make([]runtime.StackRecord, int(float64(n)*1.1))
123 } else {
124 return p.stacks[0:n]
125 }
126 }
127 }
128
129 // SelfFrames returns frames that belong to the profiler so that we can ignore
130 // them when exporting the final profile.
131 func (p *profiler) SelfFrames() []*runtime.Frame {
132 if p.selfFrame != nil {
133 return []*runtime.Frame{p.selfFrame}
134 }
135 return nil
136 }
137
138 func newWallclockProfile() *wallclockProfile {
139 return &wallclockProfile{stacks: map[[32]uintptr]*wallclockStack{}}
140 }
141
142 // wallclockProfile holds a wallclock profile that can be exported in different
143 // formats.
144 type wallclockProfile struct {
145 stacks map[[32]uintptr]*wallclockStack
146 ignore []*runtime.Frame
147 }
148
149 // wallclockStack holds the symbolized frames of a stack trace and the number
150 // of times it has been seen.
151 type wallclockStack struct {
152 frames []*runtime.Frame
153 count int
154 }
155
156 // Ignore sets a list of frames that should be ignored when exporting the
157 // profile.
158 func (p *wallclockProfile) Ignore(frames ...*runtime.Frame) {
159 p.ignore = frames
160 }
161
162 // Add adds the given stack traces to the profile.
163 func (p *wallclockProfile) Add(stackRecords []runtime.StackRecord) {
164 for _, stackRecord := range stackRecords {
165 if _, ok := p.stacks[stackRecord.Stack0]; !ok {
166 ws := &wallclockStack{}
167 // symbolize pcs into frames
168 frames := runtime.CallersFrames(stackRecord.Stack())
169 for {
170 frame, more := frames.Next()
171 ws.frames = append(ws.frames, &frame)
172 if !more {
173 break
174 }
175 }
176 p.stacks[stackRecord.Stack0] = ws
177 }
178 p.stacks[stackRecord.Stack0].count++
179 }
180 }
181
182 func (p *wallclockProfile) Export(w io.Writer, f Format, hz int, startTime, endTime time.Time) error {
183 switch f {
184 case FormatFolded:
185 return p.exportFolded(w)
186 case FormatPprof:
187 return p.exportPprof(hz, startTime, endTime).Write(w)
188 default:
189 return fmt.Errorf("unknown format: %q", f)
190 }
191 }
192
193 // exportStacks returns the stacks in this profile except those that have been
194 // set to Ignore().
195 func (p *wallclockProfile) exportStacks() []*wallclockStack {
196 stacks := make([]*wallclockStack, 0, len(p.stacks))
197 nextStack:
198 for _, ws := range p.stacks {
199 for _, f := range ws.frames {
200 for _, igf := range p.ignore {
201 if f.Entry == igf.Entry {
202 continue nextStack
203 }
204 }
205 }
206 stacks = append(stacks, ws)
207 }
208 return stacks
209 }
210
211 func (p *wallclockProfile) exportFolded(w io.Writer) error {
212 var lines []string
213 stacks := p.exportStacks()
214 for _, ws := range stacks {
215 var foldedStack []string
216 for _, f := range ws.frames {
217 foldedStack = append(foldedStack, f.Function)
218 }
219 line := fmt.Sprintf("%s %d", strings.Join(foldedStack, ";"), ws.count)
220 lines = append(lines, line)
221 }
222 sort.Strings(lines)
223 _, err := io.WriteString(w, strings.Join(lines, "\n")+"\n")
224 return err
225 }
226
227 func (p *wallclockProfile) exportPprof(hz int, startTime, endTime time.Time) *profile.Profile {
228 prof := &profile.Profile{}
229 m := &profile.Mapping{ID: 1, HasFunctions: true}
230 prof.Period = int64(1e9 / hz) // Number of nanoseconds between samples.
231 prof.TimeNanos = startTime.UnixNano()
232 prof.DurationNanos = int64(endTime.Sub(startTime))
233 prof.Mapping = []*profile.Mapping{m}
234 prof.SampleType = []*profile.ValueType{
235 {
236 Type: "samples",
237 Unit: "count",
238 },
239 {
240 Type: "time",
241 Unit: "nanoseconds",
242 },
243 }
244 prof.PeriodType = &profile.ValueType{
245 Type: "wallclock",
246 Unit: "nanoseconds",
247 }
248
249 type functionKey struct {
250 Name string
251 Filename string
252 }
253 funcIdx := map[functionKey]*profile.Function{}
254
255 type locationKey struct {
256 Function functionKey
257 Line int
258 }
259 locationIdx := map[locationKey]*profile.Location{}
260 for _, ws := range p.exportStacks() {
261 sample := &profile.Sample{
262 Value: []int64{
263 int64(ws.count),
264 int64(1000 * 1000 * 1000 / hz * ws.count),
265 },
266 }
267
268 for _, frame := range ws.frames {
269 fnKey := functionKey{Name: frame.Function, Filename: frame.File}
270 function, ok := funcIdx[fnKey]
271 if !ok {
272 function = &profile.Function{
273 ID: uint64(len(prof.Function)) + 1,
274 Name: frame.Function,
275 SystemName: frame.Function,
276 Filename: frame.File,
277 }
278 funcIdx[fnKey] = function
279 prof.Function = append(prof.Function, function)
280 }
281
282 locKey := locationKey{Function: fnKey, Line: frame.Line}
283 location, ok := locationIdx[locKey]
284 if !ok {
285 location = &profile.Location{
286 ID: uint64(len(prof.Location)) + 1,
287 Mapping: m,
288 Line: []profile.Line{{
289 Function: function,
290 Line: int64(frame.Line),
291 }},
292 }
293 locationIdx[locKey] = location
294 prof.Location = append(prof.Location, location)
295 }
296 sample.Location = append(sample.Location, location)
297 }
298 prof.Sample = append(prof.Sample, sample)
299 }
300 return prof
301 }
302
303 type symbolizedStacks map[[32]uintptr][]frameCount
304
305 func (w wallclockProfile) Symbolize(exclude *runtime.Frame) symbolizedStacks {
306 m := make(symbolizedStacks)
307 outer:
308 for stack0, ws := range w.stacks {
309 frames := runtime.CallersFrames((&runtime.StackRecord{Stack0: stack0}).Stack())
310
311 for {
312 frame, more := frames.Next()
313 if frame.Entry == exclude.Entry {
314 continue outer
315 }
316 m[stack0] = append(m[stack0], frameCount{Frame: &frame, Count: ws.count})
317 if !more {
318 break
319 }
320 }
321 }
322 return m
323 }
324
325 type frameCount struct {
326 *runtime.Frame
327 Count int
328 }
329