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 }