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 +}