github.com/ejcx/wazero@v1.1.0/cache_test.go (about)

     1  package wazero
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	goruntime "runtime"
    10  	"testing"
    11  
    12  	"github.com/tetratelabs/wazero/internal/platform"
    13  	"github.com/tetratelabs/wazero/internal/testing/require"
    14  	"github.com/tetratelabs/wazero/internal/wasm"
    15  )
    16  
    17  //go:embed internal/integration_test/vs/testdata/fac.wasm
    18  var facWasm []byte
    19  
    20  //go:embed internal/integration_test/vs/testdata/mem_grow.wasm
    21  var memGrowWasm []byte
    22  
    23  func TestCompilationCache(t *testing.T) {
    24  	ctx := context.Background()
    25  	// Ensures the normal Wasm module compilation cache works.
    26  	t.Run("non-host module", func(t *testing.T) {
    27  		foo, bar := getCacheSharedRuntimes(ctx, t)
    28  		cacheInst := foo.cache
    29  
    30  		// Create a different type id on the bar's store so that we can emulate that bar instantiated the module before facWasm.
    31  		_, err := bar.store.GetFunctionTypeIDs(
    32  			// Arbitrary one is fine as long as it is not used in facWasm.
    33  			[]wasm.FunctionType{{Params: []wasm.ValueType{
    34  				wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
    35  				wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
    36  				wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
    37  				wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
    38  				wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
    39  			}}})
    40  		require.NoError(t, err)
    41  
    42  		// add interpreter first, to ensure compiler support isn't order dependent
    43  		eng := foo.cache.engs[engineKindInterpreter]
    44  		if platform.CompilerSupported() {
    45  			eng = foo.cache.engs[engineKindCompiler]
    46  		}
    47  
    48  		// Try compiling.
    49  		compiled, err := foo.CompileModule(ctx, facWasm)
    50  		require.NoError(t, err)
    51  		// Also check it is actually cached.
    52  		require.Equal(t, uint32(1), eng.CompiledModuleCount())
    53  		barCompiled, err := bar.CompileModule(ctx, facWasm)
    54  		require.NoError(t, err)
    55  
    56  		// Ensures compiled modules are the same modulo type IDs, which is unique per store.
    57  		require.Equal(t, compiled.(*compiledModule).module, barCompiled.(*compiledModule).module)
    58  		require.Equal(t, compiled.(*compiledModule).closeWithModule, barCompiled.(*compiledModule).closeWithModule)
    59  		require.Equal(t, compiled.(*compiledModule).compiledEngine, barCompiled.(*compiledModule).compiledEngine)
    60  		// TypeIDs must be different as we create a different type ID on bar beforehand.
    61  		require.NotEqual(t, compiled.(*compiledModule).typeIDs, barCompiled.(*compiledModule).typeIDs)
    62  
    63  		// Two runtimes are completely separate except the compilation cache,
    64  		// therefore it should be ok to instantiate the same name module for each of them.
    65  		fooInst, err := foo.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name"))
    66  		require.NoError(t, err)
    67  		barInst, err := bar.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name"))
    68  		require.NoError(t, err)
    69  		// Two instances are not equal.
    70  		require.NotEqual(t, fooInst, barInst)
    71  
    72  		// Closing two runtimes shouldn't clear the cache as cache.Close must be explicitly called to clear the cache.
    73  		err = foo.Close(ctx)
    74  		require.NoError(t, err)
    75  		err = bar.Close(ctx)
    76  		require.NoError(t, err)
    77  		require.Equal(t, uint32(1), eng.CompiledModuleCount())
    78  
    79  		// Close the cache, and ensure the engine is closed.
    80  		err = cacheInst.Close(ctx)
    81  		require.NoError(t, err)
    82  		require.Equal(t, uint32(0), eng.CompiledModuleCount())
    83  	})
    84  
    85  	// Even when cache is configured, compiled host modules must be different as that's the way
    86  	// to provide per-runtime isolation on Go functions.
    87  	t.Run("host module", func(t *testing.T) {
    88  		foo, bar := getCacheSharedRuntimes(ctx, t)
    89  
    90  		goFn := func() (dummy uint32) { return }
    91  		fooCompiled, err := foo.NewHostModuleBuilder("env").
    92  			NewFunctionBuilder().WithFunc(goFn).Export("go_fn").
    93  			Compile(testCtx)
    94  		require.NoError(t, err)
    95  		barCompiled, err := bar.NewHostModuleBuilder("env").
    96  			NewFunctionBuilder().WithFunc(goFn).Export("go_fn").
    97  			Compile(testCtx)
    98  		require.NoError(t, err)
    99  
   100  		// Ensures they are different.
   101  		require.NotEqual(t, fooCompiled, barCompiled)
   102  	})
   103  
   104  	t.Run("memory limit should not affect caches", func(t *testing.T) {
   105  		// Creates new cache instance and pass it to the config.
   106  		c := NewCompilationCache()
   107  		config := NewRuntimeConfig().WithCompilationCache(c)
   108  
   109  		// create two different runtimes with separate memory limits
   110  		rt0 := NewRuntimeWithConfig(ctx, config)
   111  		rt1 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(2))
   112  		rt2 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(4))
   113  
   114  		// the compiled module is not equal because the memory limits are applied to the Memory instance
   115  		module0, _ := rt0.CompileModule(ctx, memGrowWasm)
   116  		module1, _ := rt1.CompileModule(ctx, memGrowWasm)
   117  		module2, _ := rt2.CompileModule(ctx, memGrowWasm)
   118  
   119  		max0, _ := module0.ExportedMemories()["memory"].Max()
   120  		max1, _ := module1.ExportedMemories()["memory"].Max()
   121  		max2, _ := module2.ExportedMemories()["memory"].Max()
   122  		require.Equal(t, uint32(5), max0)
   123  		require.Equal(t, uint32(2), max1)
   124  		require.Equal(t, uint32(4), max2)
   125  
   126  		compiledModule0 := module0.(*compiledModule)
   127  		compiledModule1 := module1.(*compiledModule)
   128  		compiledModule2 := module2.(*compiledModule)
   129  
   130  		// compare the compiled engine which contains the underlying "codes"
   131  		require.Equal(t, compiledModule0.compiledEngine, compiledModule1.compiledEngine)
   132  		require.Equal(t, compiledModule1.compiledEngine, compiledModule2.compiledEngine)
   133  	})
   134  }
   135  
   136  func getCacheSharedRuntimes(ctx context.Context, t *testing.T) (foo, bar *runtime) {
   137  	// Creates new cache instance and pass it to the config.
   138  	c := NewCompilationCache()
   139  	config := NewRuntimeConfig().WithCompilationCache(c)
   140  
   141  	_foo := NewRuntimeWithConfig(ctx, config)
   142  	_bar := NewRuntimeWithConfig(ctx, config)
   143  
   144  	var ok bool
   145  	foo, ok = _foo.(*runtime)
   146  	require.True(t, ok)
   147  	bar, ok = _bar.(*runtime)
   148  	require.True(t, ok)
   149  
   150  	// Make sure that two runtimes share the same cache instance.
   151  	require.Equal(t, foo.cache, bar.cache)
   152  	return
   153  }
   154  
   155  func TestCache_ensuresFileCache(t *testing.T) {
   156  	const version = "dev"
   157  	// We expect to create a version-specific subdirectory.
   158  	expectedSubdir := fmt.Sprintf("wazero-dev-%s-%s", goruntime.GOARCH, goruntime.GOOS)
   159  
   160  	t.Run("ok", func(t *testing.T) {
   161  		dir := t.TempDir()
   162  		c := &cache{}
   163  		err := c.ensuresFileCache(dir, version)
   164  		require.NoError(t, err)
   165  	})
   166  	t.Run("create dir", func(t *testing.T) {
   167  		tmpDir := path.Join(t.TempDir(), "1", "2", "3")
   168  		dir := path.Join(tmpDir, "foo") // Non-existent directory.
   169  
   170  		c := &cache{}
   171  		err := c.ensuresFileCache(dir, version)
   172  		require.NoError(t, err)
   173  
   174  		requireContainsDir(t, tmpDir, "foo")
   175  	})
   176  	t.Run("create relative dir", func(t *testing.T) {
   177  		tmpDir, oldwd := requireChdirToTemp(t)
   178  		defer os.Chdir(oldwd) //nolint
   179  		dir := "foo"
   180  
   181  		c := &cache{}
   182  		err := c.ensuresFileCache(dir, version)
   183  		require.NoError(t, err)
   184  
   185  		requireContainsDir(t, tmpDir, dir)
   186  	})
   187  	t.Run("basedir is not a dir", func(t *testing.T) {
   188  		f, err := os.CreateTemp(t.TempDir(), "nondir")
   189  		require.NoError(t, err)
   190  		defer f.Close()
   191  
   192  		c := &cache{}
   193  		err = c.ensuresFileCache(f.Name(), version)
   194  		require.Contains(t, err.Error(), "is not dir")
   195  	})
   196  	t.Run("versiondir is not a dir", func(t *testing.T) {
   197  		dir := t.TempDir()
   198  		require.NoError(t, os.WriteFile(path.Join(dir, expectedSubdir), []byte{}, 0o600))
   199  		c := &cache{}
   200  		err := c.ensuresFileCache(dir, version)
   201  		require.Contains(t, err.Error(), "is not dir")
   202  	})
   203  }
   204  
   205  // requireContainsDir ensures the directory was created in the correct path,
   206  // as file.Abs can return slightly different answers for a temp directory. For
   207  // example, /var/folders/... vs /private/var/folders/...
   208  func requireContainsDir(t *testing.T, parent, dir string) {
   209  	entries, err := os.ReadDir(parent)
   210  	require.NoError(t, err)
   211  	require.Equal(t, 1, len(entries))
   212  	require.Equal(t, dir, entries[0].Name())
   213  	require.True(t, entries[0].IsDir())
   214  }
   215  
   216  func requireChdirToTemp(t *testing.T) (string, string) {
   217  	tmpDir := t.TempDir()
   218  	oldwd, err := os.Getwd()
   219  	require.NoError(t, err)
   220  	require.NoError(t, os.Chdir(tmpDir))
   221  	return tmpDir, oldwd
   222  }
   223  
   224  func TestCache_Close(t *testing.T) {
   225  	t.Run("all engines", func(t *testing.T) {
   226  		c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, &mockEngine{}}}
   227  		err := c.Close(testCtx)
   228  		require.NoError(t, err)
   229  		for i := engineKind(0); i < engineKindCount; i++ {
   230  			require.True(t, c.engs[i].(*mockEngine).closed)
   231  		}
   232  	})
   233  	t.Run("only interp", func(t *testing.T) {
   234  		c := &cache{engs: [engineKindCount]wasm.Engine{nil, &mockEngine{}}}
   235  		err := c.Close(testCtx)
   236  		require.NoError(t, err)
   237  		require.True(t, c.engs[engineKindInterpreter].(*mockEngine).closed)
   238  	})
   239  	t.Run("only compiler", func(t *testing.T) {
   240  		c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, nil}}
   241  		err := c.Close(testCtx)
   242  		require.NoError(t, err)
   243  		require.True(t, c.engs[engineKindCompiler].(*mockEngine).closed)
   244  	})
   245  }