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  }