github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/integration_test/engine/hammer_test.go (about)

     1  package adhoc
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/tetratelabs/wazero"
     9  	"github.com/tetratelabs/wazero/api"
    10  	"github.com/tetratelabs/wazero/internal/platform"
    11  	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
    12  	"github.com/tetratelabs/wazero/internal/testing/hammer"
    13  	"github.com/tetratelabs/wazero/internal/testing/require"
    14  	"github.com/tetratelabs/wazero/internal/wasm"
    15  	"github.com/tetratelabs/wazero/sys"
    16  )
    17  
    18  var hammers = map[string]testCase{
    19  	// Tests here are similar to what's described in /RATIONALE.md, but deviate as they involve blocking functions.
    20  	"close importing module while in use":                 {f: closeImportingModuleWhileInUse},
    21  	"close imported module while in use":                  {f: closeImportedModuleWhileInUse},
    22  	"concurrent compilation, instantiation and execution": {f: concurrentCompilationInstantiationExecution},
    23  }
    24  
    25  func TestEngineCompiler_hammer(t *testing.T) {
    26  	if !platform.CompilerSupported() {
    27  		t.Skip()
    28  	}
    29  	runAllTests(t, hammers, wazero.NewRuntimeConfigCompiler(), false)
    30  }
    31  
    32  func TestEngineInterpreter_hammer(t *testing.T) {
    33  	runAllTests(t, hammers, wazero.NewRuntimeConfigInterpreter(), false)
    34  }
    35  
    36  func concurrentCompilationInstantiationExecution(t *testing.T, r wazero.Runtime) {
    37  	P := 16              // max count of goroutines
    38  	if testing.Short() { // Adjust down if `-test.short`
    39  		P = 4
    40  	}
    41  
    42  	hammer.NewHammer(t, P, 50).Run(func(p, n int) {
    43  		var body []byte
    44  		for i := 0; i < p*n; i++ {
    45  			body = append(body, wasm.OpcodeLocalGet, 0, wasm.OpcodeI32Const, 1, wasm.OpcodeI32Add, wasm.OpcodeLocalSet, 0)
    46  		}
    47  		body = append(body, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd)
    48  
    49  		bin := binaryencoding.EncodeModule(&wasm.Module{
    50  			TypeSection:     []wasm.FunctionType{{Results: []wasm.ValueType{i32}}},
    51  			FunctionSection: []wasm.Index{0},
    52  			CodeSection:     []wasm.Code{{LocalTypes: []wasm.ValueType{i32}, Body: body}},
    53  			ExportSection:   []wasm.Export{{Index: 0, Type: wasm.ExternTypeFunc, Name: "f"}},
    54  		})
    55  		m, err := r.Instantiate(testCtx, bin)
    56  		require.NoError(t, err)
    57  		fn := m.ExportedFunction("f")
    58  		require.NotNil(t, fn)
    59  		res, err := fn.Call(testCtx)
    60  		require.NoError(t, err)
    61  		require.Equal(t, uint64(p*n), res[0])
    62  	}, nil)
    63  }
    64  
    65  func closeImportingModuleWhileInUse(t *testing.T, r wazero.Runtime) {
    66  	closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
    67  		// Close the importing module, despite calls being in-flight.
    68  		require.NoError(t, importing.Close(testCtx))
    69  
    70  		// Prove a module can be redefined even with in-flight calls.
    71  		binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32)
    72  		importing, err := r.Instantiate(testCtx, binary)
    73  		require.NoError(t, err)
    74  		return imported, importing
    75  	})
    76  }
    77  
    78  func closeImportedModuleWhileInUse(t *testing.T, r wazero.Runtime) {
    79  	closeModuleWhileInUse(t, r, func(imported, importing api.Module) (api.Module, api.Module) {
    80  		// Close the importing and imported module, despite calls being in-flight.
    81  		require.NoError(t, importing.Close(testCtx))
    82  		require.NoError(t, imported.Close(testCtx))
    83  
    84  		// Redefine the imported module, with a function that no longer blocks.
    85  		imported, err := r.NewHostModuleBuilder(imported.Name()).
    86  			NewFunctionBuilder().
    87  			WithFunc(func(ctx context.Context, x uint32) uint32 {
    88  				return x
    89  			}).
    90  			Export("return_input").
    91  			Instantiate(testCtx)
    92  		require.NoError(t, err)
    93  
    94  		// Redefine the importing module, which should link to the redefined host module.
    95  		binary := callReturnImportWasm(t, imported.Name(), importing.Name(), i32)
    96  		importing, err = r.Instantiate(testCtx, binary)
    97  		require.NoError(t, err)
    98  
    99  		return imported, importing
   100  	})
   101  }
   102  
   103  func closeModuleWhileInUse(t *testing.T, r wazero.Runtime, closeFn func(imported, importing api.Module) (api.Module, api.Module)) {
   104  	P := 8               // max count of goroutines
   105  	if testing.Short() { // Adjust down if `-test.short`
   106  		P = 4
   107  	}
   108  
   109  	// To know return path works on a closed module, we need to block calls.
   110  	var calls sync.WaitGroup
   111  	calls.Add(P)
   112  	blockAndReturn := func(ctx context.Context, x uint32) uint32 {
   113  		calls.Wait()
   114  		return x
   115  	}
   116  
   117  	// Create the host module, which exports the blocking function.
   118  	imported, err := r.NewHostModuleBuilder(t.Name() + "-imported").
   119  		NewFunctionBuilder().WithFunc(blockAndReturn).Export("return_input").
   120  		Instantiate(testCtx)
   121  	require.NoError(t, err)
   122  	defer imported.Close(testCtx)
   123  
   124  	// Import that module.
   125  	binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32)
   126  	importing, err := r.Instantiate(testCtx, binary)
   127  	require.NoError(t, err)
   128  	defer importing.Close(testCtx)
   129  
   130  	// As this is a blocking function call, only run 1 per goroutine.
   131  	i := importing // pin the module used inside goroutines
   132  	hammer.NewHammer(t, P, 1).Run(func(p, n int) {
   133  		// In all cases, the importing module is closed, so the error should have that as its module name.
   134  		fn := i.ExportedFunction("call_return_input")
   135  		require.NotNil(t, fn)
   136  		_, err := fn.Call(testCtx, 3)
   137  		require.Equal(t, sys.NewExitError(0), err)
   138  	}, func() { // When all functions are in-flight, re-assign the modules.
   139  		imported, importing = closeFn(imported, importing)
   140  		// Unblock all the calls
   141  		calls.Add(-P)
   142  	})
   143  	// As references may have changed, ensure we close both.
   144  	defer imported.Close(testCtx)
   145  	defer importing.Close(testCtx)
   146  	if t.Failed() {
   147  		return // At least one test failed, so return now.
   148  	}
   149  
   150  	// If unloading worked properly, a new function call should route to the newly instantiated module.
   151  	fn := importing.ExportedFunction("call_return_input")
   152  	require.NotNil(t, fn)
   153  	res, err := fn.Call(testCtx, 3)
   154  	require.NoError(t, err)
   155  	require.Equal(t, uint64(3), res[0])
   156  }