Files
wazero/cache_test.go
2023-03-15 13:45:52 +09:00

246 lines
8.8 KiB
Go

package wazero
import (
"context"
_ "embed"
"fmt"
"os"
"path"
goruntime "runtime"
"testing"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
//go:embed internal/integration_test/vs/testdata/fac.wasm
var facWasm []byte
//go:embed internal/integration_test/vs/testdata/mem_grow.wasm
var memGrowWasm []byte
func TestCompilationCache(t *testing.T) {
ctx := context.Background()
// Ensures the normal Wasm module compilation cache works.
t.Run("non-host module", func(t *testing.T) {
foo, bar := getCacheSharedRuntimes(ctx, t)
cacheInst := foo.cache
// Create a different type id on the bar's store so that we can emulate that bar instantiated the module before facWasm.
_, err := bar.store.GetFunctionTypeIDs(
// Arbitrary one is fine as long as it is not used in facWasm.
[]wasm.FunctionType{{Params: []wasm.ValueType{
wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
}}})
require.NoError(t, err)
// add interpreter first, to ensure compiler support isn't order dependent
eng := foo.cache.engs[engineKindInterpreter]
if platform.CompilerSupported() {
eng = foo.cache.engs[engineKindCompiler]
}
// Try compiling.
compiled, err := foo.CompileModule(ctx, facWasm)
require.NoError(t, err)
// Also check it is actually cached.
require.Equal(t, uint32(1), eng.CompiledModuleCount())
barCompiled, err := bar.CompileModule(ctx, facWasm)
require.NoError(t, err)
// Ensures compiled modules are the same modulo type IDs, which is unique per store.
require.Equal(t, compiled.(*compiledModule).module, barCompiled.(*compiledModule).module)
require.Equal(t, compiled.(*compiledModule).closeWithModule, barCompiled.(*compiledModule).closeWithModule)
require.Equal(t, compiled.(*compiledModule).compiledEngine, barCompiled.(*compiledModule).compiledEngine)
// TypeIDs must be different as we create a different type ID on bar beforehand.
require.NotEqual(t, compiled.(*compiledModule).typeIDs, barCompiled.(*compiledModule).typeIDs)
// Two runtimes are completely separate except the compilation cache,
// therefore it should be ok to instantiate the same name module for each of them.
fooInst, err := foo.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name"))
require.NoError(t, err)
barInst, err := bar.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name"))
require.NoError(t, err)
// Two instances are not equal.
require.NotEqual(t, fooInst, barInst)
// Closing two runtimes shouldn't clear the cache as cache.Close must be explicitly called to clear the cache.
err = foo.Close(ctx)
require.NoError(t, err)
err = bar.Close(ctx)
require.NoError(t, err)
require.Equal(t, uint32(1), eng.CompiledModuleCount())
// Close the cache, and ensure the engine is closed.
err = cacheInst.Close(ctx)
require.NoError(t, err)
require.Equal(t, uint32(0), eng.CompiledModuleCount())
})
// Even when cache is configured, compiled host modules must be different as that's the way
// to provide per-runtime isolation on Go functions.
t.Run("host module", func(t *testing.T) {
foo, bar := getCacheSharedRuntimes(ctx, t)
goFn := func() (dummy uint32) { return }
fooCompiled, err := foo.NewHostModuleBuilder("env").
NewFunctionBuilder().WithFunc(goFn).Export("go_fn").
Compile(testCtx)
require.NoError(t, err)
barCompiled, err := bar.NewHostModuleBuilder("env").
NewFunctionBuilder().WithFunc(goFn).Export("go_fn").
Compile(testCtx)
require.NoError(t, err)
// Ensures they are different.
require.NotEqual(t, fooCompiled, barCompiled)
})
t.Run("memory limit should not affect caches", func(t *testing.T) {
// Creates new cache instance and pass it to the config.
c := NewCompilationCache()
config := NewRuntimeConfig().WithCompilationCache(c)
// create two different runtimes with separate memory limits
rt0 := NewRuntimeWithConfig(ctx, config)
rt1 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(2))
rt2 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(4))
// the compiled module is not equal because the memory limits are applied to the Memory instance
module0, _ := rt0.CompileModule(ctx, memGrowWasm)
module1, _ := rt1.CompileModule(ctx, memGrowWasm)
module2, _ := rt2.CompileModule(ctx, memGrowWasm)
max0, _ := module0.ExportedMemories()["memory"].Max()
max1, _ := module1.ExportedMemories()["memory"].Max()
max2, _ := module2.ExportedMemories()["memory"].Max()
require.Equal(t, uint32(5), max0)
require.Equal(t, uint32(2), max1)
require.Equal(t, uint32(4), max2)
compiledModule0 := module0.(*compiledModule)
compiledModule1 := module1.(*compiledModule)
compiledModule2 := module2.(*compiledModule)
// compare the compiled engine which contains the underlying "codes"
require.Equal(t, compiledModule0.compiledEngine, compiledModule1.compiledEngine)
require.Equal(t, compiledModule1.compiledEngine, compiledModule2.compiledEngine)
})
}
func getCacheSharedRuntimes(ctx context.Context, t *testing.T) (foo, bar *runtime) {
// Creates new cache instance and pass it to the config.
c := NewCompilationCache()
config := NewRuntimeConfig().WithCompilationCache(c)
_foo := NewRuntimeWithConfig(ctx, config)
_bar := NewRuntimeWithConfig(ctx, config)
var ok bool
foo, ok = _foo.(*runtime)
require.True(t, ok)
bar, ok = _bar.(*runtime)
require.True(t, ok)
// Make sure that two runtimes share the same cache instance.
require.Equal(t, foo.cache, bar.cache)
return
}
func TestCache_ensuresFileCache(t *testing.T) {
const version = "dev"
// We expect to create a version-specific subdirectory.
expectedSubdir := fmt.Sprintf("wazero-dev-%s-%s", goruntime.GOARCH, goruntime.GOOS)
t.Run("ok", func(t *testing.T) {
dir := t.TempDir()
c := &cache{}
err := c.ensuresFileCache(dir, version)
require.NoError(t, err)
})
t.Run("create dir", func(t *testing.T) {
tmpDir := path.Join(t.TempDir(), "1", "2", "3")
dir := path.Join(tmpDir, "foo") // Non-existent directory.
c := &cache{}
err := c.ensuresFileCache(dir, version)
require.NoError(t, err)
requireContainsDir(t, tmpDir, "foo")
})
t.Run("create relative dir", func(t *testing.T) {
tmpDir, oldwd := requireChdirToTemp(t)
defer os.Chdir(oldwd) //nolint
dir := "foo"
c := &cache{}
err := c.ensuresFileCache(dir, version)
require.NoError(t, err)
requireContainsDir(t, tmpDir, dir)
})
t.Run("basedir is not a dir", func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "nondir")
require.NoError(t, err)
defer f.Close()
c := &cache{}
err = c.ensuresFileCache(f.Name(), version)
require.Contains(t, err.Error(), "is not dir")
})
t.Run("versiondir is not a dir", func(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(path.Join(dir, expectedSubdir), []byte{}, 0o600))
c := &cache{}
err := c.ensuresFileCache(dir, version)
require.Contains(t, err.Error(), "is not dir")
})
}
// requireContainsDir ensures the directory was created in the correct path,
// as file.Abs can return slightly different answers for a temp directory. For
// example, /var/folders/... vs /private/var/folders/...
func requireContainsDir(t *testing.T, parent, dir string) {
entries, err := os.ReadDir(parent)
require.NoError(t, err)
require.Equal(t, 1, len(entries))
require.Equal(t, dir, entries[0].Name())
require.True(t, entries[0].IsDir())
}
func requireChdirToTemp(t *testing.T) (string, string) {
tmpDir := t.TempDir()
oldwd, err := os.Getwd()
require.NoError(t, err)
require.NoError(t, os.Chdir(tmpDir))
return tmpDir, oldwd
}
func TestCache_Close(t *testing.T) {
t.Run("all engines", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, &mockEngine{}}}
err := c.Close(testCtx)
require.NoError(t, err)
for i := engineKind(0); i < engineKindCount; i++ {
require.True(t, c.engs[i].(*mockEngine).closed)
}
})
t.Run("only interp", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{nil, &mockEngine{}}}
err := c.Close(testCtx)
require.NoError(t, err)
require.True(t, c.engs[engineKindInterpreter].(*mockEngine).closed)
})
t.Run("only compiler", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, nil}}
err := c.Close(testCtx)
require.NoError(t, err)
require.True(t, c.engs[engineKindCompiler].(*mockEngine).closed)
})
}