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

     1  package bench
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"encoding/binary"
     7  	"fmt"
     8  	"math"
     9  	"testing"
    10  
    11  	"wa-lang.org/wazero/api"
    12  	"wa-lang.org/wazero/internal/engine/compiler"
    13  	"wa-lang.org/wazero/internal/platform"
    14  	"wa-lang.org/wazero/internal/testing/require"
    15  	"wa-lang.org/wazero/internal/wasm"
    16  )
    17  
    18  const (
    19  	// callGoHostName is the name of exported function which calls the
    20  	// Go-implemented host function.
    21  	callGoHostName = "call_go_host"
    22  	// callGoReflectHostName is the name of exported function which calls the
    23  	// Go-implemented host function defined in reflection.
    24  	callGoReflectHostName = "call_go_reflect_host"
    25  	// callWasmHostName is the name of exported function which calls the
    26  	// Wasm-implemented host function.
    27  	callWasmHostName = "call_wasm_host"
    28  )
    29  
    30  // BenchmarkHostFunctionCall measures the cost of host function calls whose target functions are either
    31  // Go-implemented or Wasm-implemented, and compare the results between them.
    32  func BenchmarkHostFunctionCall(b *testing.B) {
    33  	if !platform.CompilerSupported() {
    34  		b.Skip()
    35  	}
    36  
    37  	m := setupHostCallBench(func(err error) {
    38  		if err != nil {
    39  			b.Fatal(err)
    40  		}
    41  	})
    42  
    43  	const offset = uint64(100)
    44  	const val = float32(1.1234)
    45  
    46  	binary.LittleEndian.PutUint32(m.Memory.Buffer[offset:], math.Float32bits(val))
    47  
    48  	for _, fn := range []string{callGoReflectHostName, callGoHostName, callWasmHostName} {
    49  		fn := fn
    50  
    51  		b.Run(fn, func(b *testing.B) {
    52  			ce, err := getCallEngine(m, fn)
    53  			if err != nil {
    54  				b.Fatal(err)
    55  			}
    56  
    57  			b.ResetTimer()
    58  			for i := 0; i < b.N; i++ {
    59  				res, err := ce.Call(testCtx, m.CallCtx, []uint64{offset})
    60  				if err != nil {
    61  					b.Fatal(err)
    62  				}
    63  				if uint32(res[0]) != math.Float32bits(val) {
    64  					b.Fail()
    65  				}
    66  			}
    67  		})
    68  	}
    69  }
    70  
    71  func TestBenchmarkFunctionCall(t *testing.T) {
    72  	if !platform.CompilerSupported() {
    73  		t.Skip()
    74  	}
    75  
    76  	m := setupHostCallBench(func(err error) {
    77  		require.NoError(t, err)
    78  	})
    79  
    80  	callWasmHost, err := getCallEngine(m, callWasmHostName)
    81  	require.NoError(t, err)
    82  
    83  	callGoHost, err := getCallEngine(m, callGoHostName)
    84  	require.NoError(t, err)
    85  
    86  	callGoReflectHost, err := getCallEngine(m, callGoReflectHostName)
    87  	require.NoError(t, err)
    88  
    89  	require.NotNil(t, callWasmHost)
    90  	require.NotNil(t, callGoHost)
    91  	require.NotNil(t, callGoReflectHost)
    92  
    93  	tests := []struct {
    94  		offset uint32
    95  		val    float32
    96  	}{
    97  		{offset: 0, val: math.Float32frombits(0xffffffff)},
    98  		{offset: 100, val: 1.12314},
    99  		{offset: wasm.MemoryPageSize - 4, val: 1.12314},
   100  	}
   101  
   102  	mem := m.Memory.Buffer
   103  
   104  	for _, f := range []struct {
   105  		name string
   106  		ce   wasm.CallEngine
   107  	}{
   108  		{name: "go", ce: callGoHost},
   109  		{name: "go-reflect", ce: callGoReflectHost},
   110  		{name: "wasm", ce: callWasmHost},
   111  	} {
   112  		f := f
   113  		t.Run(f.name, func(t *testing.T) {
   114  			for _, tc := range tests {
   115  				binary.LittleEndian.PutUint32(mem[tc.offset:], math.Float32bits(tc.val))
   116  				res, err := f.ce.Call(context.Background(), m.CallCtx, []uint64{uint64(tc.offset)})
   117  				require.NoError(t, err)
   118  				require.Equal(t, math.Float32bits(tc.val), uint32(res[0]))
   119  			}
   120  		})
   121  	}
   122  }
   123  
   124  func getCallEngine(m *wasm.ModuleInstance, name string) (ce wasm.CallEngine, err error) {
   125  	f := m.Exports[name].Function
   126  	if f == nil {
   127  		err = fmt.Errorf("%s not found", name)
   128  		return
   129  	}
   130  
   131  	ce, err = m.Engine.NewCallEngine(m.CallCtx, f)
   132  	return
   133  }
   134  
   135  func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance {
   136  	eng := compiler.NewEngine(context.Background(), api.CoreFeaturesV2)
   137  
   138  	ft := &wasm.FunctionType{
   139  		Params:           []wasm.ValueType{wasm.ValueTypeI32},
   140  		Results:          []wasm.ValueType{wasm.ValueTypeF32},
   141  		ParamNumInUint64: 1, ResultNumInUint64: 1,
   142  	}
   143  
   144  	// Build the host module.
   145  	hostModule := &wasm.Module{
   146  		TypeSection:     []*wasm.FunctionType{ft},
   147  		FunctionSection: []wasm.Index{0, 0, 0},
   148  		CodeSection: []*wasm.Code{
   149  			{
   150  				IsHostFunction: true,
   151  				GoFunc: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) {
   152  					ret, ok := mod.Memory().ReadUint32Le(ctx, uint32(stack[0]))
   153  					if !ok {
   154  						panic("couldn't read memory")
   155  					}
   156  					stack[0] = uint64(ret)
   157  				}),
   158  			},
   159  			wasm.MustParseGoReflectFuncCode(
   160  				func(ctx context.Context, m api.Module, pos uint32) float32 {
   161  					ret, ok := m.Memory().ReadUint32Le(ctx, pos)
   162  					if !ok {
   163  						panic("couldn't read memory")
   164  					}
   165  					return math.Float32frombits(ret)
   166  				},
   167  			),
   168  			{
   169  				IsHostFunction: true,
   170  				Body: []byte{
   171  					wasm.OpcodeLocalGet, 0,
   172  					wasm.OpcodeI32Load, 0x2, 0x0, // offset = 0
   173  					wasm.OpcodeF32ReinterpretI32,
   174  					wasm.OpcodeEnd,
   175  				},
   176  			},
   177  		},
   178  		ExportSection: []*wasm.Export{
   179  			{Name: "go", Type: wasm.ExternTypeFunc, Index: 0},
   180  			{Name: "go-reflect", Type: wasm.ExternTypeFunc, Index: 1},
   181  			{Name: "wasm", Type: wasm.ExternTypeFunc, Index: 2},
   182  		},
   183  		ID: wasm.ModuleID{1, 2, 3, 4, 5},
   184  	}
   185  	hostModule.BuildFunctionDefinitions()
   186  
   187  	host := &wasm.ModuleInstance{Name: "host", TypeIDs: []wasm.FunctionTypeID{0}}
   188  	host.Functions = host.BuildFunctions(hostModule)
   189  	host.BuildExports(hostModule.ExportSection)
   190  	goFn := host.Exports["go"].Function
   191  	goReflectFn := host.Exports["go-reflect"].Function
   192  	wasnFn := host.Exports["wasm"].Function
   193  
   194  	err := eng.CompileModule(testCtx, hostModule, nil)
   195  	requireNoError(err)
   196  
   197  	hostME, err := eng.NewModuleEngine(host.Name, hostModule, nil, host.Functions)
   198  	requireNoError(err)
   199  	linkModuleToEngine(host, hostME)
   200  
   201  	// Build the importing module.
   202  	importingModule := &wasm.Module{
   203  		TypeSection: []*wasm.FunctionType{ft},
   204  		ImportSection: []*wasm.Import{
   205  			// Placeholders for imports from hostModule.
   206  			{Type: wasm.ExternTypeFunc},
   207  			{Type: wasm.ExternTypeFunc},
   208  			{Type: wasm.ExternTypeFunc},
   209  		},
   210  		FunctionSection: []wasm.Index{0, 0, 0},
   211  		ExportSection: []*wasm.Export{
   212  			{Name: callGoHostName, Type: wasm.ExternTypeFunc, Index: 2},
   213  			{Name: callGoReflectHostName, Type: wasm.ExternTypeFunc, Index: 3},
   214  			{Name: callWasmHostName, Type: wasm.ExternTypeFunc, Index: 4},
   215  		},
   216  		CodeSection: []*wasm.Code{
   217  			{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Calling the index 0 = host.go.
   218  			{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Calling the index 1 = host.go-reflect.
   219  			{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 2, wasm.OpcodeEnd}}, // Calling the index 2 = host.wasm.
   220  		},
   221  		// Indicates that this module has a memory so that compilers are able to assemble memory-related initialization.
   222  		MemorySection: &wasm.Memory{Min: 1},
   223  		ID:            wasm.ModuleID{1},
   224  	}
   225  
   226  	importingModule.BuildFunctionDefinitions()
   227  	err = eng.CompileModule(testCtx, importingModule, nil)
   228  	requireNoError(err)
   229  
   230  	importing := &wasm.ModuleInstance{TypeIDs: []wasm.FunctionTypeID{0}}
   231  	importingFunctions := importing.BuildFunctions(importingModule)
   232  	importing.Functions = append([]*wasm.FunctionInstance{goFn, wasnFn}, importingFunctions...)
   233  	importing.BuildExports(importingModule.ExportSection)
   234  
   235  	importingMe, err := eng.NewModuleEngine(importing.Name, importingModule, []*wasm.FunctionInstance{goFn, goReflectFn, wasnFn}, importingFunctions)
   236  	requireNoError(err)
   237  	linkModuleToEngine(importing, importingMe)
   238  
   239  	importing.Memory = &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize), Min: 1, Cap: 1, Max: 1}
   240  	return importing
   241  }
   242  
   243  func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
   244  	module.Engine = me
   245  	module.CallCtx = wasm.NewCallContext(nil, module, nil)
   246  }