github.com/stealthrocket/wzprof@v0.2.1-0.20230830205924-5fa86be5e5b3/cpu.go (about) 1 package wzprof 2 3 import ( 4 "context" 5 "net/http" 6 "strconv" 7 "sync" 8 "time" 9 10 "github.com/google/pprof/profile" 11 "github.com/tetratelabs/wazero/api" 12 "github.com/tetratelabs/wazero/experimental" 13 ) 14 15 // CPUProfiler is the implementation of a performance profiler recording 16 // samples of CPU time spent in functions of a WebAssembly module. 17 // 18 // The profiler generates samples of two types: 19 // - "sample" counts the number of function calls. 20 // - "cpu" records the time spent in function calls (in nanoseconds). 21 type CPUProfiler struct { 22 p *Profiling 23 mutex sync.Mutex 24 counts stackCounterMap 25 frames []cpuTimeFrame 26 traces []stackTrace 27 time func() int64 28 start time.Time 29 host bool 30 } 31 32 // CPUProfilerOption is a type used to represent configuration options for 33 // CPUProfiler instances created by NewCPUProfiler. 34 type CPUProfilerOption func(*CPUProfiler) 35 36 // HostTime confiures a CPU time profiler to account for time spent in calls 37 // to host functions. 38 // 39 // Default to false. 40 func HostTime(enable bool) CPUProfilerOption { 41 return func(p *CPUProfiler) { p.host = enable } 42 } 43 44 // TimeFunc configures the time function used by the CPU profiler to collect 45 // monotonic timestamps. 46 // 47 // By default, the system's monotonic time is used. 48 func TimeFunc(time func() int64) CPUProfilerOption { 49 return func(p *CPUProfiler) { p.time = time } 50 } 51 52 type cpuTimeFrame struct { 53 start int64 54 sub int64 55 trace stackTrace 56 } 57 58 func newCPUProfiler(p *Profiling, options ...CPUProfilerOption) *CPUProfiler { 59 c := &CPUProfiler{ 60 p: p, 61 time: nanotime, 62 } 63 for _, opt := range options { 64 opt(c) 65 } 66 return c 67 } 68 69 // StartProfile begins recording the CPU profile. The method returns a boolean 70 // to indicate whether starting the profile succeeded (e.g. false is returned if 71 // it was already started). 72 func (p *CPUProfiler) StartProfile() bool { 73 p.mutex.Lock() 74 defer p.mutex.Unlock() 75 76 if p.counts != nil { 77 return false // already started 78 } 79 80 p.counts = make(stackCounterMap) 81 p.start = time.Now() 82 return true 83 } 84 85 // StopProfile stops recording and returns the CPU profile. The method returns 86 // nil if recording of the CPU profile wasn't started. 87 func (p *CPUProfiler) StopProfile(sampleRate float64) *profile.Profile { 88 p.mutex.Lock() 89 samples, start := p.counts, p.start 90 p.counts = nil 91 p.mutex.Unlock() 92 93 if samples == nil { 94 return nil 95 } 96 97 duration := time.Since(start) 98 99 if !p.host { 100 for k, sample := range samples { 101 if sample.stack.host() { 102 delete(samples, k) 103 } 104 } 105 } 106 107 ratios := []float64{ 108 1 / sampleRate, 109 // Time values are not influenced by the sampling rate so we don't have 110 // to scale them out. 111 1, 112 } 113 114 return buildProfile(p.p, samples, start, duration, p.SampleType(), ratios) 115 } 116 117 // Name returns "profile" to match the name of the CPU profiler in pprof. 118 func (p *CPUProfiler) Name() string { 119 return "profile" 120 } 121 122 // Desc returns a description copied from net/http/pprof. 123 func (p *CPUProfiler) Desc() string { 124 return profileDescriptions[p.Name()] 125 } 126 127 // Count returns the number of execution stacks currently recorded in p. 128 func (p *CPUProfiler) Count() int { 129 p.mutex.Lock() 130 n := len(p.counts) 131 p.mutex.Unlock() 132 return n 133 } 134 135 // SampleType returns the set of value types present in samples recorded by the 136 // CPU profiler. 137 func (p *CPUProfiler) SampleType() []*profile.ValueType { 138 return []*profile.ValueType{ 139 {Type: "samples", Unit: "count"}, 140 {Type: "cpu", Unit: "nanoseconds"}, 141 } 142 } 143 144 // NewHandler returns a http handler allowing the profiler to be exposed on a 145 // pprof-compatible http endpoint. 146 // 147 // The sample rate is a value between 0 and 1 used to scale the profile results 148 // based on the sampling rate applied to the profiler so the resulting values 149 // remain representative. 150 // 151 // The symbolizer passed as argument is used to resolve names of program 152 // locations recorded in the profile. 153 func (p *CPUProfiler) NewHandler(sampleRate float64) http.Handler { 154 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 155 duration := 30 * time.Second 156 157 if seconds := r.FormValue("seconds"); seconds != "" { 158 n, err := strconv.ParseInt(seconds, 10, 64) 159 if err == nil && n > 0 { 160 duration = time.Duration(n) * time.Second 161 } 162 } 163 164 ctx := r.Context() 165 deadline, ok := ctx.Deadline() 166 if ok { 167 if timeout := time.Until(deadline); duration > timeout { 168 serveError(w, http.StatusBadRequest, "profile duration exceeds server's WriteTimeout") 169 return 170 } 171 } 172 173 if !p.StartProfile() { 174 serveError(w, http.StatusInternalServerError, "Could not enable CPU profiling: profiler already running") 175 return 176 } 177 178 timer := time.NewTimer(duration) 179 select { 180 case <-timer.C: 181 case <-ctx.Done(): 182 } 183 timer.Stop() 184 serveProfile(w, p.StopProfile(sampleRate)) 185 }) 186 } 187 188 // NewFunctionListener returns a function listener suited to record CPU timings 189 // of calls to the function passed as argument. 190 func (p *CPUProfiler) NewFunctionListener(def api.FunctionDefinition) experimental.FunctionListener { 191 name := def.Name() 192 if len(p.p.onlyFunctions) > 0 { 193 _, keep := p.p.onlyFunctions[name] 194 if !keep { 195 return nil 196 } 197 } 198 _, skip := p.p.filteredFunctions[name] 199 if skip { 200 return nil 201 } 202 return profilingListener{p.p, cpuProfiler{p}} 203 } 204 205 type cpuProfiler struct{ *CPUProfiler } 206 207 func (p cpuProfiler) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, _ []uint64, si experimental.StackIterator) { 208 var frame cpuTimeFrame 209 p.mutex.Lock() 210 211 if p.counts != nil { 212 start := p.time() 213 trace := stackTrace{} 214 215 if i := len(p.traces); i > 0 { 216 i-- 217 trace = p.traces[i] 218 p.traces = p.traces[:i] 219 } 220 221 frame = cpuTimeFrame{ 222 start: start, 223 trace: makeStackTrace(trace, si), 224 } 225 } 226 227 p.mutex.Unlock() 228 p.frames = append(p.frames, frame) 229 } 230 231 func (p cpuProfiler) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, _ []uint64) { 232 i := len(p.frames) - 1 233 f := p.frames[i] 234 p.frames = p.frames[:i] 235 236 if f.start != 0 { 237 duration := p.time() - f.start 238 if i := len(p.frames); i > 0 { 239 p.frames[i-1].sub += duration 240 } 241 duration -= f.sub 242 p.mutex.Lock() 243 if p.counts != nil { 244 p.counts.observe(f.trace, duration) 245 } 246 p.mutex.Unlock() 247 p.traces = append(p.traces, f.trace) 248 } 249 } 250 251 func (p cpuProfiler) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, _ error) { 252 p.After(ctx, mod, def, nil) 253 }