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  }