github.com/stealthrocket/wzprof@v0.2.1-0.20230830205924-5fa86be5e5b3/cmd/wzprof/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"math"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"os/signal"
    15  	"path/filepath"
    16  	"runtime"
    17  	"runtime/pprof"
    18  	"strings"
    19  
    20  	"github.com/google/pprof/profile"
    21  	"github.com/tetratelabs/wazero"
    22  	"github.com/tetratelabs/wazero/experimental"
    23  	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
    24  
    25  	"github.com/stealthrocket/wzprof"
    26  )
    27  
    28  func main() {
    29  	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    30  	defer cancel()
    31  
    32  	if err := run(ctx); err != nil {
    33  		stderr.Print(err)
    34  		os.Exit(1)
    35  	}
    36  }
    37  
    38  const defaultSampleRate = 1.0 / 19
    39  
    40  type program struct {
    41  	filePath    string
    42  	args        []string
    43  	pprofAddr   string
    44  	cpuProfile  string
    45  	memProfile  string
    46  	sampleRate  float64
    47  	hostProfile bool
    48  	hostTime    bool
    49  	inuseMemory bool
    50  	mounts      []string
    51  }
    52  
    53  func (prog *program) run(ctx context.Context) error {
    54  	wasmName := filepath.Base(prog.filePath)
    55  	wasmCode, err := os.ReadFile(prog.filePath)
    56  	if err != nil {
    57  		return fmt.Errorf("reading wasm module: %w", err)
    58  	}
    59  
    60  	p := wzprof.ProfilingFor(wasmCode)
    61  
    62  	cpu := p.CPUProfiler(wzprof.HostTime(prog.hostTime))
    63  	mem := p.MemoryProfiler(wzprof.InuseMemory(prog.inuseMemory))
    64  
    65  	var listeners []experimental.FunctionListenerFactory
    66  	if prog.cpuProfile != "" || prog.pprofAddr != "" {
    67  		stdout.Printf("enabling cpu profiler")
    68  		listeners = append(listeners, cpu)
    69  	}
    70  	if prog.memProfile != "" || prog.pprofAddr != "" {
    71  		stdout.Printf("enabling memory profiler")
    72  		listeners = append(listeners, mem)
    73  	}
    74  	if prog.sampleRate < 1 {
    75  		stdout.Printf("configuring sampling rate to %.2g%%", prog.sampleRate)
    76  		for i, lstn := range listeners {
    77  			listeners[i] = wzprof.Sample(prog.sampleRate, lstn)
    78  		}
    79  	}
    80  
    81  	ctx = context.WithValue(ctx,
    82  		experimental.FunctionListenerFactoryKey{},
    83  		experimental.MultiFunctionListenerFactory(listeners...),
    84  	)
    85  
    86  	runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().
    87  		WithDebugInfoEnabled(true).
    88  		WithCustomSections(true))
    89  
    90  	stdout.Printf("compiling wasm module %s", prog.filePath)
    91  	compiledModule, err := runtime.CompileModule(ctx, wasmCode)
    92  	if err != nil {
    93  		return fmt.Errorf("compiling wasm module: %w", err)
    94  	}
    95  	err = p.Prepare(compiledModule)
    96  	if err != nil {
    97  		return fmt.Errorf("preparing wasm module: %w", err)
    98  	}
    99  
   100  	if prog.pprofAddr != "" {
   101  		u := &url.URL{Scheme: "http", Host: prog.pprofAddr, Path: "/debug/pprof"}
   102  		stdout.Printf("starting prrof http sever at %s", u)
   103  
   104  		server := http.NewServeMux()
   105  		server.Handle("/debug/pprof/", wzprof.Handler(prog.sampleRate, cpu, mem))
   106  
   107  		go func() {
   108  			if err := http.ListenAndServe(prog.pprofAddr, server); err != nil {
   109  				stderr.Println(err)
   110  			}
   111  		}()
   112  	}
   113  
   114  	if prog.hostProfile {
   115  		if prog.cpuProfile != "" {
   116  			f, err := os.Create(prog.cpuProfile)
   117  			if err != nil {
   118  				return err
   119  			}
   120  			startCPUProfile(f)
   121  			defer stopCPUProfile(f)
   122  		}
   123  
   124  		if prog.memProfile != "" {
   125  			f, err := os.Create(prog.memProfile)
   126  			if err != nil {
   127  				return err
   128  			}
   129  			defer writeHeapProfile(f)
   130  		}
   131  	}
   132  
   133  	if prog.cpuProfile != "" {
   134  		cpu.StartProfile()
   135  		defer func() {
   136  			p := cpu.StopProfile(prog.sampleRate)
   137  			if !prog.hostProfile {
   138  				writeProfile("cpu", wasmName, prog.cpuProfile, p)
   139  			}
   140  		}()
   141  	}
   142  
   143  	if prog.memProfile != "" {
   144  		defer func() {
   145  			p := mem.NewProfile(prog.sampleRate)
   146  			if !prog.hostProfile {
   147  				writeProfile("memory", wasmName, prog.memProfile, p)
   148  			}
   149  		}()
   150  	}
   151  
   152  	ctx, cancel := context.WithCancelCause(ctx)
   153  	go func() {
   154  		defer cancel(nil)
   155  		stdout.Printf("instantiating host module: wasi_snapshot_preview1")
   156  		wasi_snapshot_preview1.MustInstantiate(ctx, runtime)
   157  
   158  		config := wazero.NewModuleConfig().
   159  			WithStdout(os.Stdout).
   160  			WithStderr(os.Stderr).
   161  			WithStdin(os.Stdin).
   162  			WithRandSource(rand.Reader).
   163  			WithSysNanosleep().
   164  			WithSysNanotime().
   165  			WithSysWalltime().
   166  			WithArgs(append([]string{wasmName}, prog.args...)...).
   167  			WithFSConfig(createFSConfig(prog.mounts))
   168  
   169  		moduleName := compiledModule.Name()
   170  		if moduleName == "" {
   171  			moduleName = wasmName
   172  		}
   173  		stdout.Printf("instantiating guest module: %s", moduleName)
   174  		instance, err := runtime.InstantiateModule(ctx, compiledModule, config)
   175  		if err != nil {
   176  			cancel(fmt.Errorf("instantiating guest module: %w", err))
   177  			return
   178  		}
   179  		if err := instance.Close(ctx); err != nil {
   180  			cancel(fmt.Errorf("closing guest module: %w", err))
   181  			return
   182  		}
   183  	}()
   184  
   185  	<-ctx.Done()
   186  	return silenceContextCanceled(context.Cause(ctx))
   187  }
   188  
   189  func silenceContextCanceled(err error) error {
   190  	if err == context.Canceled {
   191  		err = nil
   192  	}
   193  	return err
   194  }
   195  
   196  var (
   197  	pprofAddr    string
   198  	cpuProfile   string
   199  	memProfile   string
   200  	sampleRate   float64
   201  	hostProfile  bool
   202  	hostTime     bool
   203  	inuseMemory  bool
   204  	verbose      bool
   205  	mounts       string
   206  	printVersion bool
   207  
   208  	version = "dev"
   209  	stdout  = log.Default()
   210  	stderr  = log.New(os.Stderr, "ERROR: ", 0)
   211  )
   212  
   213  func init() {
   214  	flag.StringVar(&pprofAddr, "pprof-addr", "", "Address where to expose a pprof HTTP endpoint.")
   215  	flag.StringVar(&cpuProfile, "cpuprofile", "", "Write a CPU profile to the specified file before exiting.")
   216  	flag.StringVar(&memProfile, "memprofile", "", "Write a memory profile to the specified file before exiting.")
   217  	flag.Float64Var(&sampleRate, "sample", defaultSampleRate, "Set the profile sampling rate (0-1).")
   218  	flag.BoolVar(&hostProfile, "host", false, "Generate profiles of the host instead of the guest application.")
   219  	flag.BoolVar(&hostTime, "iowait", false, "Include time spent waiting on I/O in guest CPU profile.")
   220  	flag.BoolVar(&inuseMemory, "inuse", false, "Include snapshots of memory in use (experimental).")
   221  	flag.BoolVar(&verbose, "verbose", false, "Enable more output")
   222  	flag.StringVar(&mounts, "mount", "", "Comma-separated list of directories to mount (e.g. /tmp:/tmp:ro).")
   223  	flag.BoolVar(&printVersion, "version", false, "Print the wzprof version.")
   224  }
   225  
   226  func run(ctx context.Context) error {
   227  	flag.Parse()
   228  
   229  	if printVersion {
   230  		fmt.Printf("wzprof version %s\n", version)
   231  		return nil
   232  	}
   233  
   234  	args := flag.Args()
   235  	if len(args) < 1 {
   236  		// TODO: print flag usage
   237  		return fmt.Errorf("usage: wzprof </path/to/app.wasm>")
   238  	}
   239  
   240  	if verbose {
   241  		log.SetPrefix("==> ")
   242  		log.SetFlags(0)
   243  		log.SetOutput(os.Stdout)
   244  	} else {
   245  		log.SetOutput(io.Discard)
   246  	}
   247  
   248  	filePath := args[0]
   249  
   250  	rate := int(math.Ceil(1 / sampleRate))
   251  	runtime.SetBlockProfileRate(rate)
   252  	runtime.SetMutexProfileFraction(rate)
   253  
   254  	return (&program{
   255  		filePath:    filePath,
   256  		args:        args[1:],
   257  		pprofAddr:   pprofAddr,
   258  		cpuProfile:  cpuProfile,
   259  		memProfile:  memProfile,
   260  		sampleRate:  sampleRate,
   261  		hostProfile: hostProfile,
   262  		hostTime:    hostTime,
   263  		inuseMemory: inuseMemory,
   264  		mounts:      split(mounts),
   265  	}).run(ctx)
   266  }
   267  
   268  func split(s string) []string {
   269  	if s == "" {
   270  		return nil
   271  	}
   272  	return strings.Split(s, ",")
   273  }
   274  
   275  func startCPUProfile(f *os.File) {
   276  	if err := pprof.StartCPUProfile(f); err != nil {
   277  		stderr.Print("starting CPU profile:", err)
   278  	}
   279  }
   280  
   281  func stopCPUProfile(f *os.File) {
   282  	stdout.Printf("writing host cpu profile to %s", f.Name())
   283  	pprof.StopCPUProfile()
   284  }
   285  
   286  func writeHeapProfile(f *os.File) {
   287  	stdout.Printf("writing host memory profile to %s", f.Name())
   288  	if err := pprof.WriteHeapProfile(f); err != nil {
   289  		stderr.Print("writing memory profile:", err)
   290  	}
   291  }
   292  
   293  func writeProfile(profileName, wasmName, path string, prof *profile.Profile) {
   294  	m := &profile.Mapping{ID: 1, File: wasmName}
   295  	prof.Mapping = []*profile.Mapping{m}
   296  	stdout.Printf("writing guest %s profile to %s", profileName, path)
   297  	if err := wzprof.WriteProfile(path, prof); err != nil {
   298  		stderr.Print("writing profile:", err)
   299  	}
   300  }
   301  
   302  func createFSConfig(mounts []string) wazero.FSConfig {
   303  	fs := wazero.NewFSConfig()
   304  	for _, m := range mounts {
   305  		parts := strings.Split(m, ":")
   306  		if len(parts) < 2 {
   307  			stderr.Fatalf("invalid mount: %s", m)
   308  		}
   309  
   310  		var mode string
   311  		if len(parts) == 3 {
   312  			mode = parts[2]
   313  		}
   314  
   315  		if mode == "ro" {
   316  			fs = fs.WithReadOnlyDirMount(parts[0], parts[1])
   317  			continue
   318  		}
   319  
   320  		fs = fs.WithDirMount(parts[0], parts[1])
   321  	}
   322  	return fs
   323  }