From 35500f9b856fab0eefd2c34b89229bdcd7df4a54 Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Tue, 10 Jan 2023 09:32:42 +0900 Subject: [PATCH] Introduces Cache API (#1016) This introduces the new API wazero.Cache interface which can be passed to wazero.RuntimeConfig. Users can configure this to share the underlying compilation cache across multiple wazero.Runtime. And along the way, this deletes the experimental file cache API as it's replaced by this new API. Signed-off-by: Takeshi Yoneda Co-authored-by: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> --- cache.go | 101 +++++++++++ cache_example_test.go | 59 +++++++ cache_test.go | 164 ++++++++++++++++++ cmd/wazero/wazero.go | 25 ++- config.go | 26 ++- experimental/compilation_cache.go | 78 --------- .../compilation_cache_example_test.go | 56 ------ experimental/compilation_cache_test.go | 100 ----------- imports/go/example/stars.go | 8 +- internal/engine/compiler/engine.go | 21 ++- internal/engine/compiler/engine_cache.go | 10 +- internal/engine/compiler/engine_cache_test.go | 21 +-- internal/engine/compiler/engine_test.go | 4 +- internal/engine/interpreter/interpreter.go | 8 +- .../engine/interpreter/interpreter_test.go | 2 +- .../compilationcache.go | 4 +- .../file_cache.go | 16 +- .../file_cache_test.go | 2 +- internal/gojs/compiler_test.go | 7 +- internal/integration_test/bench/bench_test.go | 5 +- .../bench/hostfunc_bench_test.go | 2 +- .../filecache/filecache_test.go | 7 +- .../integration_test/spectest/spectest.go | 5 +- .../integration_test/spectest/v1/spec_test.go | 4 +- .../integration_test/spectest/v2/spec_test.go | 4 +- internal/wasm/engine.go | 3 + internal/wasm/store_test.go | 5 + runtime.go | 27 ++- runtime_test.go | 57 ++++-- 29 files changed, 497 insertions(+), 334 deletions(-) create mode 100644 cache.go create mode 100644 cache_example_test.go create mode 100644 cache_test.go delete mode 100644 experimental/compilation_cache.go delete mode 100644 experimental/compilation_cache_example_test.go delete mode 100644 experimental/compilation_cache_test.go rename internal/{compilationcache => filecache}/compilationcache.go (96%) rename internal/{compilationcache => filecache}/file_cache.go (81%) rename internal/{compilationcache => filecache}/file_cache_test.go (99%) diff --git a/cache.go b/cache.go new file mode 100644 index 00000000..1cb91209 --- /dev/null +++ b/cache.go @@ -0,0 +1,101 @@ +package wazero + +import ( + "context" + "errors" + "fmt" + "os" + "path" + "path/filepath" + goruntime "runtime" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/filecache" + "github.com/tetratelabs/wazero/internal/version" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// CompilationCache reduces time spent compiling (Runtime.CompileModule) the same wasm module. +// +// Instances of this can be reused across multiple runtimes, if configured via RuntimeConfig. +type CompilationCache interface{ api.Closer } + +// NewCompilationCache returns a new CompilationCache to be passed to RuntimeConfig. +// This configures only in-memory cache, and doesn't persist to the file system. See wazero.NewCompilationCacheWithDir for detail. +// +// The returned CompilationCache can be used to share the in-memory compilation results across multiple instances of wazero.Runtime. +func NewCompilationCache() CompilationCache { + return &cache{} +} + +// NewCompilationCacheWithDir is like wazero.NewCompilationCache except the result also writes +// state into the directory specified by `dirname` parameter. +// +// If the dirname doesn't exist, this creates it or returns an error. +// +// Those running wazero as a CLI or frequently restarting a process using the same wasm should +// use this feature to reduce time waiting to compile the same module a second time. +// +// The contents written into dirname are wazero-version specific, meaning different versions of +// wazero will duplicate entries for the same input wasm. +// +// Note: The embedder must safeguard this directory from external changes. +func NewCompilationCacheWithDir(dirname string) (CompilationCache, error) { + c := &cache{} + err := c.ensuresFileCache(dirname, version.GetWazeroVersion()) + return c, err +} + +// cache implements Cache interface. +type cache struct { + // eng is the engine for this cache. If the cache is configured, the engine is shared across multiple instances of + // Runtime, and its lifetime is not bound to them. Instead, the engine is alive until Cache.Close is called. + eng wasm.Engine + + fileCache filecache.Cache +} + +// Close implements the same method on the Cache interface. +func (c *cache) Close(_ context.Context) (err error) { + if c.eng != nil { + err = c.eng.Close() + } + return +} + +func (c *cache) ensuresFileCache(dir string, wazeroVersion string) error { + // Resolve a potentially relative directory into an absolute one. + var err error + dir, err = filepath.Abs(dir) + if err != nil { + return err + } + + // Ensure the user-supplied directory. + if err = mkdir(dir); err != nil { + return err + } + + // Create a version-specific directory to avoid conflicts. + dirname := path.Join(dir, "wazero-"+wazeroVersion+"-"+goruntime.GOARCH+"-"+goruntime.GOOS) + if err = mkdir(dirname); err != nil { + return err + } + + c.fileCache = filecache.New(dirname) + return nil +} + +func mkdir(dirname string) error { + if st, err := os.Stat(dirname); errors.Is(err, os.ErrNotExist) { + // If the directory not found, create the cache dir. + if err = os.MkdirAll(dirname, 0o700); err != nil { + return fmt.Errorf("create directory %s: %v", dirname, err) + } + } else if err != nil { + return err + } else if !st.IsDir() { + return fmt.Errorf("%s is not dir", dirname) + } + return nil +} diff --git a/cache_example_test.go b/cache_example_test.go new file mode 100644 index 00000000..e40754cc --- /dev/null +++ b/cache_example_test.go @@ -0,0 +1,59 @@ +package wazero_test + +import ( + "context" + _ "embed" + "log" + "os" + + "github.com/tetratelabs/wazero" +) + +// This is a basic example of using the file system compilation cache via wazero.NewCompilationCacheWithDir. +// The main goal is to show how it is configured. +func Example_compileCache() { + // Prepare a cache directory. + cacheDir, err := os.MkdirTemp("", "example") + if err != nil { + log.Panicln(err) + } + defer os.RemoveAll(cacheDir) + + ctx := context.Background() + + // Create a runtime config which shares a compilation cache directory. + cache := newCompilationCacheWithDir(cacheDir) + defer cache.Close(ctx) + config := wazero.NewRuntimeConfig().WithCompilationCache(cache) + + // Using the same wazero.CompilationCache instance allows the in-memory cache sharing. + newRuntimeCompileClose(ctx, config) + newRuntimeCompileClose(ctx, config) + + // Since the above stored compiled functions to disk as well, below won't compile from scratch. + // Instead, compilation result stored in the directory is re-used. + newRuntimeCompileClose(ctx, config.WithCompilationCache(newCompilationCacheWithDir(cacheDir))) + newRuntimeCompileClose(ctx, config.WithCompilationCache(newCompilationCacheWithDir(cacheDir))) + + // Output: + // +} + +func newCompilationCacheWithDir(cacheDir string) wazero.CompilationCache { + cache, err := wazero.NewCompilationCacheWithDir(cacheDir) + if err != nil { + log.Panicln(err) + } + return cache +} + +// newRuntimeCompileDestroy creates a new wazero.Runtime, compile a binary, and then delete the runtime. +func newRuntimeCompileClose(ctx context.Context, config wazero.RuntimeConfig) { + r := wazero.NewRuntimeWithConfig(ctx, config) + defer r.Close(ctx) // This closes everything this Runtime created except the file system cache. + + _, err := r.CompileModule(ctx, addWasm) + if err != nil { + log.Panicln(err) + } +} diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 00000000..665a8c88 --- /dev/null +++ b/cache_test.go @@ -0,0 +1,164 @@ +package wazero + +import ( + "context" + _ "embed" + "fmt" + "os" + "path" + goruntime "runtime" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +//go:embed internal/integration_test/vs/testdata/fac.wasm +var facWasm []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 + + // Try compiling. + compiled, err := foo.CompileModule(ctx, facWasm) + require.NoError(t, err) + // Also check it is actually cached. + require.Equal(t, uint32(1), cacheInst.eng.CompiledModuleCount()) + barCompiled, err := bar.CompileModule(ctx, facWasm) + require.NoError(t, err) + + // Ensures compiled modules are the same. + require.Equal(t, compiled, barCompiled) + + // 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), cacheInst.eng.CompiledModuleCount()) + + // Close the cache, and ensure the engine is closed. + err = cacheInst.Close(ctx) + require.NoError(t, err) + require.Equal(t, uint32(0), cacheInst.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) + }) +} + +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 +} diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index c256ecab..abf65379 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -91,10 +91,12 @@ func doCompile(args []string, stdErr io.Writer, exit func(code int)) { exit(1) } - ctx := maybeUseCacheDir(context.Background(), cacheDir, stdErr, exit) - - ctx = maybeUseCacheDir(ctx, cacheDir, stdErr, exit) + c := wazero.NewRuntimeConfig() + if cache := maybeUseCacheDir(cacheDir, stdErr, exit); cache != nil { + c.WithCompilationCache(cache) + } + ctx := context.Background() rt := wazero.NewRuntime(ctx) defer rt.Close(ctx) @@ -200,9 +202,12 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(cod ctx := maybeHostLogging(context.Background(), hostLogging, stdErr, exit) - ctx = maybeUseCacheDir(ctx, cacheDir, stdErr, exit) + rtc := wazero.NewRuntimeConfig() + if cache := maybeUseCacheDir(cacheDir, stdErr, exit); cache != nil { + rtc.WithCompilationCache(cache) + } - rt := wazero.NewRuntime(ctx) + rt := wazero.NewRuntimeWithConfig(ctx, rtc) defer rt.Close(ctx) // Because we are running a binary directly rather than embedding in an application, @@ -325,16 +330,18 @@ func cacheDirFlag(flags *flag.FlagSet) *string { "Contents are re-used for the same version of wazero.") } -func maybeUseCacheDir(ctx context.Context, cacheDir *string, stdErr io.Writer, exit func(code int)) context.Context { +func maybeUseCacheDir(cacheDir *string, stdErr io.Writer, exit func(code int)) (cache wazero.CompilationCache) { if dir := *cacheDir; dir != "" { - if ctx, err := experimental.WithCompilationCacheDirName(ctx, dir); err != nil { + var err error + cache, err = wazero.NewCompilationCacheWithDir(dir) + if err != nil { fmt.Fprintf(stdErr, "invalid cachedir: %v\n", err) exit(1) } else { - return ctx + return } } - return ctx + return } func printUsage(stdErr io.Writer) { diff --git a/config.go b/config.go index 788372ac..88f67159 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/engine/compiler" "github.com/tetratelabs/wazero/internal/engine/interpreter" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/platform" internalsys "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/wasm" @@ -101,6 +102,21 @@ type RuntimeConfig interface { // DWARF "custom sections" that are often stripped, depending on // optimization flags passed to the compiler. WithDebugInfoEnabled(bool) RuntimeConfig + + // WithCompilationCache configures how runtime caches the compiled modules. In the default configuration, compilation results are + // only in-memory until Runtime.Close is closed, and not shareable by multiple Runtime. + // + // Below defines the shared cache across multiple instances of Runtime: + // + // // Creates the new Cache and the runtime configuration with it. + // cache := wazero.NewCompilationCache() + // defer cache.Close() + // config := wazero.NewRuntimeConfig().WithCompilationCache(c) + // + // // Creates two runtimes while sharing compilation caches. + // foo := wazero.NewRuntimeWithConfig(context.Background(), config) + // bar := wazero.NewRuntimeWithConfig(context.Background(), config) + WithCompilationCache(CompilationCache) RuntimeConfig } // NewRuntimeConfig returns a RuntimeConfig using the compiler if it is supported in this environment, @@ -115,7 +131,8 @@ type runtimeConfig struct { memoryCapacityFromMax bool isInterpreter bool dwarfDisabled bool // negative as defaults to enabled - newEngine func(context.Context, api.CoreFeatures) wasm.Engine + newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine + cache CompilationCache } // engineLessConfig helps avoid copy/pasting the wrong defaults. @@ -178,6 +195,13 @@ func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeCon return ret } +// WithCompilationCache implements RuntimeConfig.WithCompilationCache +func (c *runtimeConfig) WithCompilationCache(ca CompilationCache) RuntimeConfig { + ret := c.clone() + ret.cache = ca + return ret +} + // WithMemoryCapacityFromMax implements RuntimeConfig.WithMemoryCapacityFromMax func (c *runtimeConfig) WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig { ret := c.clone() diff --git a/experimental/compilation_cache.go b/experimental/compilation_cache.go deleted file mode 100644 index 5d7df913..00000000 --- a/experimental/compilation_cache.go +++ /dev/null @@ -1,78 +0,0 @@ -package experimental - -import ( - "context" - "errors" - "fmt" - "os" - "path" - "path/filepath" - "runtime" - - "github.com/tetratelabs/wazero/internal/compilationcache" - "github.com/tetratelabs/wazero/internal/version" -) - -// WithCompilationCacheDirName configures the destination directory of the compilation cache. -// Regardless of the usage of this, the compiled functions are cached in memory, but its lifetime is -// bound to the lifetime of wazero.Runtime or wazero.CompiledModule. -// -// If the dirname doesn't exist, this creates the directory. -// -// With the given non-empty directory, wazero persists the cache into the directory and that cache -// will be used as long as the running wazero version match the version of compilation wazero. -// -// A cache is only valid for use in one wazero.Runtime at a time. Concurrent use -// of a wazero.Runtime is supported, but multiple runtimes must not share the -// same directory. -// -// Note: The embedder must safeguard this directory from external changes. -// -// Usage: -// -// ctx, _ := experimental.WithCompilationCacheDirName(context.Background(), "/home/me/.cache/wazero") -// r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler()) -func WithCompilationCacheDirName(ctx context.Context, baseDirname string) (context.Context, error) { - // Allow overriding for testing - var wazeroVersion string - if v := ctx.Value(version.WazeroVersionKey{}); v != nil { - wazeroVersion = v.(string) - } else { - wazeroVersion = version.GetWazeroVersion() - } - - // Resolve a potentially relative directory into an absolute one. - var err error - baseDirname, err = filepath.Abs(baseDirname) - if err != nil { - return nil, err - } - - // Ensure the user-supplied directory. - if err = mkdir(baseDirname); err != nil { - return nil, err - } - - // Create a version-specific directory to avoid conflicts. - dirname := path.Join(baseDirname, "wazero-"+wazeroVersion+"-"+runtime.GOARCH+"-"+runtime.GOOS) - if err = mkdir(dirname); err != nil { - return nil, err - } - - ctx = context.WithValue(ctx, compilationcache.FileCachePathKey{}, dirname) - return ctx, nil -} - -func mkdir(dirname string) error { - if st, err := os.Stat(dirname); errors.Is(err, os.ErrNotExist) { - // If the directory not found, create the cache dir. - if err = os.MkdirAll(dirname, 0o700); err != nil { - return fmt.Errorf("create directory %s: %v", dirname, err) - } - } else if err != nil { - return err - } else if !st.IsDir() { - return fmt.Errorf("%s is not dir", dirname) - } - return nil -} diff --git a/experimental/compilation_cache_example_test.go b/experimental/compilation_cache_example_test.go deleted file mode 100644 index c7752638..00000000 --- a/experimental/compilation_cache_example_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package experimental_test - -import ( - "context" - _ "embed" - "log" - "os" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/experimental" -) - -// This is a basic example of using the file system compilation cache via WithCompilationCacheDirName. -// The main goal is to show how it is configured. -func Example_withCompilationCacheDirName() { - // Prepare a cache directory. - cacheDir, err := os.MkdirTemp("", "example") - if err != nil { - log.Panicln(err) - } - defer os.RemoveAll(cacheDir) - - // Append the directory into the context for configuration. - ctx, err := experimental.WithCompilationCacheDirName(context.Background(), cacheDir) - if err != nil { - log.Panicln(err) - } - - // Repeat newRuntimeCompileClose with the same cache directory. - newRuntimeCompileClose(ctx) - // Since the above stored compiled functions to dist, below won't compile. - // Instead, code stored in the file cache is re-used. - newRuntimeCompileClose(ctx) - newRuntimeCompileClose(ctx) - - // Output: - // -} - -// fsWasm was generated by the following: -// -// cd testdata; wat2wasm --debug-names fs.wat -// -//go:embed testdata/fs.wasm -var fsWasm []byte - -// newRuntimeCompileDestroy creates a new wazero.Runtime, compile a binary, and then delete the runtime. -func newRuntimeCompileClose(ctx context.Context) { - r := wazero.NewRuntime(ctx) - defer r.Close(ctx) // This closes everything this Runtime created except the file system cache. - - _, err := r.CompileModule(ctx, fsWasm) - if err != nil { - log.Panicln(err) - } -} diff --git a/experimental/compilation_cache_test.go b/experimental/compilation_cache_test.go deleted file mode 100644 index 14f5430b..00000000 --- a/experimental/compilation_cache_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package experimental - -import ( - "context" - "fmt" - "os" - "path" - "path/filepath" - "runtime" - "testing" - - "github.com/tetratelabs/wazero/internal/compilationcache" - "github.com/tetratelabs/wazero/internal/testing/require" - "github.com/tetratelabs/wazero/internal/version" -) - -func TestWithCompilationCacheDirName(t *testing.T) { - ctx := context.WithValue(context.Background(), version.WazeroVersionKey{}, "dev") - // We expect to create a version-specific subdirectory. - expectedSubdir := fmt.Sprintf("wazero-dev-%s-%s", runtime.GOARCH, runtime.GOOS) - - t.Run("ok", func(t *testing.T) { - dir := t.TempDir() - ctx, err := WithCompilationCacheDirName(ctx, dir) - require.NoError(t, err) - actual, ok := ctx.Value(compilationcache.FileCachePathKey{}).(string) - require.True(t, ok) - require.Equal(t, path.Join(dir, expectedSubdir), actual) - - // Ensure that the sanity check file has been removed. - entries, err := os.ReadDir(actual) - require.NoError(t, err) - require.Equal(t, 0, len(entries)) - }) - t.Run("create dir", func(t *testing.T) { - tmpDir := path.Join(t.TempDir(), "1", "2", "3") - dir := path.Join(tmpDir, "foo") // Non-existent directory. - absDir, err := filepath.Abs(dir) - require.NoError(t, err) - - ctx, err := WithCompilationCacheDirName(ctx, dir) - require.NoError(t, err) - actual, ok := ctx.Value(compilationcache.FileCachePathKey{}).(string) - require.True(t, ok) - require.Equal(t, path.Join(absDir, expectedSubdir), actual) - - requireContainsDir(t, tmpDir, "foo", actual) - }) - t.Run("create relative dir", func(t *testing.T) { - tmpDir, oldwd := requireChdirToTemp(t) - defer os.Chdir(oldwd) //nolint - dir := "foo" - absDir, err := filepath.Abs(dir) - require.NoError(t, err) - - ctx, err := WithCompilationCacheDirName(ctx, dir) - require.NoError(t, err) - actual, ok := ctx.Value(compilationcache.FileCachePathKey{}).(string) - require.True(t, ok) - require.Equal(t, path.Join(absDir, expectedSubdir), actual) - - requireContainsDir(t, tmpDir, dir, actual) - }) - 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() - - _, err = WithCompilationCacheDirName(ctx, f.Name()) - 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)) - - _, err := WithCompilationCacheDirName(ctx, dir) - 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, actual string) { - require.True(t, filepath.IsAbs(actual)) - - 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 -} diff --git a/imports/go/example/stars.go b/imports/go/example/stars.go index 865f7be7..d6a78eb9 100644 --- a/imports/go/example/stars.go +++ b/imports/go/example/stars.go @@ -12,7 +12,6 @@ import ( "time" "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/experimental" gojs "github.com/tetratelabs/wazero/imports/go" "github.com/tetratelabs/wazero/sys" ) @@ -25,13 +24,16 @@ func main() { // The Wasm binary (stars/main.wasm) is very large (>7.5MB). Use wazero's // compilation cache to reduce performance penalty of multiple runs. compilationCacheDir := ".build" - ctx, err := experimental.WithCompilationCacheDirName(context.Background(), compilationCacheDir) + + cache, err := wazero.NewCompilationCacheWithDir(compilationCacheDir) if err != nil { log.Panicln(err) } + ctx := context.Background() + // Create a new WebAssembly Runtime. - r := wazero.NewRuntime(ctx) + r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) defer r.Close(ctx) // This closes everything this Runtime created. // Add the host functions used by `GOARCH=wasm GOOS=js` diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go index f3bc1e17..95539fe1 100644 --- a/internal/engine/compiler/engine.go +++ b/internal/engine/compiler/engine.go @@ -13,7 +13,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/asm" - "github.com/tetratelabs/wazero/internal/compilationcache" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/version" "github.com/tetratelabs/wazero/internal/wasm" @@ -30,7 +30,7 @@ type ( engine struct { enabledFeatures api.CoreFeatures codes map[wasm.ModuleID][]*code // guarded by mutex. - Cache compilationcache.Cache + fileCache filecache.Cache mux sync.RWMutex // setFinalizer defaults to runtime.SetFinalizer, but overridable for tests. setFinalizer func(obj interface{}, finalizer interface{}) @@ -482,6 +482,15 @@ func (e *engine) DeleteCompiledModule(module *wasm.Module) { e.deleteCodes(module) } +// Close implements the same method as documented on wasm.Engine. +func (e *engine) Close() (err error) { + e.mux.Lock() + defer e.mux.Unlock() + // Releasing the references to compiled codes including the memory-mapped machine codes. + e.codes = nil + return +} + // CompileModule implements the same method as documented on wasm.Engine. func (e *engine) CompileModule(ctx context.Context, module *wasm.Module, listeners []experimental.FunctionListener) error { if _, ok, err := e.getCodes(module); ok { // cache hit! @@ -795,11 +804,11 @@ func (f *function) getSourceOffsetInWasmBinary(pc uint64) uint64 { } } -func NewEngine(ctx context.Context, enabledFeatures api.CoreFeatures) wasm.Engine { - return newEngine(ctx, enabledFeatures) +func NewEngine(ctx context.Context, enabledFeatures api.CoreFeatures, fileCache filecache.Cache) wasm.Engine { + return newEngine(ctx, enabledFeatures, fileCache) } -func newEngine(ctx context.Context, enabledFeatures api.CoreFeatures) *engine { +func newEngine(ctx context.Context, enabledFeatures api.CoreFeatures, fileCache filecache.Cache) *engine { var wazeroVersion string if v := ctx.Value(version.WazeroVersionKey{}); v != nil { wazeroVersion = v.(string) @@ -808,7 +817,7 @@ func newEngine(ctx context.Context, enabledFeatures api.CoreFeatures) *engine { enabledFeatures: enabledFeatures, codes: map[wasm.ModuleID][]*code{}, setFinalizer: runtime.SetFinalizer, - Cache: compilationcache.NewFileCache(ctx), + fileCache: fileCache, wazeroVersion: wazeroVersion, } } diff --git a/internal/engine/compiler/engine_cache.go b/internal/engine/compiler/engine_cache.go index 309010fe..9540dc27 100644 --- a/internal/engine/compiler/engine_cache.go +++ b/internal/engine/compiler/engine_cache.go @@ -55,21 +55,21 @@ func (e *engine) getCodesFromMemory(module *wasm.Module) (codes []*code, ok bool } func (e *engine) addCodesToCache(module *wasm.Module, codes []*code) (err error) { - if e.Cache == nil || module.IsHostModule { + if e.fileCache == nil || module.IsHostModule { return } - err = e.Cache.Add(module.ID, serializeCodes(e.wazeroVersion, codes)) + err = e.fileCache.Add(module.ID, serializeCodes(e.wazeroVersion, codes)) return } func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool, err error) { - if e.Cache == nil || module.IsHostModule { + if e.fileCache == nil || module.IsHostModule { return } // Check if the entries exist in the external cache. var cached io.ReadCloser - cached, hit, err = e.Cache.Get(module.ID) + cached, hit, err = e.fileCache.Get(module.ID) if !hit || err != nil { return } @@ -83,7 +83,7 @@ func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool hit = false return } else if staleCache { - return nil, false, e.Cache.Delete(module.ID) + return nil, false, e.fileCache.Delete(module.ID) } for i, c := range codes { diff --git a/internal/engine/compiler/engine_cache_test.go b/internal/engine/compiler/engine_cache_test.go index 47290f00..44e09562 100644 --- a/internal/engine/compiler/engine_cache_test.go +++ b/internal/engine/compiler/engine_cache_test.go @@ -2,7 +2,6 @@ package compiler import ( "bytes" - "context" "crypto/sha256" "encoding/binary" "errors" @@ -11,7 +10,7 @@ import ( "testing" "testing/iotest" - "github.com/tetratelabs/wazero/internal/compilationcache" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/u32" "github.com/tetratelabs/wazero/internal/u64" @@ -293,11 +292,9 @@ func TestEngine_getCodesFromCache(t *testing.T) { e := engine{} if tc.ext != nil { tmp := t.TempDir() - e.Cache = compilationcache.NewFileCache( - context.WithValue(context.Background(), compilationcache.FileCachePathKey{}, tmp), - ) + e.fileCache = filecache.New(tmp) for key, value := range tc.ext { - err := e.Cache.Add(key, bytes.NewReader(value)) + err := e.fileCache.Add(key, bytes.NewReader(value)) require.NoError(t, err) } } @@ -313,7 +310,7 @@ func TestEngine_getCodesFromCache(t *testing.T) { require.Equal(t, tc.expCodes, codes) if tc.ext != nil && tc.expDeleted { - _, hit, err := e.Cache.Get(tc.key) + _, hit, err := e.fileCache.Get(tc.key) require.NoError(t, err) require.False(t, hit) } @@ -328,9 +325,8 @@ func TestEngine_addCodesToCache(t *testing.T) { require.NoError(t, err) }) t.Run("host module", func(t *testing.T) { - tc := compilationcache.NewFileCache(context.WithValue(context.Background(), - compilationcache.FileCachePathKey{}, t.TempDir())) - e := engine{Cache: tc} + tc := filecache.New(t.TempDir()) + e := engine{fileCache: tc} codes := []*code{{stackPointerCeil: 123, codeSegment: []byte{1, 2, 3}}} m := &wasm.Module{ID: sha256.Sum256(nil), IsHostModule: true} // Host module! err := e.addCodesToCache(m, codes) @@ -341,9 +337,8 @@ func TestEngine_addCodesToCache(t *testing.T) { require.False(t, hit) }) t.Run("add", func(t *testing.T) { - tc := compilationcache.NewFileCache(context.WithValue(context.Background(), - compilationcache.FileCachePathKey{}, t.TempDir())) - e := engine{Cache: tc} + tc := filecache.New(t.TempDir()) + e := engine{fileCache: tc} m := &wasm.Module{} codes := []*code{{stackPointerCeil: 123, codeSegment: []byte{1, 2, 3}}} err := e.addCodesToCache(m, codes) diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index d9499343..a1ea8045 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -43,7 +43,7 @@ func (e *engineTester) ListenerFactory() experimental.FunctionListenerFactory { // NewEngine implements the same method as documented on enginetest.EngineTester. func (e *engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine { - return newEngine(context.Background(), enabledFeatures) + return newEngine(context.Background(), enabledFeatures, nil) } // CompiledFunctionPointerValue implements the same method as documented on enginetest.EngineTester. @@ -237,7 +237,7 @@ func TestCompiler_Releasecode_Panic(t *testing.T) { // See comments on initialStackSize and initialCallFrameStackSize. func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { enabledFeatures := api.CoreFeaturesV1 - e := newEngine(context.Background(), enabledFeatures) + e := newEngine(context.Background(), enabledFeatures, nil) s, ns := wasm.NewStore(enabledFeatures, e) const hostModuleName = "env" diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index 63da3a2d..afb305ff 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -12,6 +12,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/moremath" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasmdebug" @@ -32,13 +33,18 @@ type engine struct { mux sync.RWMutex } -func NewEngine(_ context.Context, enabledFeatures api.CoreFeatures) wasm.Engine { +func NewEngine(_ context.Context, enabledFeatures api.CoreFeatures, _ filecache.Cache) wasm.Engine { return &engine{ enabledFeatures: enabledFeatures, codes: map[wasm.ModuleID][]*code{}, } } +// Close implements the same method as documented on wasm.Engine. +func (e *engine) Close() (err error) { + return +} + // CompiledModuleCount implements the same method as documented on wasm.Engine. func (e *engine) CompiledModuleCount() uint32 { return uint32(len(e.codes)) diff --git a/internal/engine/interpreter/interpreter_test.go b/internal/engine/interpreter/interpreter_test.go index 4689c362..1863fb1a 100644 --- a/internal/engine/interpreter/interpreter_test.go +++ b/internal/engine/interpreter/interpreter_test.go @@ -86,7 +86,7 @@ func (e engineTester) ListenerFactory() experimental.FunctionListenerFactory { // NewEngine implements enginetest.EngineTester NewEngine. func (e engineTester) NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine { - return NewEngine(context.Background(), enabledFeatures) + return NewEngine(context.Background(), enabledFeatures, nil) } // CompiledFunctionPointerValue implements enginetest.EngineTester CompiledFunctionPointerValue. diff --git a/internal/compilationcache/compilationcache.go b/internal/filecache/compilationcache.go similarity index 96% rename from internal/compilationcache/compilationcache.go rename to internal/filecache/compilationcache.go index 54954d57..b2dbd465 100644 --- a/internal/compilationcache/compilationcache.go +++ b/internal/filecache/compilationcache.go @@ -1,4 +1,4 @@ -package compilationcache +package filecache import ( "crypto/sha256" @@ -13,7 +13,7 @@ import ( // its cache once closed. This cache allows the runtime to rebuild its // in-memory cache quicker, significantly reducing first-hit penalty on a hit. // -// See NewFileCache for the example implementation. +// See New for the example implementation. type Cache interface { // Get is called when the runtime is trying to get the cached compiled functions. // Implementations are supposed to return compiled function in io.Reader with ok=true diff --git a/internal/compilationcache/file_cache.go b/internal/filecache/file_cache.go similarity index 81% rename from internal/compilationcache/file_cache.go rename to internal/filecache/file_cache.go index fc7e0116..a3030588 100644 --- a/internal/compilationcache/file_cache.go +++ b/internal/filecache/file_cache.go @@ -1,7 +1,6 @@ -package compilationcache +package filecache import ( - "context" "encoding/hex" "errors" "io" @@ -10,16 +9,9 @@ import ( "sync" ) -// FileCachePathKey is a context.Context Value key. Its value is a string -// representing the compilation cache directory. -type FileCachePathKey struct{} - -// NewFileCache returns a new Cache implemented by fileCache. -func NewFileCache(ctx context.Context) Cache { - if fsValue := ctx.Value(FileCachePathKey{}); fsValue != nil { - return newFileCache(fsValue.(string)) - } - return nil +// New returns a new Cache implemented by fileCache. +func New(dir string) Cache { + return newFileCache(dir) } func newFileCache(dir string) *fileCache { diff --git a/internal/compilationcache/file_cache_test.go b/internal/filecache/file_cache_test.go similarity index 99% rename from internal/compilationcache/file_cache_test.go rename to internal/filecache/file_cache_test.go index d147b1cb..bf2b0617 100644 --- a/internal/compilationcache/file_cache_test.go +++ b/internal/filecache/file_cache_test.go @@ -1,6 +1,6 @@ //go:build go1.18 -package compilationcache +package filecache import ( "bytes" diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go index 7431a401..d6092fd6 100644 --- a/internal/gojs/compiler_test.go +++ b/internal/gojs/compiler_test.go @@ -16,7 +16,6 @@ import ( "time" "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/experimental" gojs "github.com/tetratelabs/wazero/imports/go" "github.com/tetratelabs/wazero/internal/fstest" internalgojs "github.com/tetratelabs/wazero/internal/gojs" @@ -63,7 +62,7 @@ var testBin []byte // testCtx is configured in TestMain to re-use wazero's compilation cache. var ( - testCtx context.Context + testCtx = context.Background() testFS = fstest.FS rt wazero.Runtime ) @@ -92,14 +91,14 @@ func TestMain(m *testing.M) { log.Panicln(err) } defer os.RemoveAll(compilationCacheDir) - testCtx, err = experimental.WithCompilationCacheDirName(context.Background(), compilationCacheDir) + cache, err := wazero.NewCompilationCacheWithDir(compilationCacheDir) if err != nil { log.Panicln(err) } // Seed wazero's compilation cache to see any error up-front and to prevent // one test from a cache-miss performance penalty. - rt = wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig()) + rt = wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig().WithCompilationCache(cache)) _, err = rt.CompileModule(testCtx, testBin) if err != nil { log.Panicln(err) diff --git a/internal/integration_test/bench/bench_test.go b/internal/integration_test/bench/bench_test.go index ccdc7f82..ddff41b5 100644 --- a/internal/integration_test/bench/bench_test.go +++ b/internal/integration_test/bench/bench_test.go @@ -12,7 +12,6 @@ import ( "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/internal/platform" ) @@ -72,12 +71,12 @@ func BenchmarkCompilation(b *testing.B) { // Note: recreate runtime each time in the loop to ensure that // recompilation happens if the extern cache is not used. b.Run("with extern cache", func(b *testing.B) { - ctx, err := experimental.WithCompilationCacheDirName(context.Background(), b.TempDir()) + cache, err := wazero.NewCompilationCacheWithDir(b.TempDir()) if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { - r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler()) + r := wazero.NewRuntimeWithConfig(context.Background(), wazero.NewRuntimeConfigCompiler().WithCompilationCache(cache)) runCompilation(b, r) } }) diff --git a/internal/integration_test/bench/hostfunc_bench_test.go b/internal/integration_test/bench/hostfunc_bench_test.go index 3e5acf80..c1d097af 100644 --- a/internal/integration_test/bench/hostfunc_bench_test.go +++ b/internal/integration_test/bench/hostfunc_bench_test.go @@ -134,7 +134,7 @@ func getCallEngine(m *wasm.ModuleInstance, name string) (ce wasm.CallEngine, err } func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance { - eng := compiler.NewEngine(context.Background(), api.CoreFeaturesV2) + eng := compiler.NewEngine(context.Background(), api.CoreFeaturesV2, nil) ft := &wasm.FunctionType{ Params: []wasm.ValueType{wasm.ValueTypeI32}, diff --git a/internal/integration_test/filecache/filecache_test.go b/internal/integration_test/filecache/filecache_test.go index 591cfb96..1fb61fd1 100644 --- a/internal/integration_test/filecache/filecache_test.go +++ b/internal/integration_test/filecache/filecache_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/engine/compiler" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/integration_test/spectest" v1 "github.com/tetratelabs/wazero/internal/integration_test/spectest/v1" "github.com/tetratelabs/wazero/internal/platform" @@ -60,8 +60,7 @@ func TestSpecTestCompilerCache(t *testing.T) { require.True(t, len(files) > 0) } else { // Run the spectest with the file cache. - ctx, err := experimental.WithCompilationCacheDirName(context.Background(), cacheDir) - require.NoError(t, err) - spectest.Run(t, v1.Testcases, ctx, compiler.NewEngine, v1.EnabledFeatures) + fc := filecache.New(cacheDir) + spectest.Run(t, v1.Testcases, context.Background(), fc, compiler.NewEngine, v1.EnabledFeatures) } } diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 19115489..3a9ae3e3 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/moremath" "github.com/tetratelabs/wazero/internal/sys" "github.com/tetratelabs/wazero/internal/testing/require" @@ -349,7 +350,7 @@ func maybeSetMemoryCap(mod *wasm.Module) { // Run runs all the test inside the testDataFS file system where all the cases are described // via JSON files created from wast2json. -func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(context.Context, api.CoreFeatures) wasm.Engine, enabledFeatures api.CoreFeatures) { +func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, fc filecache.Cache, newEngine func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine, enabledFeatures api.CoreFeatures) { files, err := testDataFS.ReadDir("testdata") require.NoError(t, err) @@ -375,7 +376,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func( wastName := basename(base.SourceFile) t.Run(wastName, func(t *testing.T) { - s, ns := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures)) + s, ns := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures, fc)) addSpectestModule(t, ctx, s, ns, enabledFeatures) var lastInstantiatedModuleName string diff --git a/internal/integration_test/spectest/v1/spec_test.go b/internal/integration_test/spectest/v1/spec_test.go index 52e926ce..b668755b 100644 --- a/internal/integration_test/spectest/v1/spec_test.go +++ b/internal/integration_test/spectest/v1/spec_test.go @@ -14,9 +14,9 @@ func TestCompiler(t *testing.T) { if !platform.CompilerSupported() { t.Skip() } - spectest.Run(t, Testcases, context.Background(), compiler.NewEngine, EnabledFeatures) + spectest.Run(t, Testcases, context.Background(), nil, compiler.NewEngine, EnabledFeatures) } func TestInterpreter(t *testing.T) { - spectest.Run(t, Testcases, context.Background(), interpreter.NewEngine, EnabledFeatures) + spectest.Run(t, Testcases, context.Background(), nil, interpreter.NewEngine, EnabledFeatures) } diff --git a/internal/integration_test/spectest/v2/spec_test.go b/internal/integration_test/spectest/v2/spec_test.go index 13719175..0c3bd70b 100644 --- a/internal/integration_test/spectest/v2/spec_test.go +++ b/internal/integration_test/spectest/v2/spec_test.go @@ -22,9 +22,9 @@ func TestCompiler(t *testing.T) { if !platform.CompilerSupported() { t.Skip() } - spectest.Run(t, testcases, context.Background(), compiler.NewEngine, enabledFeatures) + spectest.Run(t, testcases, context.Background(), nil, compiler.NewEngine, enabledFeatures) } func TestInterpreter(t *testing.T) { - spectest.Run(t, testcases, context.Background(), interpreter.NewEngine, enabledFeatures) + spectest.Run(t, testcases, context.Background(), nil, interpreter.NewEngine, enabledFeatures) } diff --git a/internal/wasm/engine.go b/internal/wasm/engine.go index 01f137e3..6fcc3efa 100644 --- a/internal/wasm/engine.go +++ b/internal/wasm/engine.go @@ -9,6 +9,9 @@ import ( // Engine is a Store-scoped mechanism to compile functions declared or imported by a module. // This is a top-level type implemented by an interpreter or compiler. type Engine interface { + // Close closes this engine, and releases all the compiled cache. + Close() (err error) + // CompileModule implements the same method as documented on wasm.Engine. CompileModule(ctx context.Context, module *Module, listeners []experimental.FunctionListener) error diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 95017b5f..ce09008b 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -349,6 +349,11 @@ func newStore() (*Store, *Namespace) { return NewStore(api.CoreFeaturesV1, &mockEngine{shouldCompileFail: false, callFailIndex: -1}) } +// CompileModule implements the same method as documented on wasm.Engine. +func (e *mockEngine) Close() error { + return nil +} + // CompileModule implements the same method as documented on wasm.Engine. func (e *mockEngine) CompileModule(context.Context, *Module, []experimental.FunctionListener) error { return nil diff --git a/runtime.go b/runtime.go index 3270533f..d5d0c5a9 100644 --- a/runtime.go +++ b/runtime.go @@ -130,8 +130,22 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime { ctx = context.WithValue(ctx, version.WazeroVersionKey{}, wazeroVersion) } config := rConfig.(*runtimeConfig) - store, ns := wasm.NewStore(config.enabledFeatures, config.newEngine(ctx, config.enabledFeatures)) + var engine wasm.Engine + var cacheImpl *cache + if c := config.cache; c != nil { + // If the Cache is configured, we share the engine. + cacheImpl = c.(*cache) + if cacheImpl.eng == nil { + cacheImpl.eng = config.newEngine(ctx, config.enabledFeatures, cacheImpl.fileCache) + } + engine = cacheImpl.eng + } else { + // Otherwise, we create a new engine. + engine = config.newEngine(ctx, config.enabledFeatures, nil) + } + store, ns := wasm.NewStore(config.enabledFeatures, engine) return &runtime{ + cache: cacheImpl, store: store, ns: &namespace{store: store, ns: ns}, enabledFeatures: config.enabledFeatures, @@ -145,13 +159,13 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime { // runtime allows decoupling of public interfaces from internal representation. type runtime struct { store *wasm.Store + cache *cache ns *namespace enabledFeatures api.CoreFeatures memoryLimitPages uint32 memoryCapacityFromMax bool isInterpreter bool dwarfDisabled bool - compiledModules []*compiledModule } // NewNamespace implements Runtime.NewNamespace. @@ -200,8 +214,6 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod if err = r.store.Engine.CompileModule(ctx, internal, listeners); err != nil { return nil, err } - - r.compiledModules = append(r.compiledModules, c) return c, nil } @@ -247,9 +259,10 @@ func (r *runtime) Close(ctx context.Context) error { // CloseWithExitCode implements Runtime.CloseWithExitCode func (r *runtime) CloseWithExitCode(ctx context.Context, exitCode uint32) error { err := r.store.CloseWithExitCode(ctx, exitCode) - for _, c := range r.compiledModules { - if e := c.Close(ctx); e != nil && err == nil { - err = e + if r.cache == nil { + // Close the engine if the cache is not configured, which means that this engine is scoped in this runtime. + if errCloseEngine := r.store.Engine.Close(); errCloseEngine != nil { + return errCloseEngine } } return err diff --git a/runtime_test.go b/runtime_test.go index 8c749bb4..873d306f 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -9,6 +9,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/filecache" "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/version" @@ -41,12 +42,12 @@ func (h *HostContext) Value(key interface{}) interface{} { return nil } func TestNewRuntimeWithConfig_version(t *testing.T) { cfg := NewRuntimeConfig().(*runtimeConfig) oldNewEngine := cfg.newEngine - cfg.newEngine = func(ctx context.Context, features api.CoreFeatures) wasm.Engine { + cfg.newEngine = func(ctx context.Context, features api.CoreFeatures, _ filecache.Cache) wasm.Engine { // Ensures that wazeroVersion is propagated to the engine. v := ctx.Value(version.WazeroVersionKey{}) require.NotNil(t, v) require.Equal(t, wazeroVersion, v.(string)) - return oldNewEngine(ctx, features) + return oldNewEngine(ctx, features, nil) } _ = NewRuntimeWithConfig(testCtx, cfg) } @@ -631,29 +632,41 @@ func TestHostFunctionWithCustomContext(t *testing.T) { } func TestRuntime_Close_ClosesCompiledModules(t *testing.T) { - engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}} - conf := *engineLessConfig - conf.newEngine = func(context.Context, api.CoreFeatures) wasm.Engine { - return engine + for _, tc := range []struct { + name string + withCompilationCache bool + }{ + {name: "with cache", withCompilationCache: true}, + {name: "without cache", withCompilationCache: false}, + } { + t.Run(tc.name, func(t *testing.T) { + engine := &mockEngine{name: "mock", cachedModules: map[*wasm.Module]struct{}{}} + conf := *engineLessConfig + conf.newEngine = func(context.Context, api.CoreFeatures, filecache.Cache) wasm.Engine { return engine } + if tc.withCompilationCache { + conf.cache = NewCompilationCache() + } + r := NewRuntimeWithConfig(testCtx, &conf) + defer r.Close(testCtx) + + // Normally compiled modules are closed when instantiated but this is never instantiated. + _, err := r.CompileModule(testCtx, binaryNamedZero) + require.NoError(t, err) + require.Equal(t, uint32(1), engine.CompiledModuleCount()) + + err = r.Close(testCtx) + require.NoError(t, err) + + // Closing the runtime should remove the compiler cache if cache is not configured. + require.Equal(t, !tc.withCompilationCache, engine.closed) + }) } - r := NewRuntimeWithConfig(testCtx, &conf) - defer r.Close(testCtx) - - // Normally compiled modules are closed when instantiated but this is never instantiated. - _, err := r.CompileModule(testCtx, binaryNamedZero) - require.NoError(t, err) - require.Equal(t, uint32(1), engine.CompiledModuleCount()) - - err = r.Close(testCtx) - require.NoError(t, err) - - // Closing the runtime should remove the compiler cache - require.Zero(t, engine.CompiledModuleCount()) } type mockEngine struct { name string cachedModules map[*wasm.Module]struct{} + closed bool } // CompileModule implements the same method as documented on wasm.Engine. @@ -676,3 +689,9 @@ func (e *mockEngine) DeleteCompiledModule(module *wasm.Module) { func (e *mockEngine) NewModuleEngine(_ string, _ *wasm.Module, _ []wasm.FunctionInstance) (wasm.ModuleEngine, error) { return nil, nil } + +// NewModuleEngine implements the same method as documented on wasm.Close. +func (e *mockEngine) Close() (err error) { + e.closed = true + return +}