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 }