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