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