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  }