wa-lang.org/wazero@v1.0.2/internal/integration_test/vs/bench.go (about) 1 package vs 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path" 8 "runtime" 9 "sort" 10 "testing" 11 "text/tabwriter" 12 13 "wa-lang.org/wazero/internal/testing/require" 14 ) 15 16 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 17 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 18 19 // ensureCompilerFastest is overridable via ldflags. e.g. 20 // 21 // -ldflags '-X github.com/tetratelabs/wazero/internal/integration_test/vs.ensureCompilerFastest=true' 22 var ensureCompilerFastest = "false" 23 24 const compilerRuntime = "wazero-compiler" 25 26 // runTestBenchmark_Call_CompilerFastest ensures that Compiler is the fastest engine for function invocations. 27 // This is disabled by default, and can be run with -ldflags '-X github.com/tetratelabs/wazero/vs.ensureCompilerFastest=true'. 28 func runTestBenchmark_Call_CompilerFastest(t *testing.T, rtCfg *RuntimeConfig, name string, call func(Module, int) error, vsRuntime Runtime) { 29 if ensureCompilerFastest != "true" { 30 t.Skip() 31 } 32 33 type benchResult struct { 34 name string 35 nsOp float64 36 } 37 38 results := make([]benchResult, 0, 2) 39 // Add the result for Compiler 40 compilerNsOp := runCallBenchmark(NewWazeroCompilerRuntime(), rtCfg, call) 41 results = append(results, benchResult{name: compilerRuntime, nsOp: compilerNsOp}) 42 43 // Add a result for the runtime we're comparing against 44 vsNsOp := runCallBenchmark(vsRuntime, rtCfg, call) 45 results = append(results, benchResult{name: vsRuntime.Name(), nsOp: vsNsOp}) 46 47 sort.Slice(results, func(i, j int) bool { 48 return results[i].nsOp < results[j].nsOp 49 }) 50 51 // Print results before deciding if this failed 52 w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) 53 _, _ = fmt.Fprintf(w, "Benchmark%s/Call-16\n", name) 54 for _, result := range results { 55 _, _ = fmt.Fprintf(w, "%s\t%.2f\tns/op\n", result.name, result.nsOp) 56 } 57 _ = w.Flush() 58 59 // Fail if compiler wasn't fastest! 60 require.Equal(t, compilerRuntime, results[0].name, "%s is faster than %s. "+ 61 "Run with ensureCompilerFastest=false instead to see the detailed result", 62 results[0].name, compilerRuntime) 63 } 64 65 func runCallBenchmark(rt Runtime, rtCfg *RuntimeConfig, call func(Module, int) error) float64 { 66 result := testing.Benchmark(func(b *testing.B) { 67 benchmarkCall(b, rt, rtCfg, call) 68 }) 69 // https://github.com/golang/go/blob/fd09e88722e0af150bf8960e95e8da500ad91001/src/testing/benchmark.go#L428-L432 70 nsOp := float64(result.T.Nanoseconds()) / float64(result.N) 71 return nsOp 72 } 73 74 func benchmark(b *testing.B, runtime func() Runtime, rtCfg *RuntimeConfig, call func(Module, int) error) { 75 rt := runtime() 76 b.Run("Compile", func(b *testing.B) { 77 benchmarkCompile(b, rt, rtCfg) 78 }) 79 b.Run("Instantiate", func(b *testing.B) { 80 benchmarkInstantiate(b, rt, rtCfg) 81 }) 82 83 // Don't burn CPU when this is already going to be called in runTestBenchmark_Call_CompilerFastest 84 if ensureCompilerFastest != "true" || rt.Name() == compilerRuntime { 85 b.Run("Call", func(b *testing.B) { 86 benchmarkCall(b, rt, rtCfg, call) 87 }) 88 } 89 } 90 91 func benchmarkCompile(b *testing.B, rt Runtime, rtCfg *RuntimeConfig) { 92 for i := 0; i < b.N; i++ { 93 if err := rt.Compile(testCtx, rtCfg); err != nil { 94 b.Fatal(err) 95 } 96 if err := rt.Close(testCtx); err != nil { 97 b.Fatal(err) 98 } 99 } 100 } 101 102 func benchmarkInstantiate(b *testing.B, rt Runtime, rtCfg *RuntimeConfig) { 103 // Compile outside the benchmark loop 104 if err := rt.Compile(testCtx, rtCfg); err != nil { 105 b.Fatal(err) 106 } 107 defer rt.Close(testCtx) 108 109 b.ResetTimer() 110 for i := 0; i < b.N; i++ { 111 mod, err := rt.Instantiate(testCtx, rtCfg) 112 if err != nil { 113 b.Fatal(err) 114 } 115 err = mod.Close(testCtx) 116 if err != nil { 117 b.Fatal(err) 118 } 119 } 120 } 121 122 func benchmarkCall(b *testing.B, rt Runtime, rtCfg *RuntimeConfig, call func(Module, int) error) { 123 // Initialize outside the benchmark loop 124 if err := rt.Compile(testCtx, rtCfg); err != nil { 125 b.Fatal(err) 126 } 127 defer rt.Close(testCtx) 128 mod, err := rt.Instantiate(testCtx, rtCfg) 129 if err != nil { 130 b.Fatal(err) 131 } 132 defer mod.Close(testCtx) 133 b.ResetTimer() 134 for i := 0; i < b.N; i++ { 135 if err := call(mod, i); err != nil { 136 b.Fatal(err) 137 } 138 } 139 } 140 141 func testCall(t *testing.T, runtime func() Runtime, rtCfg *RuntimeConfig, testCall func(*testing.T, Module, int, int)) { 142 rt := runtime() 143 err := rt.Compile(testCtx, rtCfg) 144 require.NoError(t, err) 145 defer rt.Close(testCtx) 146 147 // Ensure the module can be re-instantiated times, even if not all runtimes allow renaming. 148 for i := 0; i < 10; i++ { 149 m, err := rt.Instantiate(testCtx, rtCfg) 150 require.NoError(t, err) 151 152 // Large loop in test is only to show the function is stable (ex doesn't leak or crash on Nth use). 153 for j := 0; j < 1000; j++ { 154 testCall(t, m, i, j) 155 } 156 157 require.NoError(t, m.Close(testCtx)) 158 } 159 } 160 161 func readRelativeFile(relativePath string) []byte { 162 // We can't resolve relative paths as init() is called from each of its subdirs 163 _, source, _, _ := runtime.Caller(1) // 1 as this utility is in a different source than the caller. 164 realPath := path.Join(path.Dir(source), relativePath) 165 bytes, err := os.ReadFile(realPath) 166 if err != nil { 167 panic(err) 168 } 169 return bytes 170 }