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