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