github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/agent/gospy/gospy.go (about)

     1  //go:build !nogospy
     2  // +build !nogospy
     3  
     4  package gospy
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"fmt"
    10  	"io"
    11  	"runtime"
    12  	"runtime/pprof"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/hashicorp/go-multierror"
    17  
    18  	custom_pprof "github.com/pyroscope-io/pyroscope/pkg/agent/pprof"
    19  	"github.com/pyroscope-io/pyroscope/pkg/agent/spy"
    20  	"github.com/pyroscope-io/pyroscope/pkg/convert"
    21  	"github.com/pyroscope-io/pyroscope/pkg/storage/tree"
    22  )
    23  
    24  // TODO: make this configurable
    25  // TODO: pass lower level structures between go and rust?
    26  var bufferLength = 1024 * 64
    27  
    28  type GoSpy struct {
    29  	resetMutex    sync.Mutex
    30  	reset         bool
    31  	stop          bool
    32  	profileType   spy.ProfileType
    33  	disableGCRuns bool
    34  	sampleRate    uint32
    35  
    36  	lastGCGeneration uint32
    37  
    38  	stopCh chan struct{}
    39  	buf    *bytes.Buffer
    40  }
    41  
    42  func startCPUProfile(w io.Writer, hz uint32) error {
    43  	// idea here is that for most people we're starting the default profiler
    44  	//   but if you want to use a different sampling rate we use our experimental profiler
    45  	if hz == 100 {
    46  		return pprof.StartCPUProfile(w)
    47  	}
    48  	return custom_pprof.StartCPUProfile(w, hz)
    49  }
    50  
    51  func stopCPUProfile(hz uint32) {
    52  	// idea here is that for most people we're starting the default profiler
    53  	//   but if you want to use a different sampling rate we use our experimental profiler
    54  	if hz == 100 {
    55  		pprof.StopCPUProfile()
    56  		return
    57  	}
    58  	custom_pprof.StopCPUProfile()
    59  }
    60  
    61  func Start(params spy.InitParams) (spy.Spy, error) {
    62  	s := &GoSpy{
    63  		stopCh:        make(chan struct{}),
    64  		buf:           &bytes.Buffer{},
    65  		profileType:   params.ProfileType,
    66  		disableGCRuns: params.DisableGCRuns,
    67  		sampleRate:    params.SampleRate,
    68  	}
    69  	if s.profileType == spy.ProfileCPU {
    70  		if err := startCPUProfile(s.buf, params.SampleRate); err != nil {
    71  			return nil, err
    72  		}
    73  	}
    74  	return s, nil
    75  }
    76  
    77  func (s *GoSpy) Stop() error {
    78  	s.stop = true
    79  	<-s.stopCh
    80  	return nil
    81  }
    82  
    83  // TODO: this is not the most elegant solution as it creates global state
    84  //
    85  //	the idea here is that we can reuse heap profiles
    86  var (
    87  	lastProfileMutex     sync.Mutex
    88  	lastProfile          *tree.Profile
    89  	lastProfileCreatedAt time.Time
    90  )
    91  
    92  func getHeapProfile(b *bytes.Buffer) *tree.Profile {
    93  	lastProfileMutex.Lock()
    94  	defer lastProfileMutex.Unlock()
    95  
    96  	if lastProfile == nil || !lastProfileCreatedAt.After(time.Now().Add(-1*time.Second)) {
    97  		pprof.WriteHeapProfile(b)
    98  		g, _ := gzip.NewReader(bytes.NewReader(b.Bytes()))
    99  
   100  		lastProfile, _ = convert.ParsePprof(g)
   101  		lastProfileCreatedAt = time.Now()
   102  	}
   103  
   104  	return lastProfile
   105  }
   106  
   107  func numGC() uint32 {
   108  	var memStats runtime.MemStats
   109  	runtime.ReadMemStats(&memStats)
   110  	return memStats.NumGC
   111  }
   112  
   113  // Snapshot calls callback function with stack-trace or error.
   114  func (s *GoSpy) Snapshot(cb func(*spy.Labels, []byte, uint64) error) (errs error) {
   115  	s.resetMutex.Lock()
   116  	defer s.resetMutex.Unlock()
   117  
   118  	// before the upload rate is reached, no need to read the profile data
   119  	if !s.reset {
   120  		return nil
   121  	}
   122  	s.reset = false
   123  
   124  	if s.profileType == spy.ProfileCPU {
   125  		// stop the previous cycle of sample collection
   126  		stopCPUProfile(s.sampleRate)
   127  		defer func() {
   128  			// start a new cycle of sample collection
   129  			if err := startCPUProfile(s.buf, s.sampleRate); err != nil {
   130  				errs = multierror.Append(errs, err)
   131  			}
   132  		}()
   133  
   134  		// new gzip reader with the read data in buffer
   135  		r, err := gzip.NewReader(bytes.NewReader(s.buf.Bytes()))
   136  		if err != nil {
   137  			return fmt.Errorf("new gzip reader: %w", err)
   138  		}
   139  
   140  		// parse the read data with pprof format
   141  		profile, err := convert.ParsePprof(r)
   142  		if err != nil {
   143  			return fmt.Errorf("parse pprof: %w", err)
   144  		}
   145  		err = profile.Get("samples", func(labels *spy.Labels, name []byte, val int) error {
   146  			return cb(labels, name, uint64(val))
   147  		})
   148  		if err != nil {
   149  			return fmt.Errorf("parsing stack trace: %w", err)
   150  		}
   151  	} else {
   152  		// this is current GC generation
   153  		currentGCGeneration := numGC()
   154  
   155  		// sometimes GC doesn't run within 10 seconds
   156  		//   in such cases we force a GC run
   157  		//   users can disable it with disableGCRuns option
   158  		if currentGCGeneration == s.lastGCGeneration && !s.disableGCRuns {
   159  			runtime.GC()
   160  			currentGCGeneration = numGC()
   161  		}
   162  
   163  		// if there's no GC run then the profile is gonna be the same
   164  		//   in such case it does not make sense to upload the same profile twice
   165  		if currentGCGeneration != s.lastGCGeneration {
   166  			err := getHeapProfile(s.buf).Get(string(s.profileType), func(labels *spy.Labels, name []byte, val int) error {
   167  				return cb(labels, name, uint64(val))
   168  			})
   169  			s.lastGCGeneration = currentGCGeneration
   170  			if err != nil {
   171  				return fmt.Errorf("parsing stack trace: %w", err)
   172  			}
   173  		}
   174  	}
   175  	s.buf.Reset()
   176  	return nil
   177  }
   178  
   179  func (s *GoSpy) Reset() {
   180  	s.resetMutex.Lock()
   181  	defer s.resetMutex.Unlock()
   182  
   183  	s.reset = true
   184  }
   185  
   186  func init() {
   187  	spy.RegisterSpy("gospy", Start)
   188  }