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  }