Files
wazero/internal/engine/compiler/engine_cache.go
Takeshi Yoneda 35500f9b85 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 <takeshi@tetrate.io>
Co-authored-by: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com>
2023-01-10 09:32:42 +09:00

206 lines
5.6 KiB
Go

package compiler
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/u32"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
)
func (e *engine) deleteCodes(module *wasm.Module) {
e.mux.Lock()
defer e.mux.Unlock()
delete(e.codes, module.ID)
// Note: we do not call e.Cache.Delete, as the lifetime of
// the content is up to the implementation of extencache.Cache interface.
}
func (e *engine) addCodes(module *wasm.Module, codes []*code, withGoFunc bool) (err error) {
e.addCodesToMemory(module, codes)
if !withGoFunc {
err = e.addCodesToCache(module, codes)
}
return
}
func (e *engine) getCodes(module *wasm.Module) (codes []*code, ok bool, err error) {
codes, ok = e.getCodesFromMemory(module)
if ok {
return
}
codes, ok, err = e.getCodesFromCache(module)
if ok {
e.addCodesToMemory(module, codes)
}
return
}
func (e *engine) addCodesToMemory(module *wasm.Module, codes []*code) {
e.mux.Lock()
defer e.mux.Unlock()
e.codes[module.ID] = codes
}
func (e *engine) getCodesFromMemory(module *wasm.Module) (codes []*code, ok bool) {
e.mux.RLock()
defer e.mux.RUnlock()
codes, ok = e.codes[module.ID]
return
}
func (e *engine) addCodesToCache(module *wasm.Module, codes []*code) (err error) {
if e.fileCache == nil || module.IsHostModule {
return
}
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.fileCache == nil || module.IsHostModule {
return
}
// Check if the entries exist in the external cache.
var cached io.ReadCloser
cached, hit, err = e.fileCache.Get(module.ID)
if !hit || err != nil {
return
}
// Otherwise, we hit the cache on external cache.
// We retrieve *code structures from `cached`.
var staleCache bool
// Note: cached.Close is ensured to be called in deserializeCodes.
codes, staleCache, err = deserializeCodes(e.wazeroVersion, cached)
if err != nil {
hit = false
return
} else if staleCache {
return nil, false, e.fileCache.Delete(module.ID)
}
for i, c := range codes {
c.indexInModule = wasm.Index(i)
c.sourceModule = module
}
return
}
var wazeroMagic = "WAZERO" // version must be synced with the tag of the wazero library.
func serializeCodes(wazeroVersion string, codes []*code) io.Reader {
buf := bytes.NewBuffer(nil)
// First 6 byte: WAZERO header.
buf.WriteString(wazeroMagic)
// Next 1 byte: length of version:
buf.WriteByte(byte(len(wazeroVersion)))
// Version of wazero.
buf.WriteString(wazeroVersion)
// Number of *code (== locally defined functions in the module): 4 bytes.
buf.Write(u32.LeBytes(uint32(len(codes))))
for _, c := range codes {
// The stack pointer ceil (8 bytes).
buf.Write(u64.LeBytes(c.stackPointerCeil))
// The length of code segment (8 bytes).
buf.Write(u64.LeBytes(uint64(len(c.codeSegment))))
// Append the native code.
buf.Write(c.codeSegment)
}
return bytes.NewReader(buf.Bytes())
}
func deserializeCodes(wazeroVersion string, reader io.ReadCloser) (codes []*code, staleCache bool, err error) {
defer reader.Close()
cacheHeaderSize := len(wazeroMagic) + 1 /* version size */ + len(wazeroVersion) + 4 /* number of functions */
// Read the header before the native code.
header := make([]byte, cacheHeaderSize)
n, err := reader.Read(header)
if err != nil {
return nil, false, fmt.Errorf("compilationcache: error reading header: %v", err)
}
if n != cacheHeaderSize {
return nil, false, fmt.Errorf("compilationcache: invalid header length: %d", n)
}
// Check the version compatibility.
versionSize := int(header[len(wazeroMagic)])
cachedVersionBegin, cachedVersionEnd := len(wazeroMagic)+1, len(wazeroMagic)+1+versionSize
if cachedVersionEnd >= len(header) {
staleCache = true
return
} else if cachedVersion := string(header[cachedVersionBegin:cachedVersionEnd]); cachedVersion != wazeroVersion {
staleCache = true
return
}
functionsNum := binary.LittleEndian.Uint32(header[len(header)-4:])
codes = make([]*code, 0, functionsNum)
var eightBytes [8]byte
var nativeCodeLen uint64
for i := uint32(0); i < functionsNum; i++ {
c := &code{}
// Read the stack pointer ceil.
if c.stackPointerCeil, err = readUint64(reader, &eightBytes); err != nil {
err = fmt.Errorf("compilationcache: error reading func[%d] stack pointer ceil: %v", i, err)
break
}
// Read (and mmap) the native code.
if nativeCodeLen, err = readUint64(reader, &eightBytes); err != nil {
err = fmt.Errorf("compilationcache: error reading func[%d] reading native code size: %v", i, err)
break
}
if c.codeSegment, err = platform.MmapCodeSegment(reader, int(nativeCodeLen)); err != nil {
err = fmt.Errorf("compilationcache: error mmapping func[%d] code (len=%d): %v", i, nativeCodeLen, err)
break
}
codes = append(codes, c)
}
if err != nil {
for _, c := range codes {
if errMunmap := platform.MunmapCodeSegment(c.codeSegment); errMunmap != nil {
// Munmap failure shouldn't happen.
panic(errMunmap)
}
}
codes = nil
}
return
}
// readUint64 strictly reads a uint64 in little-endian byte order, using the
// given array as a buffer. This returns io.EOF if less than 8 bytes were read.
func readUint64(reader io.Reader, b *[8]byte) (uint64, error) {
s := b[0:8]
n, err := reader.Read(s)
if err != nil {
return 0, err
} else if n < 8 { // more strict than reader.Read
return 0, io.EOF
}
// read the u64 from the underlying buffer
ret := binary.LittleEndian.Uint64(s)
// clear the underlying array
for i := 0; i < 8; i++ {
b[i] = 0
}
return ret, nil
}