forked from tetratelabs/wazero
-
Notifications
You must be signed in to change notification settings - Fork 1
/
cache_test.go
245 lines (210 loc) · 8.83 KB
/
cache_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package wazero
import (
"context"
_ "embed"
"fmt"
"os"
"path"
goruntime "runtime"
"testing"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
//go:embed internal/integration_test/vs/testdata/fac.wasm
var facWasm []byte
//go:embed internal/integration_test/vs/testdata/mem_grow.wasm
var memGrowWasm []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
// Create a different type id on the bar's store so that we can emulate that bar instantiated the module before facWasm.
_, err := bar.store.GetFunctionTypeIDs(
// Arbitrary one is fine as long as it is not used in facWasm.
[]wasm.FunctionType{{Params: []wasm.ValueType{
wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeV128, wasm.ValueTypeI32, wasm.ValueTypeI32,
wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32, wasm.ValueTypeI32,
}}})
require.NoError(t, err)
// add interpreter first, to ensure compiler support isn't order dependent
eng := foo.cache.engs[engineKindInterpreter]
if platform.CompilerSupported() {
eng = foo.cache.engs[engineKindCompiler]
}
// Try compiling.
compiled, err := foo.CompileModule(ctx, facWasm)
require.NoError(t, err)
// Also check it is actually cached.
require.Equal(t, uint32(1), eng.CompiledModuleCount())
barCompiled, err := bar.CompileModule(ctx, facWasm)
require.NoError(t, err)
// Ensures compiled modules are the same modulo type IDs, which is unique per store.
require.Equal(t, compiled.(*compiledModule).module, barCompiled.(*compiledModule).module)
require.Equal(t, compiled.(*compiledModule).closeWithModule, barCompiled.(*compiledModule).closeWithModule)
require.Equal(t, compiled.(*compiledModule).compiledEngine, barCompiled.(*compiledModule).compiledEngine)
// TypeIDs must be different as we create a different type ID on bar beforehand.
require.NotEqual(t, compiled.(*compiledModule).typeIDs, barCompiled.(*compiledModule).typeIDs)
// 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), eng.CompiledModuleCount())
// Close the cache, and ensure the engine is closed.
err = cacheInst.Close(ctx)
require.NoError(t, err)
require.Equal(t, uint32(0), 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)
})
t.Run("memory limit should not affect caches", func(t *testing.T) {
// Creates new cache instance and pass it to the config.
c := NewCompilationCache()
config := NewRuntimeConfig().WithCompilationCache(c)
// create two different runtimes with separate memory limits
rt0 := NewRuntimeWithConfig(ctx, config)
rt1 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(2))
rt2 := NewRuntimeWithConfig(ctx, config.WithMemoryLimitPages(4))
// the compiled module is not equal because the memory limits are applied to the Memory instance
module0, _ := rt0.CompileModule(ctx, memGrowWasm)
module1, _ := rt1.CompileModule(ctx, memGrowWasm)
module2, _ := rt2.CompileModule(ctx, memGrowWasm)
max0, _ := module0.ExportedMemories()["memory"].Max()
max1, _ := module1.ExportedMemories()["memory"].Max()
max2, _ := module2.ExportedMemories()["memory"].Max()
require.Equal(t, uint32(5), max0)
require.Equal(t, uint32(2), max1)
require.Equal(t, uint32(4), max2)
compiledModule0 := module0.(*compiledModule)
compiledModule1 := module1.(*compiledModule)
compiledModule2 := module2.(*compiledModule)
// compare the compiled engine which contains the underlying "codes"
require.Equal(t, compiledModule0.compiledEngine, compiledModule1.compiledEngine)
require.Equal(t, compiledModule1.compiledEngine, compiledModule2.compiledEngine)
})
}
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
}
func TestCache_Close(t *testing.T) {
t.Run("all engines", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, &mockEngine{}}}
err := c.Close(testCtx)
require.NoError(t, err)
for i := engineKind(0); i < engineKindCount; i++ {
require.True(t, c.engs[i].(*mockEngine).closed)
}
})
t.Run("only interp", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{nil, &mockEngine{}}}
err := c.Close(testCtx)
require.NoError(t, err)
require.True(t, c.engs[engineKindInterpreter].(*mockEngine).closed)
})
t.Run("only compiler", func(t *testing.T) {
c := &cache{engs: [engineKindCount]wasm.Engine{&mockEngine{}, nil}}
err := c.Close(testCtx)
require.NoError(t, err)
require.True(t, c.engs[engineKindCompiler].(*mockEngine).closed)
})
}