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  }