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 }