github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/integration_test/engine/threads_test.go (about) 1 package adhoc 2 3 import ( 4 _ "embed" 5 "testing" 6 7 "github.com/tetratelabs/wazero" 8 "github.com/tetratelabs/wazero/api" 9 "github.com/tetratelabs/wazero/experimental" 10 "github.com/tetratelabs/wazero/internal/platform" 11 "github.com/tetratelabs/wazero/internal/testing/hammer" 12 "github.com/tetratelabs/wazero/internal/testing/require" 13 ) 14 15 // We do not currently have hammer tests for bitwise and/or operations. The tests are designed to have 16 // input that changes deterministically every iteration, which is difficult to model with these operations. 17 // This is likely why atomic and/or do not show up in the wild very often if at all. 18 var ( 19 // memory.atomic.notify, memory.atomic.wait32, memory.atomic.wait64 20 // i32.atomic.store, i32.atomic.rmw.cmpxchg 21 // i64.atomic.store, i64.atomic.rmw.cmpxchg 22 // i32.atomic.store8, i32.atomic.rmw8.cmpxchg_u 23 // i32.atomic.store16, i32.atomic.rmw16.cmpxchg_u 24 // i64.atomic.store8, i64.atomic.rmw8.cmpxchg_u 25 // i64.atomic.store16, i64.atomic.rmw16.cmpxchg_u 26 // i64.atomic.store32, i64.atomic.rmw32.cmpxchg_u 27 //go:embed testdata/threads/mutex.wasm 28 mutexWasm []byte 29 30 // i32.atomic.rmw.add, i64.atomic.rmw.add, i32.atomic.rmw8.add_u, i32.atomic.rmw16.add_u, i64.atomic.rmw8.add_u, i64.atomic.rmw16.add_u, i64.atomic.rmw32.add_u 31 //go:embed testdata/threads/add.wasm 32 addWasm []byte 33 34 // i32.atomic.rmw.sub, i64.atomic.rmw.sub, i32.atomic.rmw8.sub_u, i32.atomic.rmw16.sub_u, i64.atomic.rmw8.sub_u, i64.atomic.rmw16.sub_u, i64.atomic.rmw32.sub_u 35 //go:embed testdata/threads/sub.wasm 36 subWasm []byte 37 38 // i32.atomic.rmw.xor, i64.atomic.rmw.xor, i32.atomic.rmw8.xor_u, i32.atomic.rmw16.xor_u, i64.atomic.rmw8.xor_u, i64.atomic.rmw16.xor_u, i64.atomic.rmw32.xor_u 39 //go:embed testdata/threads/xor.wasm 40 xorWasm []byte 41 ) 42 43 var threadTests = map[string]testCase{ 44 "increment guarded by mutex": {f: incrementGuardedByMutex}, 45 "atomic add": {f: atomicAdd}, 46 "atomic sub": {f: atomicSub}, 47 "atomic xor": {f: atomicXor}, 48 } 49 50 func TestThreadsNotEnabled(t *testing.T) { 51 r := wazero.NewRuntime(testCtx) 52 _, err := r.Instantiate(testCtx, mutexWasm) 53 require.EqualError(t, err, "section memory: shared memory requested but threads feature not enabled") 54 } 55 56 func TestThreadsCompiler_hammer(t *testing.T) { 57 if !platform.CompilerSupported() { 58 t.Skip() 59 } 60 runAllTests(t, threadTests, wazero.NewRuntimeConfigCompiler().WithCoreFeatures(api.CoreFeaturesV2|experimental.CoreFeaturesThreads), false) 61 } 62 63 func TestThreadsInterpreter_hammer(t *testing.T) { 64 runAllTests(t, threadTests, wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(api.CoreFeaturesV2|experimental.CoreFeaturesThreads), false) 65 } 66 67 func incrementGuardedByMutex(t *testing.T, r wazero.Runtime) { 68 P := 8 // max count of goroutines 69 if testing.Short() { // Adjust down if `-test.short` 70 P = 4 71 } 72 tests := []struct { 73 fn string 74 }{ 75 { 76 fn: "run32", 77 }, 78 { 79 fn: "run64", 80 }, 81 { 82 fn: "run32_8", 83 }, 84 { 85 fn: "run32_16", 86 }, 87 { 88 fn: "run64_8", 89 }, 90 { 91 fn: "run64_16", 92 }, 93 { 94 fn: "run64_32", 95 }, 96 } 97 for _, tc := range tests { 98 tt := tc 99 t.Run(tt.fn, func(t *testing.T) { 100 mod, err := r.Instantiate(testCtx, mutexWasm) 101 require.NoError(t, err) 102 103 fns := make([]api.Function, P) 104 hammer.NewHammer(t, P, 30000).Run(func(p, n int) { 105 _, err := mustGetFn(mod, tt.fn, fns, p).Call(testCtx) 106 require.NoError(t, err) 107 }, func() {}) 108 109 // Cheat that LE encoding can read both 32 and 64 bits 110 res, ok := mod.Memory().ReadUint32Le(8) 111 require.True(t, ok) 112 require.Equal(t, uint32(P*30000), res) 113 }) 114 } 115 } 116 117 func atomicAdd(t *testing.T, r wazero.Runtime) { 118 P := 8 // max count of goroutines 119 if testing.Short() { // Adjust down if `-test.short` 120 P = 4 121 } 122 tests := []struct { 123 fn string 124 exp int 125 }{ 126 { 127 fn: "run32", 128 exp: P * 30000, 129 }, 130 { 131 fn: "run64", 132 exp: P * 30000, 133 }, 134 { 135 fn: "run32_8", 136 // Overflows 137 exp: (P * 30000) % (1 << 8), 138 }, 139 { 140 fn: "run32_16", 141 // Overflows 142 exp: (P * 30000) % (1 << 16), 143 }, 144 { 145 fn: "run64_8", 146 // Overflows 147 exp: (P * 30000) % (1 << 8), 148 }, 149 { 150 fn: "run64_16", 151 // Overflows 152 exp: (P * 30000) % (1 << 16), 153 }, 154 { 155 fn: "run64_32", 156 exp: P * 30000, 157 }, 158 } 159 for _, tc := range tests { 160 tt := tc 161 t.Run(tt.fn, func(t *testing.T) { 162 mod, err := r.Instantiate(testCtx, addWasm) 163 require.NoError(t, err) 164 165 fns := make([]api.Function, P) 166 hammer.NewHammer(t, P, 30000).Run(func(p, n int) { 167 _, err := mustGetFn(mod, tt.fn, fns, p).Call(testCtx) 168 require.NoError(t, err) 169 }, func() {}) 170 171 // Cheat that LE encoding can read both 32 and 64 bits 172 res, ok := mod.Memory().ReadUint32Le(0) 173 require.True(t, ok) 174 require.Equal(t, uint32(tt.exp), res) 175 }) 176 } 177 } 178 179 func atomicSub(t *testing.T, r wazero.Runtime) { 180 P := 8 // max count of goroutines 181 if testing.Short() { // Adjust down if `-test.short` 182 P = 4 183 } 184 tests := []struct { 185 fn string 186 exp int 187 }{ 188 { 189 fn: "run32", 190 exp: -(P * 30000), 191 }, 192 { 193 fn: "run64", 194 exp: -(P * 30000), 195 }, 196 { 197 fn: "run32_8", 198 // Overflows 199 exp: (1 << 8) - ((P * 30000) % (1 << 8)), 200 }, 201 { 202 fn: "run32_16", 203 // Overflows 204 exp: (1 << 16) - ((P * 30000) % (1 << 16)), 205 }, 206 { 207 fn: "run64_8", 208 // Overflows 209 exp: (1 << 8) - ((P * 30000) % (1 << 8)), 210 }, 211 { 212 fn: "run64_16", 213 // Overflows 214 exp: (1 << 16) - ((P * 30000) % (1 << 16)), 215 }, 216 { 217 fn: "run64_32", 218 exp: -(P * 30000), 219 }, 220 } 221 for _, tc := range tests { 222 tt := tc 223 t.Run(tt.fn, func(t *testing.T) { 224 mod, err := r.Instantiate(testCtx, subWasm) 225 require.NoError(t, err) 226 227 fns := make([]api.Function, P) 228 hammer.NewHammer(t, P, 30000).Run(func(p, n int) { 229 _, err := mustGetFn(mod, tt.fn, fns, p).Call(testCtx) 230 require.NoError(t, err) 231 }, func() {}) 232 233 // Cheat that LE encoding can read both 32 and 64 bits 234 res, ok := mod.Memory().ReadUint32Le(0) 235 require.True(t, ok) 236 require.Equal(t, int32(tt.exp), int32(res)) 237 }) 238 } 239 } 240 241 func atomicXor(t *testing.T, r wazero.Runtime) { 242 P := 8 // max count of goroutines 243 if testing.Short() { // Adjust down if `-test.short` 244 P = 4 245 } 246 tests := []struct { 247 fn string 248 }{ 249 { 250 fn: "run32", 251 }, 252 { 253 fn: "run64", 254 }, 255 { 256 fn: "run32_8", 257 }, 258 { 259 fn: "run32_16", 260 }, 261 { 262 fn: "run64_8", 263 }, 264 { 265 fn: "run64_16", 266 }, 267 { 268 fn: "run64_32", 269 }, 270 } 271 for _, tc := range tests { 272 tt := tc 273 t.Run(tt.fn, func(t *testing.T) { 274 mod, err := r.Instantiate(testCtx, xorWasm) 275 require.NoError(t, err) 276 277 mod.Memory().WriteUint32Le(0, 12345) 278 279 fns := make([]api.Function, P) 280 hammer.NewHammer(t, P, 30000).Run(func(p, n int) { 281 _, err := mustGetFn(mod, tt.fn, fns, p).Call(testCtx) 282 require.NoError(t, err) 283 }, func() {}) 284 285 // Cheat that LE encoding can read both 32 and 64 bits 286 res, ok := mod.Memory().ReadUint32Le(0) 287 require.True(t, ok) 288 // Even number of iterations, the value should be unchanged. 289 require.Equal(t, uint32(12345), res) 290 }) 291 } 292 } 293 294 // mustGetFn is a helper to get a function from a module, caching the result to avoid repeated allocations. 295 // 296 // Creating ExportedFunction per invocation costs a lot here since each time the runtime allocates the execution stack, 297 // so only do it once per goroutine of the hammer. 298 func mustGetFn(m api.Module, name string, fns []api.Function, p int) api.Function { 299 if fns[p] == nil { 300 fns[p] = m.ExportedFunction(name) 301 } 302 return fns[p] 303 }