wzprof

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2023 License: Apache-2.0 Imports: 30 Imported by: 1

README

Build Go Report Card Go Reference Apache 2 License

wzprof

wzprof, pronounced as you think it should, is a pprof based profiler for WebAssembly built on top of Wazero. It offers the ability to collect CPU and Memory profiles during the execution of WebAssembly modules.

If you are interested in taking a deep-dive into how wzprof is built, you might enjoy reading:

👉 Performance in the spotlight: WebAssembly profiling for everyone

Motivation

WebAssembly runtimes typically allow profiling guest code via an external profiler such as perf, but in many cases the recording and analysis of profiles remains a difficult task, especially due to features like JIT compilation.

pprof is the de-facto standard profiling tool for Go programs, and offers some of the simplest and quickest ways to gather insight into the performance of an application.

wzprof aims to combine the capabilities and user experience of pprof with a wazero.Runtime, enabling the profiling of any application compiled to WebAssembly.

Features

wzprof mimics the approach and workflow popularized by Go pprof, and extends it to collect profiles of WebAssembly programs compiled from any programming language. The profiles produced are designed to be compatible with pprof, allowing developers to use the classic go tool pprof workflow to analyize application performance.

  • CPU: calls sampling and on-CPU time.
  • Memory: allocations (see below).
  • DWARF support (demangling, source-level profiling).
  • Integrated pprof server.
  • Library and CLI interfaces.

Usage

You can either use wzprof as a CLI or as a library if you use the Wazero runtime libraries.

To install the latest version of wzprof:

go install github.com/stealthrocket/wzprof/cmd/wzprof@latest

To use the library as code in a Go program:

go get github.com/stealthrocket/wzprof@latest
Sampling

By default, wzprof will sample calls with a ratio of 1/19. Sampling is used to limit the overhead of the profilers but the default rate might not be suitable in some cases. For example, if your processes are short running and you don't see anything in the profile, you might want to disable the sampling. To do so, use -sample 1.

Run program to completion with CPU or memory profiling

In those examples we set the sample rate to 1 to capture all samples because the test programs complete quickly.

wzprof -sample 1 -memprofile /tmp/profile ./testdata/c/simple.wasm
wzprof -sample 1 -cpuprofile /tmp/profile ./testdata/c/crunch_numbers.wasm
go tool pprof -http :4000 /tmp/profile
Connect to running pprof server

Similarly to net/http/pprof, wzprof can expose a pprof-compatible http endpoint on behalf of the guest application:

wzprof -pprof-addr :8080 ...
go tool pprof -http :3030 'http://localhost:8080/debug/pprof/profile?seconds=5'
go tool pprof -http :3030 'http://localhost:8080/debug/pprof/heap'

Profilers

⚠️ The wzprof Go APIs depend on Wazero's experimental package which makes no guarantees of backward compatilbity!

The following code snippet demonstrates how to integrate the profilers to a Wazero runtime within a Go program:

sampleRate := 1.0

p := wzprof.ProfilingFor(wasmCode)
cpu := p.CPUProfiler()
mem := p.MemoryProfiler()

ctx := context.WithValue(context.Background(),
	experimental.FunctionListenerFactoryKey{},
	experimental.MultiFunctionListenerFactory(
		wzprof.Sample(sampleRate, cpu),
		wzprof.Sample(sampleRate, mem),
    ),
)

runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)

compiledModule, err := runtime.CompileModule(ctx, wasmCode)
if err != nil {
	log.Fatal("compiling wasm module:", err)
}

err = p.Prepare(compiledModule)
if err != nil {
	return fmt.Errorf("preparing wasm module: %w", err)
}

// The CPU profiler collects records of module execution between two time
// points, the program drives where the profiler is active by calling
// StartProfile/StopProfile.
cpu.StartProfile()

moduleInstance, err := runtime.InstantiateModule(ctx, compiledModule,
	wazero.NewModuleConfig(),
)
if err != nil {
	log.Fatal("instantiating wasm module:", err)
}
if err := moduleInstance.Close(ctx); err != nil {
	log.Fatal("closing wasm module:", err)
}

cpuProfile := cpu.StopProfile(sampleRate, symbols)
memProfile := mem.NewProfile(sampleRate, symbols)

if err := wzprof.WriteProfile("cpu.pprof", cpuProfile); err != nil {
	log.Fatal("writing CPU profile:", err)
}
if err := wzprof.WriteProfile("mem.pprof", memProfile); err != nil {
	log.Fatal("writing memory profile:", err)
}

Note that the program must spearate the compilation and instantiation of WebAssembly modules in order to use the profilers, because the module must be compiled first in order to build the list of symbols from the DWARF sections.

Memory

Memory profiling works by tracing specific functions. Supported functions are:

  • malloc
  • calloc
  • realloc
  • free
  • runtime.mallocgc
  • runtime.alloc

Feel free to open a pull request to support more memory-allocating functions!

CPU

wzprof has two CPU profilers: CPU samples and CPU time.

The CPU samples profiler gives a repesentation of the guest execution by counting the number of time it sees a unique stack trace.

The CPU time profiler measures the actual time spent on-CPU without taking into account the off-CPU time (e.g waiting for I/O). For this profiler, all the host-functions are considered off-CPU.

Language support

wzprof runs some heuristics to assess what the guest module is running to adapt the way it symbolizes and walks the stack. In all other cases, it defaults to inspecting the wasm stack and uses DWARF information if present in the module.

Golang

If the guest has been compiled by golang/go 1.21+, wzprof inspects the memory to walk the Go stack, which provides full call stacks, instead of the shortened versions you would get without this support.

In addition, wzprof parses pclntab to perform symbolization. This is the same mechanism the Go runtime itself uses to display meaningful stack traces when a panic occurs.

Python 3.11

If the guest is CPython 3.11 and has been compiled with debug symbols (such as timecraft's), wzprof walks the Python interpreter call stack, not the C stack it would otherwise report. This provides more meaningful profiling information on the script being executed.

At the moment it does not support merging the C extension calls into the Python interpreter stack.

Note that a current limitation of the implementation is that unloading or reloading modules may result in an incorrect profile. If that's a problem for you please file an issue in the github tracker.

DWARF (C, Rust, Zig...)

As a fallback, if DWARF sections are available, wzprof symbolizes the wasm stack trace using the DWARF symbols stored in custom sections of the module. For this to work, you need to make sure your compiler generates those sections. For example, use -g when compiling with clang:

clang code.c -o code.wasm -g -target wasm32

Warning When using clang with any optimization level other than -O0, it will automatically run wasm-opt if that program is in your PATH. It makes DWARF information unusable by wzprof. Make sure clang can't find wasm-opt during compilation. See llvm/llvm-project#55781.

Contributing

Pull requests are welcome! Anything that is not a simple fix would probably benefit from being discussed in an issue first.

Remember to be respectful and open minded!

Documentation

Overview

Package wzprof provides pprof-based capabilities to Wazero-based hosts.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Flag added in v0.1.5

Flag returns a function listener factory which creates listeners where calls to their Before/After methods are gated by the boolean flag pointed at by the first argument.

The sampling mechanism is similar to the one implemented by Sample but it gives the application control over when the listeners are enabled instead of leaving the selection up to a probabilistic model.

func Handler

func Handler(sampleRate float64, profilers ...Profiler) http.Handler

Handler returns a http handler which responds with the pprof-formatted profile named by the request. For example, "/debug/pprof/heap" serves the "heap" profile.

Handler responds to a request for "/debug/pprof/" with an HTML page listing the available profiles.

func Sample

Sample returns a function listener factory which creates listeners where calls to their Before/After methods is sampled at the given sample rate.

Giving a zero or negative sampling rate disables the function listeners entirely.

Giving a sampling rate of one or more disables sampling, function listeners are invoked for all function calls.

func WriteProfile

func WriteProfile(path string, prof *profile.Profile) error

WriteProfile writes a profile to a file at the given path.

Types

type CPUProfiler

type CPUProfiler struct {
	// contains filtered or unexported fields
}

CPUProfiler is the implementation of a performance profiler recording samples of CPU time spent in functions of a WebAssembly module.

The profiler generates samples of two types: - "sample" counts the number of function calls. - "cpu" records the time spent in function calls (in nanoseconds).

func (*CPUProfiler) Count

func (p *CPUProfiler) Count() int

Count returns the number of execution stacks currently recorded in p.

func (*CPUProfiler) Desc

func (p *CPUProfiler) Desc() string

Desc returns a description copied from net/http/pprof.

func (*CPUProfiler) Name

func (p *CPUProfiler) Name() string

Name returns "profile" to match the name of the CPU profiler in pprof.

func (*CPUProfiler) NewFunctionListener

func (p *CPUProfiler) NewFunctionListener(def api.FunctionDefinition) experimental.FunctionListener

NewFunctionListener returns a function listener suited to record CPU timings of calls to the function passed as argument.

func (*CPUProfiler) NewHandler

func (p *CPUProfiler) NewHandler(sampleRate float64) http.Handler

NewHandler returns a http handler allowing the profiler to be exposed on a pprof-compatible http endpoint.

The sample rate is a value between 0 and 1 used to scale the profile results based on the sampling rate applied to the profiler so the resulting values remain representative.

The symbolizer passed as argument is used to resolve names of program locations recorded in the profile.

func (*CPUProfiler) SampleType

func (p *CPUProfiler) SampleType() []*profile.ValueType

SampleType returns the set of value types present in samples recorded by the CPU profiler.

func (*CPUProfiler) StartProfile

func (p *CPUProfiler) StartProfile() bool

StartProfile begins recording the CPU profile. The method returns a boolean to indicate whether starting the profile succeeded (e.g. false is returned if it was already started).

func (*CPUProfiler) StopProfile

func (p *CPUProfiler) StopProfile(sampleRate float64) *profile.Profile

StopProfile stops recording and returns the CPU profile. The method returns nil if recording of the CPU profile wasn't started.

type CPUProfilerOption

type CPUProfilerOption func(*CPUProfiler)

CPUProfilerOption is a type used to represent configuration options for CPUProfiler instances created by NewCPUProfiler.

func HostTime

func HostTime(enable bool) CPUProfilerOption

HostTime confiures a CPU time profiler to account for time spent in calls to host functions.

Default to false.

func TimeFunc

func TimeFunc(time func() int64) CPUProfilerOption

TimeFunc configures the time function used by the CPU profiler to collect monotonic timestamps.

By default, the system's monotonic time is used.

type MemoryProfiler

type MemoryProfiler struct {
	// contains filtered or unexported fields
}

MemoryProfiler is the implementation of a performance profiler recording samples of memory allocation and utilization.

The profiler generates the following samples: - "alloc_objects" records the locations where objects are allocated - "alloc_space" records the locations where bytes are allocated - "inuse_objects" records the allocation of active objects - "inuse_space" records the bytes used by active objects

"alloc_objects" and "alloc_space" are all time counters since the start of the program, while "inuse_objects" and "inuse_space" capture the current state of the program at the time the profile is taken.

func (*MemoryProfiler) Count

func (p *MemoryProfiler) Count() int

Count returns the number of allocation stacks recorded in p.

func (*MemoryProfiler) Desc

func (p *MemoryProfiler) Desc() string

Desc returns a description copied from net/http/pprof.

func (*MemoryProfiler) Name

func (p *MemoryProfiler) Name() string

Name returns "allocs" to match the name of the memory profiler in pprof.

func (*MemoryProfiler) NewFunctionListener

NewFunctionListener returns a function listener suited to install a hook on functions responsible for memory allocation.

The listener recognizes multiple memory allocation functions used by compilers and libraries. It uses the function name to detect memory allocators, currently supporting libc, Go, and TinyGo.

func (*MemoryProfiler) NewHandler

func (p *MemoryProfiler) NewHandler(sampleRate float64) http.Handler

NewHandler returns a http handler allowing the profiler to be exposed on a pprof-compatible http endpoint.

The sample rate is a value between 0 and 1 used to scale the profile results based on the sampling rate applied to the profiler so the resulting values remain representative.

The symbolizer passed as argument is used to resolve names of program locations recorded in the profile.

func (*MemoryProfiler) NewProfile

func (p *MemoryProfiler) NewProfile(sampleRate float64) *profile.Profile

NewProfile takes a snapshot of the current memory allocation state and builds a profile representing the state of the program memory.

func (*MemoryProfiler) SampleType

func (p *MemoryProfiler) SampleType() []*profile.ValueType

SampleType returns the set of value types present in samples recorded by the memory profiler.

type MemoryProfilerOption

type MemoryProfilerOption func(*MemoryProfiler)

MemoryProfilerOption is a type used to represent configuration options for MemoryProfiler instances created by NewMemoryProfiler.

func InuseMemory

func InuseMemory(enable bool) MemoryProfilerOption

InuseMemory is a memory profiler option which enables tracking of allocated and freed objects to generate snapshots of the current state of a program memory.

type Profiler

type Profiler interface {
	experimental.FunctionListenerFactory

	// Name of the profiler.
	Name() string

	// Desc is a human-readable description of the profiler.
	Desc() string

	// Count the number of execution stacks recorded in the profiler.
	Count() int

	// SampleType returns the set of value types present in samples recorded by
	// the profiler.
	SampleType() []*profile.ValueType

	// NewHandler returns a new http handler suited to expose profiles on a
	// pprof endpoint.
	NewHandler(sampleRate float64) http.Handler
}

Profiler is an interface implemented by all profiler types available in this package.

type Profiling added in v0.1.5

type Profiling struct {
	// contains filtered or unexported fields
}

Profiling mechanism for a given WASM binary. Entry point to generate Profilers.

func ProfilingFor added in v0.1.5

func ProfilingFor(wasm []byte) *Profiling

ProfilingFor a given wasm binary. The resulting Profiling needs to be prepared after Wazero module compilation.

func (*Profiling) CPUProfiler added in v0.1.5

func (p *Profiling) CPUProfiler(options ...CPUProfilerOption) *CPUProfiler

CPUProfiler constructs a new instance of CPUProfiler using the given time function to record the CPU time consumed.

func (*Profiling) MemoryProfiler added in v0.1.5

func (p *Profiling) MemoryProfiler(options ...MemoryProfilerOption) *MemoryProfiler

MemoryProfiler constructs a new instance of MemoryProfiler using the given time function to record the profile execution time.

func (*Profiling) Prepare added in v0.1.5

func (p *Profiling) Prepare(mod wazero.CompiledModule) error

Prepare selects the most appropriate analysis functions for the guest code in the provided module.

Directories

Path Synopsis
cmd
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL