wa-lang.org/wazero@v1.0.2/internal/integration_test/bench/bench_test.go (about)

     1  package bench
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	_ "embed"
     7  	"fmt"
     8  	"runtime"
     9  	"strconv"
    10  	"sync"
    11  	"testing"
    12  
    13  	"wa-lang.org/wazero"
    14  	"wa-lang.org/wazero/api"
    15  	"wa-lang.org/wazero/experimental"
    16  	"wa-lang.org/wazero/imports/wasi_snapshot_preview1"
    17  	"wa-lang.org/wazero/internal/platform"
    18  )
    19  
    20  // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
    21  var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
    22  
    23  // caseWasm was compiled from TinyGo testdata/case.go
    24  //
    25  //go:embed testdata/case.wasm
    26  var caseWasm []byte
    27  
    28  func BenchmarkInvocation(b *testing.B) {
    29  	b.Run("interpreter", func(b *testing.B) {
    30  		m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigInterpreter())
    31  		defer m.Close(testCtx)
    32  		runAllInvocationBenches(b, m)
    33  	})
    34  	if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" {
    35  		b.Run("compiler", func(b *testing.B) {
    36  			m := instantiateHostFunctionModuleWithEngine(b, wazero.NewRuntimeConfigCompiler())
    37  			defer m.Close(testCtx)
    38  			runAllInvocationBenches(b, m)
    39  		})
    40  	}
    41  }
    42  
    43  func BenchmarkInitialization(b *testing.B) {
    44  	b.Run("interpreter", func(b *testing.B) {
    45  		r := createRuntime(b, wazero.NewRuntimeConfigInterpreter())
    46  		runInitializationBench(b, r)
    47  	})
    48  
    49  	b.Run("interpreter-multiple", func(b *testing.B) {
    50  		r := createRuntime(b, wazero.NewRuntimeConfigInterpreter())
    51  		runInitializationConcurrentBench(b, r)
    52  	})
    53  
    54  	if runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" {
    55  		b.Run("compiler", func(b *testing.B) {
    56  			r := createRuntime(b, wazero.NewRuntimeConfigCompiler())
    57  			runInitializationBench(b, r)
    58  		})
    59  
    60  		b.Run("compiler-multiple", func(b *testing.B) {
    61  			r := createRuntime(b, wazero.NewRuntimeConfigCompiler())
    62  			runInitializationConcurrentBench(b, r)
    63  		})
    64  	}
    65  }
    66  
    67  func BenchmarkCompilation(b *testing.B) {
    68  	if !platform.CompilerSupported() {
    69  		b.Skip()
    70  	}
    71  
    72  	// Note: recreate runtime each time in the loop to ensure that
    73  	// recompilation happens if the extern cache is not used.
    74  	b.Run("with extern cache", func(b *testing.B) {
    75  		ctx, err := experimental.WithCompilationCacheDirName(context.Background(), b.TempDir())
    76  		if err != nil {
    77  			b.Fatal(err)
    78  		}
    79  		for i := 0; i < b.N; i++ {
    80  			r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
    81  			runCompilation(b, r)
    82  		}
    83  	})
    84  	b.Run("without extern cache", func(b *testing.B) {
    85  		b.ResetTimer()
    86  		for i := 0; i < b.N; i++ {
    87  			r := wazero.NewRuntimeWithConfig(context.Background(), wazero.NewRuntimeConfigCompiler())
    88  			runCompilation(b, r)
    89  		}
    90  	})
    91  }
    92  
    93  func runCompilation(b *testing.B, r wazero.Runtime) wazero.CompiledModule {
    94  	compiled, err := r.CompileModule(testCtx, caseWasm)
    95  	if err != nil {
    96  		b.Fatal(err)
    97  	}
    98  	return compiled
    99  }
   100  
   101  func runInitializationBench(b *testing.B, r wazero.Runtime) {
   102  	compiled := runCompilation(b, r)
   103  	defer compiled.Close(testCtx)
   104  	// Configure with real sources to avoid performance hit initializing fake ones. These sources are not used
   105  	// in the benchmark.
   106  	config := wazero.NewModuleConfig().WithSysNanotime().WithSysWalltime().WithRandSource(rand.Reader)
   107  	b.ResetTimer()
   108  	for i := 0; i < b.N; i++ {
   109  		mod, err := r.InstantiateModule(testCtx, compiled, config)
   110  		if err != nil {
   111  			b.Fatal(err)
   112  		}
   113  		mod.Close(testCtx)
   114  	}
   115  }
   116  
   117  func runInitializationConcurrentBench(b *testing.B, r wazero.Runtime) {
   118  	compiled := runCompilation(b, r)
   119  	defer compiled.Close(testCtx)
   120  	// Configure with real sources to avoid performance hit initializing fake ones. These sources are not used
   121  	// in the benchmark.
   122  	config := wazero.NewModuleConfig().WithSysNanotime().WithSysWalltime().WithRandSource(rand.Reader)
   123  	wg := &sync.WaitGroup{}
   124  	wg.Add(b.N)
   125  	b.ResetTimer()
   126  	for i := 0; i < b.N; i++ {
   127  		go func(name string) {
   128  			_, err := r.InstantiateModule(testCtx, compiled, config.WithName(name))
   129  			if err != nil {
   130  				b.Error(err)
   131  			}
   132  			wg.Done()
   133  		}(strconv.Itoa(i))
   134  	}
   135  	wg.Wait()
   136  	b.StopTimer()
   137  }
   138  
   139  func runAllInvocationBenches(b *testing.B, m api.Module) {
   140  	runBase64Benches(b, m)
   141  	runFibBenches(b, m)
   142  	runStringManipulationBenches(b, m)
   143  	runReverseArrayBenches(b, m)
   144  	runRandomMatMul(b, m)
   145  }
   146  
   147  func runBase64Benches(b *testing.B, m api.Module) {
   148  	base64 := m.ExportedFunction("base64")
   149  
   150  	for _, numPerExec := range []int{5, 100, 10000} {
   151  		numPerExec := uint64(numPerExec)
   152  		b.ResetTimer()
   153  		b.Run(fmt.Sprintf("base64_%d_per_exec", numPerExec), func(b *testing.B) {
   154  			for i := 0; i < b.N; i++ {
   155  				if _, err := base64.Call(testCtx, numPerExec); err != nil {
   156  					b.Fatal(err)
   157  				}
   158  			}
   159  		})
   160  	}
   161  }
   162  
   163  func runFibBenches(b *testing.B, m api.Module) {
   164  	fibonacci := m.ExportedFunction("fibonacci")
   165  
   166  	for _, num := range []int{5, 10, 20, 30} {
   167  		num := uint64(num)
   168  		b.ResetTimer()
   169  		b.Run(fmt.Sprintf("fib_for_%d", num), func(b *testing.B) {
   170  			for i := 0; i < b.N; i++ {
   171  				if _, err := fibonacci.Call(testCtx, num); err != nil {
   172  					b.Fatal(err)
   173  				}
   174  			}
   175  		})
   176  	}
   177  }
   178  
   179  func runStringManipulationBenches(b *testing.B, m api.Module) {
   180  	stringManipulation := m.ExportedFunction("string_manipulation")
   181  
   182  	for _, initialSize := range []int{50, 100, 1000} {
   183  		initialSize := uint64(initialSize)
   184  		b.ResetTimer()
   185  		b.Run(fmt.Sprintf("string_manipulation_size_%d", initialSize), func(b *testing.B) {
   186  			for i := 0; i < b.N; i++ {
   187  				if _, err := stringManipulation.Call(testCtx, initialSize); err != nil {
   188  					b.Fatal(err)
   189  				}
   190  			}
   191  		})
   192  	}
   193  }
   194  
   195  func runReverseArrayBenches(b *testing.B, m api.Module) {
   196  	reverseArray := m.ExportedFunction("reverse_array")
   197  
   198  	for _, arraySize := range []int{500, 1000, 10000} {
   199  		arraySize := uint64(arraySize)
   200  		b.ResetTimer()
   201  		b.Run(fmt.Sprintf("reverse_array_size_%d", arraySize), func(b *testing.B) {
   202  			for i := 0; i < b.N; i++ {
   203  				if _, err := reverseArray.Call(testCtx, arraySize); err != nil {
   204  					b.Fatal(err)
   205  				}
   206  			}
   207  		})
   208  	}
   209  }
   210  
   211  func runRandomMatMul(b *testing.B, m api.Module) {
   212  	randomMatMul := m.ExportedFunction("random_mat_mul")
   213  
   214  	for _, matrixSize := range []int{5, 10, 20} {
   215  		matrixSize := uint64(matrixSize)
   216  		b.ResetTimer()
   217  		b.Run(fmt.Sprintf("random_mat_mul_size_%d", matrixSize), func(b *testing.B) {
   218  			for i := 0; i < b.N; i++ {
   219  				if _, err := randomMatMul.Call(testCtx, matrixSize); err != nil {
   220  					b.Fatal(err)
   221  				}
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  func instantiateHostFunctionModuleWithEngine(b *testing.B, config wazero.RuntimeConfig) api.Module {
   228  	r := createRuntime(b, config)
   229  
   230  	// InstantiateModuleFromBinary runs the "_start" function which is what TinyGo compiles "main" to.
   231  	m, err := r.InstantiateModuleFromBinary(testCtx, caseWasm)
   232  	if err != nil {
   233  		b.Fatal(err)
   234  	}
   235  	return m
   236  }
   237  
   238  func createRuntime(b *testing.B, config wazero.RuntimeConfig) wazero.Runtime {
   239  	getRandomString := func(ctx context.Context, m api.Module, retBufPtr uint32, retBufSize uint32) {
   240  		results, err := m.ExportedFunction("allocate_buffer").Call(ctx, 10)
   241  		if err != nil {
   242  			b.Fatal(err)
   243  		}
   244  
   245  		offset := uint32(results[0])
   246  		m.Memory().WriteUint32Le(ctx, retBufPtr, offset)
   247  		m.Memory().WriteUint32Le(ctx, retBufSize, 10)
   248  		b := make([]byte, 10)
   249  		_, _ = rand.Read(b)
   250  		m.Memory().Write(ctx, offset, b)
   251  	}
   252  
   253  	r := wazero.NewRuntimeWithConfig(testCtx, config)
   254  
   255  	_, err := r.NewHostModuleBuilder("env").
   256  		NewFunctionBuilder().WithFunc(getRandomString).Export("get_random_string").
   257  		Instantiate(testCtx, r)
   258  	if err != nil {
   259  		b.Fatal(err)
   260  	}
   261  
   262  	// Note: host_func.go doesn't directly use WASI, but TinyGo needs to be initialized as a WASI Command.
   263  	// Add WASI to satisfy import tests
   264  	wasi_snapshot_preview1.MustInstantiate(testCtx, r)
   265  	return r
   266  }