github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/imports/wasi_snapshot_preview1/wasi_bench_test.go (about) 1 package wasi_snapshot_preview1_test 2 3 import ( 4 "embed" 5 "io/fs" 6 "os" 7 "testing" 8 9 "github.com/tetratelabs/wazero" 10 "github.com/tetratelabs/wazero/api" 11 experimentalsys "github.com/tetratelabs/wazero/experimental/sys" 12 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 13 "github.com/tetratelabs/wazero/internal/sys" 14 "github.com/tetratelabs/wazero/internal/testing/proxy" 15 "github.com/tetratelabs/wazero/internal/testing/require" 16 "github.com/tetratelabs/wazero/internal/wasip1" 17 "github.com/tetratelabs/wazero/internal/wasm" 18 ) 19 20 // configArgsEnviron ensures the result data are the same between args and ENV. 21 var configArgsEnviron = wazero.NewModuleConfig(). 22 WithArgs("aa=bbbb", "cccccc=dddddddd", "eeeeeeeeee=ffffffffffff"). 23 WithEnv("aa", "bbbb"). 24 WithEnv("cccccc", "dddddddd"). 25 WithEnv("eeeeeeeeee", "ffffffffffff") 26 27 func Benchmark_ArgsEnviron(b *testing.B) { 28 r := wazero.NewRuntime(testCtx) 29 defer r.Close(testCtx) 30 31 mod, err := instantiateProxyModule(r, configArgsEnviron) 32 if err != nil { 33 b.Fatal(err) 34 } 35 36 for _, n := range []string{ 37 wasip1.ArgsGetName, 38 wasip1.ArgsSizesGetName, 39 wasip1.EnvironGetName, 40 wasip1.EnvironSizesGetName, 41 } { 42 n := n 43 fn := mod.ExportedFunction(n) 44 b.Run(n, func(b *testing.B) { 45 b.ReportAllocs() 46 for i := 0; i < b.N; i++ { 47 results, err := fn.Call(testCtx, uint64(0), uint64(4)) 48 if err != nil { 49 b.Fatal(err) 50 } 51 requireESuccess(b, results) 52 } 53 }) 54 } 55 } 56 57 type money struct{} 58 59 // Read implements io.Reader by returning endless '$'. 60 func (money) Read(b []byte) (n int, err error) { 61 for i := range b { 62 b[i] = '$' 63 } 64 65 return len(b), nil 66 } 67 68 func Benchmark_fdRead(b *testing.B) { 69 r := wazero.NewRuntime(testCtx) 70 defer r.Close(testCtx) 71 72 mod, err := instantiateProxyModule(r, wazero.NewModuleConfig().WithStdin(money{})) 73 if err != nil { 74 b.Fatal(err) 75 } 76 fn := mod.ExportedFunction(wasip1.FdReadName) 77 78 mod.Memory().Write(0, []byte{ 79 32, 0, 0, 0, // = iovs[0].offset 80 8, 0, 0, 0, // = iovs[0].length 81 40, 0, 0, 0, // = iovs[1].offset 82 8, 0, 0, 0, // = iovs[1].length 83 48, 0, 0, 0, // = iovs[2].offset 84 16, 0, 0, 0, // = iovs[2].length 85 64, 0, 0, 0, // = iovs[3].offset 86 16, 0, 0, 0, // = iovs[3].length 87 }) 88 89 benches := []struct { 90 name string 91 iovs uint32 92 iovsCount uint32 93 }{ 94 { 95 name: "1x8", 96 iovs: 0, 97 iovsCount: 1, 98 }, 99 { 100 name: "2x16", 101 iovs: 16, 102 iovsCount: 2, 103 }, 104 } 105 106 for _, bb := range benches { 107 bc := bb 108 109 b.ReportAllocs() 110 b.Run(bc.name, func(b *testing.B) { 111 resultNread := uint32(128) // arbitrary offset 112 113 for i := 0; i < b.N; i++ { 114 results, err := fn.Call(testCtx, uint64(0), uint64(bc.iovs), uint64(bc.iovsCount), uint64(resultNread)) 115 if err != nil { 116 b.Fatal(err) 117 } 118 requireESuccess(b, results) 119 } 120 }) 121 } 122 } 123 124 //go:embed testdata 125 var testdata embed.FS 126 127 func Benchmark_fdReaddir(b *testing.B) { 128 embedFS, err := fs.Sub(testdata, "testdata") 129 if err != nil { 130 b.Fatal(err) 131 } 132 133 benches := []struct { 134 name string 135 fs fs.FS 136 // dirMount ensures direct use of sys.FS 137 dirMount string 138 // twoCalls tests performance of reading a directory in two calls. 139 twoCalls bool 140 }{ 141 { 142 name: "embed.FS", 143 fs: embedFS, 144 }, 145 { 146 name: "embed.FS - two calls", 147 fs: embedFS, 148 twoCalls: true, 149 }, 150 { 151 name: "os.DirFS", 152 fs: os.DirFS("testdata"), 153 }, 154 { 155 name: "os.DirFS - two calls", 156 fs: os.DirFS("testdata"), 157 twoCalls: true, 158 }, 159 { 160 name: "sysfs.DirFS", 161 dirMount: "testdata", 162 }, 163 { 164 name: "sysfs.DirFS - two calls", 165 dirMount: "testdata", 166 twoCalls: true, 167 }, 168 } 169 170 for _, bb := range benches { 171 bc := bb 172 173 b.Run(bc.name, func(b *testing.B) { 174 r := wazero.NewRuntime(testCtx) 175 defer r.Close(testCtx) 176 177 fsConfig := wazero.NewFSConfig() 178 if bc.fs != nil { 179 fsConfig = fsConfig.WithFSMount(bc.fs, "") 180 } else { 181 fsConfig = fsConfig.WithDirMount(bc.dirMount, "") 182 } 183 184 mod, err := instantiateProxyModule(r, wazero.NewModuleConfig().WithFSConfig(fsConfig)) 185 if err != nil { 186 b.Fatal(err) 187 } 188 189 fn := mod.ExportedFunction(wasip1.FdReaddirName) 190 191 b.ResetTimer() 192 b.ReportAllocs() 193 for i := 0; i < b.N; i++ { 194 b.StopTimer() 195 196 cookie := 0 // where to begin (last read d_next) 197 resultBufused := 0 // where to write the amount used out of bufLen 198 buf := 8 // where to start the dirents 199 bufLen := 8096 // allow up to 8KB buffer usage 200 201 // Open the root directory as a file-descriptor. 202 fsc := mod.(*wasm.ModuleInstance).Sys.FS() 203 preopenFile, ok := fsc.LookupFile(sys.FdPreopen) 204 require.True(b, ok) 205 preopen := preopenFile.FS 206 fd, errno := fsc.OpenFile(preopen, ".", experimentalsys.O_RDONLY, 0) 207 if errno != 0 { 208 b.Fatal(errno) 209 } 210 211 // Time the call to write the dirents 212 b.StartTimer() 213 214 if bc.twoCalls { 215 // Read the dot entries 216 bufLen := wasip1.DirentSize + 1 // size of "." 217 bufLen += wasip1.DirentSize + 2 // size of ".." 218 219 results, err := fn.Call(testCtx, uint64(fd), uint64(buf), uint64(bufLen), uint64(cookie), uint64(resultBufused)) 220 if err != nil { 221 b.Fatal(err) 222 } 223 requireESuccess(b, results) 224 cookie = 2 // d_next of "..", the real file we couldn't read. 225 } 226 227 results, err := fn.Call(testCtx, uint64(fd), uint64(buf), uint64(bufLen), uint64(cookie), uint64(resultBufused)) 228 if err != nil { 229 b.Fatal(err) 230 } 231 b.StopTimer() 232 233 requireESuccess(b, results) 234 if errno = fsc.CloseFile(fd); errno != 0 { 235 b.Fatal(errno) 236 } 237 } 238 }) 239 } 240 } 241 242 func Benchmark_pathFilestat(b *testing.B) { 243 embedFS, err := fs.Sub(testdata, "testdata") 244 if err != nil { 245 b.Fatal(err) 246 } 247 248 benches := []struct { 249 name string 250 fs fs.FS 251 // dirMount ensures direct use of sys.FS 252 dirMount string 253 path string 254 fd int32 255 }{ 256 { 257 name: "embed.FS fd=root", 258 fs: embedFS, 259 path: "zig", 260 fd: sys.FdPreopen, 261 }, 262 { 263 name: "embed.FS fd=directory", 264 fs: embedFS, 265 path:/* zig */ "wasi.zig", 266 }, 267 { 268 name: "os.DirFS fd=root", 269 fs: os.DirFS("testdata"), 270 path: "zig", 271 fd: sys.FdPreopen, 272 }, 273 { 274 name: "os.DirFS fd=directory", 275 fs: os.DirFS("testdata"), 276 path:/* zig */ "wasi.zig", 277 }, 278 { 279 name: "sysfs.DirFS fd=root", 280 dirMount: "testdata", 281 path: "zig", 282 fd: sys.FdPreopen, 283 }, 284 { 285 name: "sysfs.DirFS fd=directory", 286 dirMount: "testdata", 287 path:/* zig */ "wasi.zig", 288 }, 289 } 290 291 for _, bb := range benches { 292 bc := bb 293 294 b.Run(bc.name, func(b *testing.B) { 295 r := wazero.NewRuntime(testCtx) 296 defer r.Close(testCtx) 297 298 fsConfig := wazero.NewFSConfig() 299 if bc.fs != nil { 300 fsConfig = fsConfig.WithFSMount(bc.fs, "") 301 } else { 302 fsConfig = fsConfig.WithDirMount(bc.dirMount, "") 303 } 304 305 mod, err := instantiateProxyModule(r, wazero.NewModuleConfig().WithFSConfig(fsConfig)) 306 if err != nil { 307 b.Fatal(err) 308 } 309 310 // If the benchmark's file descriptor isn't root, open the file 311 // under a pre-determined directory: zig 312 fd := sys.FdPreopen 313 if bc.fd != sys.FdPreopen { 314 fsc := mod.(*wasm.ModuleInstance).Sys.FS() 315 preopenFile, ok := fsc.LookupFile(sys.FdPreopen) 316 require.True(b, ok) 317 preopen := preopenFile.FS 318 fd, errno := fsc.OpenFile(preopen, "zig", experimentalsys.O_RDONLY, 0) 319 if errno != 0 { 320 b.Fatal(errno) 321 } 322 defer fsc.CloseFile(fd) //nolint 323 } 324 325 fn := mod.ExportedFunction(wasip1.PathFilestatGetName) 326 327 b.ResetTimer() 328 b.ReportAllocs() 329 for i := 0; i < b.N; i++ { 330 b.StopTimer() 331 332 flags := uint32(0) 333 path := uint32(0) 334 pathLen := len(bc.path) 335 resultFilestat := 1024 // where to write the stat 336 337 if !mod.Memory().WriteString(path, bc.path) { 338 b.Fatal("could not write path") 339 } 340 341 // Time the call to write the stat 342 b.StartTimer() 343 results, err := fn.Call(testCtx, uint64(fd), uint64(flags), uint64(path), uint64(pathLen), uint64(resultFilestat)) 344 if err != nil { 345 b.Fatal(err) 346 } 347 b.StopTimer() 348 349 requireESuccess(b, results) 350 } 351 }) 352 } 353 } 354 355 func requireESuccess(b *testing.B, results []uint64) { 356 b.Helper() 357 if errno := wasip1.Errno(results[0]); errno != 0 { 358 b.Fatal(wasip1.ErrnoName(errno)) 359 } 360 } 361 362 type writerFunc func(buf []byte) (n int, err error) 363 364 // Write implements io.Writer by calling writerFunc. 365 func (f writerFunc) Write(buf []byte) (n int, err error) { 366 return f(buf) 367 } 368 369 func Benchmark_fdWrite(b *testing.B) { 370 r := wazero.NewRuntime(testCtx) 371 defer r.Close(testCtx) 372 373 mod, err := instantiateProxyModule(r, wazero.NewModuleConfig(). 374 WithStdout(writerFunc(func(buf []byte) (n int, err error) { return len(buf), nil })), 375 ) 376 if err != nil { 377 b.Fatal(err) 378 } 379 fn := mod.ExportedFunction(wasip1.FdWriteName) 380 381 iovs := uint32(1) // arbitrary offset 382 mod.Memory().Write(0, []byte{ 383 '?', // `iovs` is after this 384 18, 0, 0, 0, // = iovs[0].offset 385 4, 0, 0, 0, // = iovs[0].length 386 23, 0, 0, 0, // = iovs[1].offset 387 2, 0, 0, 0, // = iovs[1].length 388 '?', // iovs[0].offset is after this 389 'w', 'a', 'z', 'e', // iovs[0].length bytes 390 '?', // iovs[1].offset is after this 391 'r', 'o', // iovs[1].length bytes 392 '?', 393 }) 394 395 iovsCount := uint32(2) // The count of iovs 396 resultNwritten := uint32(26) // arbitrary offset 397 398 benches := []struct { 399 name string 400 fd int32 401 }{ 402 { 403 name: "io.Writer", 404 fd: sys.FdStdout, 405 }, 406 { 407 name: "io.Discard", 408 fd: sys.FdStderr, 409 }, 410 } 411 412 for _, bb := range benches { 413 bc := bb 414 415 b.ReportAllocs() 416 b.Run(bc.name, func(b *testing.B) { 417 for i := 0; i < b.N; i++ { 418 results, err := fn.Call(testCtx, uint64(bc.fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten)) 419 if err != nil { 420 b.Fatal(err) 421 } 422 requireESuccess(b, results) 423 } 424 }) 425 } 426 } 427 428 // instantiateProxyModule instantiates a guest that re-exports WASI functions. 429 func instantiateProxyModule(r wazero.Runtime, config wazero.ModuleConfig) (api.Module, error) { 430 wasiModuleCompiled, err := wasi_snapshot_preview1.NewBuilder(r).Compile(testCtx) 431 if err != nil { 432 return nil, err 433 } 434 435 if _, err = r.InstantiateModule(testCtx, wasiModuleCompiled, wazero.NewModuleConfig()); err != nil { 436 return nil, err 437 } 438 439 proxyBin := proxy.NewModuleBinary(wasi_snapshot_preview1.ModuleName, wasiModuleCompiled) 440 441 proxyCompiled, err := r.CompileModule(testCtx, proxyBin) 442 if err != nil { 443 return nil, err 444 } 445 446 return r.InstantiateModule(testCtx, proxyCompiled, config) 447 }