@@ -3,8 +3,10 @@ package host
33import (
44 "bytes"
55 "context"
6+ "crypto/sha256"
67 "encoding/base64"
78 "encoding/binary"
9+ "encoding/hex"
810 "encoding/json"
911 "errors"
1012 "fmt"
@@ -16,6 +18,7 @@ import (
1618 "path/filepath"
1719 "regexp"
1820 "strings"
21+ "sync"
1922 "time"
2023
2124 "github.com/andybalholm/brotli"
@@ -128,11 +131,14 @@ type ExecutionHelper = host.ExecutionHelper
128131
129132type module struct {
130133 // The compiled module is not kept resident for the lifetime of the module
131- // instance. Instead it is compiled once, serialized to a file in moduleDir,
132- // and the in-memory *wasmtime.Module is closed. Each guest invocation
133- // (callWasm) deserializes its own *wasmtime.Module from modulePath and closes
134- // it when the invocation returns, so nothing is held while suspended.
135- moduleDir string
134+ // instance. Instead it is compiled once, serialized, and written to the
135+ // process-global content-addressed moduleFileCache (see moduleCache); the
136+ // in-memory *wasmtime.Module is then closed. modulePath points at the cache
137+ // entry, which is keyed by the SHA-256 of the serialized bytes so that
138+ // modules compiled from identical binaries share a single file. Each guest
139+ // invocation (callWasm) deserializes its own *wasmtime.Module from modulePath
140+ // and closes it when the invocation returns, so nothing is held while
141+ // suspended.
136142 modulePath string
137143
138144 cfg * ModuleConfig
@@ -152,6 +158,77 @@ type module struct {
152158// callWasmFunc runs one guest invocation for the v2 (no-DAG) execution path.
153159type callWasmFunc func (timeout time.Duration , req * sdkpb.ExecuteRequest , linkWasm linkFn [* sdkpb.ExecutionResult ], exec * execution [* sdkpb.ExecutionResult ]) (time.Duration , error )
154160
161+ // moduleFileCache is the process-global store for serialized wasmtime modules.
162+ var moduleFileCache = & moduleCache {refs : map [string ]int {}}
163+
164+ // moduleCache is a content-addressed, reference-counted store for serialized
165+ // wasmtime modules. Modules compiled from identical binaries serialize to
166+ // identical bytes, so they are written once to a shared directory under a key
167+ // derived from the SHA-256 of the serialized bytes and shared by every module
168+ // instance that references them. The reference count tracks live users of each
169+ // key so a file is removed only when the last module referencing it is closed.
170+ type moduleCache struct {
171+ mu sync.Mutex
172+ dir string
173+ refs map [string ]int
174+ }
175+
176+ // store writes serialized to the cache, keyed by the SHA-256 of its contents,
177+ // and returns the path to the cache entry. The refcount check under the mutex
178+ // is the atomic decision: only the first reference to a given key writes the
179+ // file; subsequent references reuse the existing one (dedupe). The returned path
180+ // must later be passed to release exactly once (see module.Close).
181+ func (c * moduleCache ) store (serialized []byte ) (string , error ) {
182+ sum := sha256 .Sum256 (serialized )
183+ key := hex .EncodeToString (sum [:])
184+
185+ c .mu .Lock ()
186+ defer c .mu .Unlock ()
187+
188+ if err := c .ensureDir (); err != nil {
189+ return "" , err
190+ }
191+ path := filepath .Join (c .dir , key )
192+
193+ if c .refs [key ] == 0 {
194+ if err := os .WriteFile (path , serialized , 0o600 ); err != nil {
195+ return "" , fmt .Errorf ("error writing serialized module: %w" , err )
196+ }
197+ }
198+ c .refs [key ]++
199+ return path , nil
200+ }
201+
202+ // release drops one reference to the cache entry at path, removing the file once
203+ // the last reference is gone. It is safe to call with a path returned by store.
204+ func (c * moduleCache ) release (path string ) {
205+ key := filepath .Base (path )
206+
207+ c .mu .Lock ()
208+ defer c .mu .Unlock ()
209+
210+ switch {
211+ case c .refs [key ] <= 1 :
212+ delete (c .refs , key )
213+ _ = os .Remove (path )
214+ default :
215+ c .refs [key ]--
216+ }
217+ }
218+
219+ // ensureDir lazily creates the shared cache directory. The caller must hold c.mu.
220+ func (c * moduleCache ) ensureDir () error {
221+ if c .dir != "" {
222+ return nil
223+ }
224+ dir , err := os .MkdirTemp ("" , "wasm-module-cache-" )
225+ if err != nil {
226+ return fmt .Errorf ("error creating module cache dir: %w" , err )
227+ }
228+ c .dir = dir
229+ return nil
230+ }
231+
155232var _ ModuleV1 = (* module )(nil )
156233
157234type linkFn [T any ] func (m * module , store * wasmtime.Store , mod * wasmtime.Module , exec * execution [T ]) (* wasmtime.Instance , error )
@@ -416,33 +493,29 @@ func newModule(modCfg *ModuleConfig, binary []byte) (*module, error) {
416493
417494 modCfg .SdkLabeler (v2ImportName )
418495
419- // Serialize the compiled module to a file and close the in-memory module: it
420- // is not kept resident for the lifetime of the instance. Each guest
421- // invocation deserializes its own copy from this file (see callWasm).
496+ // Serialize the compiled module and close the in-memory module: it is not
497+ // kept resident for the lifetime of the instance. The serialized bytes are
498+ // stored in the process-global content-addressed cache, keyed by their
499+ // SHA-256, so modules compiled from identical binaries share one file. Each
500+ // guest invocation deserializes its own copy from this file (see callWasm).
422501 serialized , err := mod .Serialize ()
423502 mod .Close ()
424503 if err != nil {
425504 return nil , fmt .Errorf ("error serializing wasmtime module: %w" , err )
426505 }
427506
428- moduleDir , err := os . MkdirTemp ( "" , "wasm-module-" )
507+ modulePath , err := moduleFileCache . store ( serialized )
429508 if err != nil {
430- return nil , fmt .Errorf ("error creating module temp dir: %w" , err )
431- }
432- modulePath := filepath .Join (moduleDir , "module.bin" )
433- if err := os .WriteFile (modulePath , serialized , 0o600 ); err != nil {
434- _ = os .RemoveAll (moduleDir )
435- return nil , fmt .Errorf ("error writing serialized module: %w" , err )
509+ return nil , err
436510 }
437511
438512 metrics , err := newModuleMetrics ()
439513 if err != nil {
440- _ = os . RemoveAll ( moduleDir )
514+ moduleFileCache . release ( modulePath )
441515 return nil , fmt .Errorf ("error creating module metrics: %w" , err )
442516 }
443517
444518 m := & module {
445- moduleDir : moduleDir ,
446519 modulePath : modulePath ,
447520 cfg : modCfg ,
448521 metrics : metrics ,
@@ -603,8 +676,10 @@ func (m *module) Start() {}
603676
604677func (m * module ) Close () {
605678 // The engine and its epoch ticker are process-global (see wasmEngine) and
606- // shared by all modules, so they are intentionally not stopped here.
607- _ = os .RemoveAll (m .moduleDir )
679+ // shared by all modules, so they are intentionally not stopped here. The
680+ // cache entry is reference counted and removed only once the last module
681+ // referencing these serialized bytes is closed (see moduleCache).
682+ moduleFileCache .release (m .modulePath )
608683}
609684
610685func (m * module ) IsLegacyDAG () bool {
0 commit comments