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 }