github.com/tetratelabs/wazero@v1.7.1/cache_test.go (about) 1 package wazero 2 3 import ( 4 "context" 5 _ "embed" 6 "fmt" 7 "os" 8 "path" 9 goruntime "runtime" 10 "testing" 11 12 "github.com/tetratelabs/wazero/internal/platform" 13 "github.com/tetratelabs/wazero/internal/testing/require" 14 "github.com/tetratelabs/wazero/internal/wasm" 15 ) 16 17 //go:embed internal/integration_test/vs/testdata/fac.wasm 18 var facWasm []byte 19 20 //go:embed internal/integration_test/vs/testdata/mem_grow.wasm 21 var memGrowWasm []byte 22 23 func TestCompilationCache(t *testing.T) { 24 ctx := context.Background() 25 // Ensures the normal Wasm module compilation cache works. 26 t.Run("non-host module", func(t *testing.T) { 27 foo, bar := getCacheSharedRuntimes(ctx, t) 28 cacheInst := foo.cache 29 30 // Create a different type id on the bar's store so that we can emulate that bar instantiated the module before facWasm. 31 _, err := bar.store.GetFunctionTypeIDs( 32 // Arbitrary one is fine as long as it is not used in facWasm. 33 []wasm.FunctionType{{Params: []wasm.ValueType{ 34 wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, 35 wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32, 36 wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32, 37 wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32, 38 wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, 39 }}}) 40 require.NoError(t, err) 41 42 // add interpreter first, to ensure compiler support isn't order dependent 43 eng := foo.cache.engs[engineKindInterpreter] 44 if platform.CompilerSupported() { 45 eng = foo.cache.engs[engineKindCompiler] 46 } 47 48 // Try compiling. 49 compiled, err := foo.CompileModule(ctx, facWasm) 50 require.NoError(t, err) 51 // Also check it is actually cached. 52 require.Equal(t, uint32(1), eng.CompiledModuleCount()) 53 barCompiled, err := bar.CompileModule(ctx, facWasm) 54 require.NoError(t, err) 55 56 // Ensures compiled modules are the same modulo type IDs, which is unique per store. 57 require.Equal(t, compiled.(*compiledModule).module, barCompiled.(*compiledModule).module) 58 require.Equal(t, compiled.(*compiledModule).closeWithModule, barCompiled.(*compiledModule).closeWithModule) 59 require.Equal(t, compiled.(*compiledModule).compiledEngine, barCompiled.(*compiledModule).compiledEngine) 60 // TypeIDs must be different as we create a different type ID on bar beforehand. 61 require.NotEqual(t, compiled.(*compiledModule).typeIDs, barCompiled.(*compiledModule).typeIDs) 62 63 // Two runtimes are completely separate except the compilation cache, 64 // therefore it should be ok to instantiate the same name module for each of them. 65 fooInst, err := foo.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name")) 66 require.NoError(t, err) 67 barInst, err := bar.InstantiateModule(ctx, compiled, NewModuleConfig().WithName("same_name")) 68 require.NoError(t, err) 69 // Two instances are not equal. 70 require.NotEqual(t, fooInst, barInst) 71 72 // Closing two runtimes shouldn't clear the cache as cache.Close must be explicitly called to clear the cache. 73 err = foo.Close(ctx) 74 require.NoError(t, err) 75 err = bar.Close(ctx) 76 require.NoError(t, err) 77 require.Equal(t, uint32(1), eng.CompiledModuleCount()) 78 79 // Close the cache, and ensure the engine is closed. 80 err = cacheInst.Close(ctx) 81 require.NoError(t, err) 82 require.Equal(t, uint32(0), eng.CompiledModuleCount()) 83 }) 84 85 // Even when cache is configured, compiled host modules must be different as that's the way 86 // to provide per-runtime isolation on Go functions. 87 t.Run("host module", func(t *testing.T) { 88 foo, bar := getCacheSharedRuntimes(ctx, t) 89 90 goFn := func() (dummy uint32) { return } 91 fooCompiled, err := foo.NewHostModuleBuilder("env"). 92 NewFunctionBuilder().WithFunc(goFn).Export("go_fn"). 93 Compile(testCtx) 94 require.NoError(t, err) 95 barCompiled, err := bar.NewHostModuleBuilder("env"). 96 NewFunctionBuilder().WithFunc(goFn).Export("go_fn"). 97 Compile(testCtx) 98 require.NoError(t, err) 99 100 // Ensures they are different. 101 require.NotEqual(t, fooCompiled, barCompiled) 102 }) 103 104 t.Run("memory limit should not affect caches", func(t *testing.T) { 105 // Creates new cache instance and pass it to the config. 106 c := NewCompilationCache() 107 config := NewRuntimeConfig().WithCompilationCache(c) 108 109 // create two different runtimes with separate memory limits 110 rt0 := NewRuntimeWithConfig(ctx, config) 111 rt1 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(2)) 112 rt2 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(4)) 113 114 // the compiled module is not equal because the memory limits are applied to the Memory instance 115 module0, _ := rt0.CompileModule(ctx, memGrowWasm) 116 module1, _ := rt1.CompileModule(ctx, memGrowWasm) 117 module2, _ := rt2.CompileModule(ctx, memGrowWasm) 118 119 max0, _ := module0.ExportedMemories()["memory"].Max() 120 max1, _ := module1.ExportedMemories()["memory"].Max() 121 max2, _ := module2.ExportedMemories()["memory"].Max() 122 require.Equal(t, uint32(5), max0) 123 require.Equal(t, uint32(2), max1) 124 require.Equal(t, uint32(4), max2) 125 126 compiledModule0 := module0.(*compiledModule) 127 compiledModule1 := module1.(*compiledModule) 128 compiledModule2 := module2.(*compiledModule) 129 130 // compare the compiled engine which contains the underlying "codes" 131 require.Equal(t, compiledModule0.compiledEngine, compiledModule1.compiledEngine) 132 require.Equal(t, compiledModule1.compiledEngine, compiledModule2.compiledEngine) 133 }) 134 } 135 136 func getCacheSharedRuntimes(ctx context.Context, t *testing.T) (foo, bar *runtime) { 137 // Creates new cache instance and pass it to the config. 138 c := NewCompilationCache() 139 config := NewRuntimeConfig().WithCompilationCache(c) 140 141 _foo := NewRuntimeWithConfig(ctx, config) 142 _bar := NewRuntimeWithConfig(ctx, config) 143 144 var ok bool 145 foo, ok = _foo.(*runtime) 146 require.True(t, ok) 147 bar, ok = _bar.(*runtime) 148 require.True(t, ok) 149 150 // Make sure that two runtimes share the same cache instance. 151 require.Equal(t, foo.cache, bar.cache) 152 return 153 } 154 155 func TestCache_ensuresFileCache(t *testing.T) { 156 const version = "dev" 157 // We expect to create a version-specific subdirectory. 158 expectedSubdir := fmt.Sprintf("wazero-dev-%s-%s", goruntime.GOARCH, goruntime.GOOS) 159 160 t.Run("ok", func(t *testing.T) { 161 dir := t.TempDir() 162 c := &cache{} 163 err := c.ensuresFileCache(dir, version) 164 require.NoError(t, err) 165 }) 166 t.Run("create dir", func(t *testing.T) { 167 tmpDir := path.Join(t.TempDir(), "1", "2", "3") 168 dir := path.Join(tmpDir, "foo") // Non-existent directory. 169 170 c := &cache{} 171 err := c.ensuresFileCache(dir, version) 172 require.NoError(t, err) 173 174 requireContainsDir(t, tmpDir, "foo") 175 }) 176 t.Run("create relative dir", func(t *testing.T) { 177 tmpDir, oldwd := requireChdirToTemp(t) 178 defer os.Chdir(oldwd) //nolint 179 dir := "foo" 180 181 c := &cache{} 182 err := c.ensuresFileCache(dir, version) 183 require.NoError(t, err) 184 185 requireContainsDir(t, tmpDir, dir) 186 }) 187 t.Run("basedir is not a dir", func(t *testing.T) { 188 f, err := os.CreateTemp(t.TempDir(), "nondir") 189 require.NoError(t, err) 190 defer f.Close() 191 192 c := &cache{} 193 err = c.ensuresFileCache(f.Name(), version) 194 require.Contains(t, err.Error(), "is not dir") 195 }) 196 t.Run("versiondir is not a dir", func(t *testing.T) { 197 dir := t.TempDir() 198 require.NoError(t, os.WriteFile(path.Join(dir, expectedSubdir), []byte{}, 0o600)) 199 c := &cache{} 200 err := c.ensuresFileCache(dir, version) 201 require.Contains(t, err.Error(), "is not dir") 202 }) 203 } 204 205 // requireContainsDir ensures the directory was created in the correct path, 206 // as file.Abs can return slightly different answers for a temp directory. For 207 // example, /var/folders/... vs /private/var/folders/... 208 func requireContainsDir(t *testing.T, parent, dir string) { 209 entries, err := os.ReadDir(parent) 210 require.NoError(t, err) 211 require.Equal(t, 1, len(entries)) 212 require.Equal(t, dir, entries[0].Name()) 213 require.True(t, entries[0].IsDir()) 214 } 215 216 func requireChdirToTemp(t *testing.T) (string, string) { 217 tmpDir := t.TempDir() 218 oldwd, err := os.Getwd() 219 require.NoError(t, err) 220 require.NoError(t, os.Chdir(tmpDir)) 221 return tmpDir, oldwd 222 } 223 224 func TestCache_Close(t *testing.T) { 225 t.Run("all engines", func(t *testing.T) { 226 c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, &mockEngine{}}} 227 err := c.Close(testCtx) 228 require.NoError(t, err) 229 for i := engineKind(0); i < engineKindCount; i++ { 230 require.True(t, c.engs[i].(*mockEngine).closed) 231 } 232 }) 233 t.Run("only interp", func(t *testing.T) { 234 c := &cache{engs: [engineKindCount]wasm.Engine{nil, &mockEngine{}}} 235 err := c.Close(testCtx) 236 require.NoError(t, err) 237 require.True(t, c.engs[engineKindInterpreter].(*mockEngine).closed) 238 }) 239 t.Run("only compiler", func(t *testing.T) { 240 c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, nil}} 241 err := c.Close(testCtx) 242 require.NoError(t, err) 243 require.True(t, c.engs[engineKindCompiler].(*mockEngine).closed) 244 }) 245 }