1package wazero
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path"
9 "path/filepath"
10 goruntime "runtime"
11 "sync"
12
13 "github.com/tetratelabs/wazero/api"
14 "github.com/tetratelabs/wazero/internal/filecache"
15 "github.com/tetratelabs/wazero/internal/version"
16 "github.com/tetratelabs/wazero/internal/wasm"
17)
18
19// CompilationCache reduces time spent compiling (Runtime.CompileModule) the same wasm module.
20//
21// # Notes
22//
23// - This is an interface for decoupling, not third-party implementations.
24// All implementations are in wazero.
25// - Instances of this can be reused across multiple runtimes, if configured
26// via RuntimeConfig.
27// - The cache check happens before the compilation, so if multiple Goroutines are
28// trying to compile the same module simultaneously, it is possible that they
29// all compile the module. The design here is that the lock isn't held for the action "Compile"
30// but only for checking and saving the compiled result. Therefore, we strongly recommend that the embedder
31// does the centralized compilation in a single Goroutines (or multiple Goroutines per Wasm binary) to generate cache rather than
32// trying to Compile in parallel for a single module. In other words, we always recommend to produce CompiledModule
33// share it across multiple Goroutines to avoid trying to compile the same module simultaneously.
34type CompilationCache interface{ api.Closer }
35
36// NewCompilationCache returns a new CompilationCache to be passed to RuntimeConfig.
37// This configures only in-memory cache, and doesn't persist to the file system. See wazero.NewCompilationCacheWithDir for detail.
38//
39// The returned CompilationCache can be used to share the in-memory compilation results across multiple instances of wazero.Runtime.
40func NewCompilationCache() CompilationCache {
41 return &cache{}
42}
43
44// NewCompilationCacheWithDir is like wazero.NewCompilationCache except the result also writes
45// state into the directory specified by `dirname` parameter.
46//
47// If the dirname doesn't exist, this creates it or returns an error.
48//
49// Those running wazero as a CLI or frequently restarting a process using the same wasm should
50// use this feature to reduce time waiting to compile the same module a second time.
51//
52// The contents written into dirname are wazero-version specific, meaning different versions of
53// wazero will duplicate entries for the same input wasm.
54//
55// Note: The embedder must safeguard this directory from external changes.
56func NewCompilationCacheWithDir(dirname string) (CompilationCache, error) {
57 c := &cache{}
58 err := c.ensuresFileCache(dirname, version.GetWazeroVersion())
59 return c, err
60}
61
62// cache implements Cache interface.
63type cache struct {
64 // eng is the engine for this cache. If the cache is configured, the engine is shared across multiple instances of
65 // Runtime, and its lifetime is not bound to them. Instead, the engine is alive until Cache.Close is called.
66 engs [engineKindCount]wasm.Engine
67 fileCache filecache.Cache
68 initOnces [engineKindCount]sync.Once
69}
70
71func (c *cache) initEngine(ek engineKind, ne newEngine, ctx context.Context, features api.CoreFeatures) wasm.Engine {
72 c.initOnces[ek].Do(func() { c.engs[ek] = ne(ctx, features, c.fileCache) })
73 return c.engs[ek]
74}
75
76// Close implements the same method on the Cache interface.
77func (c *cache) Close(_ context.Context) (err error) {
78 for _, eng := range c.engs {
79 if eng != nil {
80 if err = eng.Close(); err != nil {
81 return
82 }
83 }
84 }
85 return
86}
87
88func (c *cache) ensuresFileCache(dir string, wazeroVersion string) error {
89 // Resolve a potentially relative directory into an absolute one.
90 var err error
91 dir, err = filepath.Abs(dir)
92 if err != nil {
93 return err
94 }
95
96 // Ensure the user-supplied directory.
97 if err = mkdir(dir); err != nil {
98 return err
99 }
100
101 // Create a version-specific directory to avoid conflicts.
102 dirname := path.Join(dir, "wazero-"+wazeroVersion+"-"+goruntime.GOARCH+"-"+goruntime.GOOS)
103 if err = mkdir(dirname); err != nil {
104 return err
105 }
106
107 c.fileCache = filecache.New(dirname)
108 return nil
109}
110
111func mkdir(dirname string) error {
112 if st, err := os.Stat(dirname); errors.Is(err, os.ErrNotExist) {
113 // If the directory not found, create the cache dir.
114 if err = os.MkdirAll(dirname, 0o700); err != nil {
115 return fmt.Errorf("create directory %s: %v", dirname, err)
116 }
117 } else if err != nil {
118 return err
119 } else if !st.IsDir() {
120 return fmt.Errorf("%s is not dir", dirname)
121 }
122 return nil
123}