Makes fake clocks increment and fixes mutability bug (#630)

This ensures fake clocks increment so that compilers that implement
sleep with them don't spin.

This also fixes a mutability bug in config where we weren't really doing
clone properly because map references are shared.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-06-17 11:31:48 +08:00
committed by GitHub
parent 338652a182
commit d6330d9cfa
10 changed files with 360 additions and 228 deletions

View File

@@ -425,6 +425,14 @@ of requiring configuration to opt-into real clocks.
See https://gruss.cc/files/fantastictimers.pdf for an example attacks.
## Why does fake time increase on reading?
Both the fake nanotime and walltime increase by 1ms on reading. Particularly in
the case of nanotime, this prevents spinning. For example, when Go compiles
`time.Sleep` using `GOOS=js GOARCH=wasm`, nanotime is used in a loop. If that
never increases, the gouroutine is mistaken for being busy. This would be worse
if a compiler implement sleep using nanotime, yet doesn't check for spinning!
## Why not `time.Clock`?
wazero can't use `time.Clock` as a plugin for clock implementation as it is

144
config.go
View File

@@ -110,7 +110,7 @@ type RuntimeConfig interface {
// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
WithFeatureSignExtensionOps(bool) RuntimeConfig
// WithFeatureSIMD enables the vector value type and vector instructions (aka SIMD). This defaults to false
// WithFeatureSIMD enables the vector value type and vector instructions (aka SIMD). This defaults to false
// as the feature was not in WebAssembly 1.0.
//
// See https://github.com/WebAssembly/spec/blob/main/proposals/simd/SIMD.md
@@ -171,83 +171,89 @@ var engineLessConfig = &runtimeConfig{
// support Compiler. Use NewRuntimeConfig to safely detect and fallback to
// NewRuntimeConfigInterpreter if needed.
func NewRuntimeConfigCompiler() RuntimeConfig {
ret := *engineLessConfig // copy
ret := engineLessConfig.clone()
ret.newEngine = compiler.NewEngine
return &ret
return ret
}
// NewRuntimeConfigInterpreter interprets WebAssembly modules instead of compiling them into assembly.
func NewRuntimeConfigInterpreter() RuntimeConfig {
ret := *engineLessConfig // copy
ret := engineLessConfig.clone()
ret.newEngine = interpreter.NewEngine
return ret
}
// clone makes a deep copy of this runtime config.
func (c *runtimeConfig) clone() *runtimeConfig {
ret := *c // copy except maps which share a ref
return &ret
}
// WithFeatureBulkMemoryOperations implements RuntimeConfig.WithFeatureBulkMemoryOperations
func (c *runtimeConfig) WithFeatureBulkMemoryOperations(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled)
// bulk-memory-operations proposal is mutually-dependant with reference-types proposal.
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled)
return &ret
return ret
}
// WithFeatureMultiValue implements RuntimeConfig.WithFeatureMultiValue
func (c *runtimeConfig) WithFeatureMultiValue(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMultiValue, enabled)
return &ret
return ret
}
// WithFeatureMutableGlobal implements RuntimeConfig.WithFeatureMutableGlobal
func (c *runtimeConfig) WithFeatureMutableGlobal(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMutableGlobal, enabled)
return &ret
return ret
}
// WithFeatureNonTrappingFloatToIntConversion implements RuntimeConfig.WithFeatureNonTrappingFloatToIntConversion
func (c *runtimeConfig) WithFeatureNonTrappingFloatToIntConversion(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureNonTrappingFloatToIntConversion, enabled)
return &ret
return ret
}
// WithFeatureReferenceTypes implements RuntimeConfig.WithFeatureReferenceTypes
func (c *runtimeConfig) WithFeatureReferenceTypes(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureReferenceTypes, enabled)
// reference-types proposal is mutually-dependant with bulk-memory-operations proposal.
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureBulkMemoryOperations, enabled)
return &ret
return ret
}
// WithFeatureSignExtensionOps implements RuntimeConfig.WithFeatureSignExtensionOps
func (c *runtimeConfig) WithFeatureSignExtensionOps(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureSignExtensionOps, enabled)
return &ret
return ret
}
// WithFeatureSIMD implements RuntimeConfig.WithFeatureSIMD
func (c *runtimeConfig) WithFeatureSIMD(enabled bool) RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureSIMD, enabled)
return &ret
return ret
}
// WithWasmCore1 implements RuntimeConfig.WithWasmCore1
func (c *runtimeConfig) WithWasmCore1() RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = wasm.Features20191205
return &ret
return ret
}
// WithWasmCore2 implements RuntimeConfig.WithWasmCore2
func (c *runtimeConfig) WithWasmCore2() RuntimeConfig {
ret := *c // copy
ret := c.clone()
ret.enabledFeatures = wasm.Features20220419
return &ret
return ret
}
// CompiledModule is a WebAssembly 1.0 module ready to be instantiated (Runtime.InstantiateModule) as an api.Module.
@@ -314,14 +320,20 @@ func NewCompileConfig() CompileConfig {
}
}
// clone makes a deep copy of this compile config.
func (c *compileConfig) clone() *compileConfig {
ret := *c // copy except maps which share a ref
return &ret
}
// WithImportRenamer implements CompileConfig.WithImportRenamer
func (c *compileConfig) WithImportRenamer(importRenamer api.ImportRenamer) CompileConfig {
if importRenamer == nil {
return c
}
ret := *c // copy
ret := c.clone()
ret.importRenamer = importRenamer
return &ret
return ret
}
// WithMemorySizer implements CompileConfig.WithMemorySizer
@@ -329,9 +341,9 @@ func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConf
if memorySizer == nil {
return c
}
ret := *c // copy
ret := c.clone()
ret.memorySizer = memorySizer
return &ret
return ret
}
// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
@@ -446,7 +458,8 @@ type ModuleConfig interface {
WithStdout(io.Writer) ModuleConfig
// WithWalltime configures the wall clock, sometimes referred to as the
// real time clock. Defaults to a constant fake result.
// real time clock. Defaults to a fake result that increases by 1ms on
// each reading.
//
// Ex. To override with your own clock:
// moduleConfig = moduleConfig.
@@ -465,7 +478,8 @@ type ModuleConfig interface {
WithSysWalltime() ModuleConfig
// WithNanotime configures the monotonic clock, used to measure elapsed
// time in nanoseconds. Defaults to a constant fake result.
// time in nanoseconds. Defaults to a fake result that increases by 1ms
// on each reading.
//
// Ex. To override with your own clock:
// moduleConfig = moduleConfig.
@@ -512,9 +526,9 @@ type moduleConfig struct {
stdout io.Writer
stderr io.Writer
randSource io.Reader
walltimeTime *sys.Walltime
walltime *sys.Walltime
walltimeResolution sys.ClockResolution
nanotimeTime *sys.Nanotime
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
args []string
// environ is pair-indexed to retain order similar to os.Environ.
@@ -529,21 +543,31 @@ func NewModuleConfig() ModuleConfig {
return &moduleConfig{
startFunctions: []string{"_start"},
environKeys: map[string]int{},
fs: internalsys.NewFSConfig(),
fs: internalsys.NewFSConfig(),
}
}
// clone makes a deep copy of this module config.
func (c *moduleConfig) clone() *moduleConfig {
ret := *c // copy except maps which share a ref
ret.environKeys = make(map[string]int, len(c.environKeys))
for key, value := range c.environKeys {
ret.environKeys[key] = value
}
ret.fs = c.fs.Clone()
return &ret
}
// WithArgs implements ModuleConfig.WithArgs
func (c *moduleConfig) WithArgs(args ...string) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.args = args
return &ret
return ret
}
// WithEnv implements ModuleConfig.WithEnv
func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
ret := *c // copy
ret := c.clone()
// Check to see if this key already exists and update it.
if i, ok := ret.environKeys[key]; ok {
ret.environ[i+1] = value // environ is pair-indexed, so the value is 1 after the key.
@@ -551,57 +575,57 @@ func (c *moduleConfig) WithEnv(key, value string) ModuleConfig {
ret.environKeys[key] = len(ret.environ)
ret.environ = append(ret.environ, key, value)
}
return &ret
return ret
}
// WithFS implements ModuleConfig.WithFS
func (c *moduleConfig) WithFS(fs fs.FS) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.fs = ret.fs.WithFS(fs)
return &ret
return ret
}
// WithName implements ModuleConfig.WithName
func (c *moduleConfig) WithName(name string) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.name = name
return &ret
return ret
}
// WithStartFunctions implements ModuleConfig.WithStartFunctions
func (c *moduleConfig) WithStartFunctions(startFunctions ...string) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.startFunctions = startFunctions
return &ret
return ret
}
// WithStderr implements ModuleConfig.WithStderr
func (c *moduleConfig) WithStderr(stderr io.Writer) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.stderr = stderr
return &ret
return ret
}
// WithStdin implements ModuleConfig.WithStdin
func (c *moduleConfig) WithStdin(stdin io.Reader) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.stdin = stdin
return &ret
return ret
}
// WithStdout implements ModuleConfig.WithStdout
func (c *moduleConfig) WithStdout(stdout io.Writer) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.stdout = stdout
return &ret
return ret
}
// WithWalltime implements ModuleConfig.WithWalltime
func (c *moduleConfig) WithWalltime(walltime sys.Walltime, resolution sys.ClockResolution) ModuleConfig {
ret := *c // copy
ret.walltimeTime = &walltime
ret := c.clone()
ret.walltime = &walltime
ret.walltimeResolution = resolution
return &ret
return ret
}
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
@@ -615,10 +639,10 @@ func (c *moduleConfig) WithSysWalltime() ModuleConfig {
// WithNanotime implements ModuleConfig.WithNanotime
func (c *moduleConfig) WithNanotime(nanotime sys.Nanotime, resolution sys.ClockResolution) ModuleConfig {
ret := *c // copy
ret.nanotimeTime = &nanotime
ret := c.clone()
ret.nanotime = &nanotime
ret.nanotimeResolution = resolution
return &ret
return ret
}
// WithSysNanotime implements ModuleConfig.WithSysNanotime
@@ -628,16 +652,16 @@ func (c *moduleConfig) WithSysNanotime() ModuleConfig {
// WithRandSource implements ModuleConfig.WithRandSource
func (c *moduleConfig) WithRandSource(source io.Reader) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.randSource = source
return &ret
return ret
}
// WithWorkDirFS implements ModuleConfig.WithWorkDirFS
func (c *moduleConfig) WithWorkDirFS(fs fs.FS) ModuleConfig {
ret := *c // copy
ret := c.clone()
ret.fs = ret.fs.WithWorkDirFS(fs)
return &ret
return ret
}
// toSysContext creates a baseline wasm.Context configured by ModuleConfig.
@@ -672,8 +696,8 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.stdout,
c.stderr,
c.randSource,
c.walltimeTime, c.walltimeResolution,
c.nanotimeTime, c.nanotimeResolution,
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
preopens,
)
}

View File

@@ -252,48 +252,58 @@ func TestModuleConfig(t *testing.T) {
tests := []struct {
name string
with func(ModuleConfig) ModuleConfig
expected ModuleConfig
expected string
}{
{
name: "WithName",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("wazero")
},
expected: &moduleConfig{
name: "wazero",
},
expected: "wazero",
},
{
name: "WithName empty",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("")
},
expected: &moduleConfig{},
},
{
name: "WithName twice",
with: func(c ModuleConfig) ModuleConfig {
return c.WithName("wazero").WithName("wa0")
},
expected: &moduleConfig{
name: "wa0",
},
expected: "wa0",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
input := &moduleConfig{}
input := NewModuleConfig()
rc := tc.with(input)
require.Equal(t, tc.expected, rc)
require.Equal(t, tc.expected, rc.(*moduleConfig).name)
// The source wasn't modified
require.Equal(t, &moduleConfig{}, input)
require.Equal(t, NewModuleConfig(), input)
})
}
}
// TestModuleConfig_toSysContext only tests the cases that change the inputs to
// sys.NewContext.
func TestModuleConfig_toSysContext(t *testing.T) {
// Always assigns clocks so that pointers are constant.
var wt sys.Walltime = func(context.Context) (int64, int32) {
return 0, 0
}
var nt sys.Nanotime = func(context.Context) int64 {
return 0
}
base := NewModuleConfig()
base.(*moduleConfig).walltime = &wt
base.(*moduleConfig).walltimeResolution = 1
base.(*moduleConfig).nanotime = &nt
base.(*moduleConfig).nanotimeResolution = 1
testFS := fstest.MapFS{}
testFS2 := fstest.MapFS{}
@@ -304,7 +314,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
}{
{
name: "empty",
input: NewModuleConfig(),
input: base,
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -313,14 +323,14 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithArgs",
input: NewModuleConfig().WithArgs("a", "bc"),
input: base.WithArgs("a", "bc"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"a", "bc"}, // args
@@ -329,14 +339,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithArgs empty ok", // Particularly argv[0] can be empty, and we have no rules about others.
input: NewModuleConfig().WithArgs("", "bc"),
input: base.WithArgs("", "bc"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"", "bc"}, // args
@@ -345,14 +356,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithArgs second call overwrites",
input: NewModuleConfig().WithArgs("a", "bc").WithArgs("bc", "a"),
input: base.WithArgs("a", "bc").WithArgs("bc", "a"),
expected: requireSysContext(t,
math.MaxUint32, // max
[]string{"bc", "a"}, // args
@@ -361,14 +373,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv",
input: NewModuleConfig().WithEnv("a", "b"),
input: base.WithEnv("a", "b"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -377,14 +390,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv empty value",
input: NewModuleConfig().WithEnv("a", ""),
input: base.WithEnv("a", ""),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -393,14 +407,14 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv twice",
input: NewModuleConfig().WithEnv("a", "b").WithEnv("c", "de"),
input: base.WithEnv("a", "b").WithEnv("c", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -409,14 +423,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv overwrites",
input: NewModuleConfig().WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"),
input: base.WithEnv("a", "bc").WithEnv("c", "de").WithEnv("a", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -425,14 +440,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithEnv twice",
input: NewModuleConfig().WithEnv("a", "b").WithEnv("c", "de"),
input: base.WithEnv("a", "b").WithEnv("c", "de"),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -441,14 +457,15 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // openedFiles
),
},
{
name: "WithFS",
input: NewModuleConfig().WithFS(testFS),
input: base.WithFS(testFS),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -457,8 +474,9 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS},
@@ -467,7 +485,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
},
{
name: "WithFS overwrites",
input: NewModuleConfig().WithFS(testFS).WithFS(testFS2),
input: base.WithFS(testFS).WithFS(testFS2),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -476,8 +494,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS2},
4: {Path: ".", FS: testFS2},
@@ -486,7 +504,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
},
{
name: "WithWorkDirFS",
input: NewModuleConfig().WithWorkDirFS(testFS),
input: base.WithWorkDirFS(testFS),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -495,8 +513,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
},
@@ -504,7 +522,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
},
{
name: "WithFS and WithWorkDirFS",
input: NewModuleConfig().WithFS(testFS).WithWorkDirFS(testFS2),
input: base.WithFS(testFS).WithWorkDirFS(testFS2),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -513,8 +531,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: "/", FS: testFS},
4: {Path: ".", FS: testFS2},
@@ -523,7 +541,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
},
{
name: "WithWorkDirFS and WithFS",
input: NewModuleConfig().WithWorkDirFS(testFS).WithFS(testFS2),
input: base.WithWorkDirFS(testFS).WithFS(testFS2),
expected: requireSysContext(t,
math.MaxUint32, // max
nil, // args
@@ -532,8 +550,8 @@ func TestModuleConfig_toSysContext(t *testing.T) {
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
map[uint32]*internalsys.FileEntry{ // openedFiles
3: {Path: ".", FS: testFS},
4: {Path: "/", FS: testFS2},
@@ -748,6 +766,49 @@ func TestModuleConfig_toSysContext_Errors(t *testing.T) {
})
}
}
func TestModuleConfig_clone(t *testing.T) {
mc := NewModuleConfig().(*moduleConfig)
cloned := mc.clone()
fs1 := fstest.MapFS{}
mc.fs.WithWorkDirFS(fs1)
mc.environKeys["2"] = 2
cloned.environKeys["1"] = 1
// Ensure the maps are not shared
require.Equal(t, map[string]int{"2": 2}, mc.environKeys)
require.Equal(t, map[string]int{"1": 1}, cloned.environKeys)
// Ensure the fs is not shared
preopens, err := cloned.fs.Preopens()
require.NoError(t, err)
require.Equal(t, map[uint32]*internalsys.FileEntry{}, preopens)
}
func TestCompiledCode_Close(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}}
var cs []*compiledModule
for i := 0; i < 10; i++ {
m := &wasm.Module{}
err := e.CompileModule(ctx, m)
require.NoError(t, err)
cs = append(cs, &compiledModule{module: m, compiledEngine: e})
}
// Before Close.
require.Equal(t, 10, len(e.cachedModules))
for _, c := range cs {
require.NoError(t, c.Close(ctx))
}
// After Close.
require.Zero(t, len(e.cachedModules))
}
}
// requireSysContext ensures wasm.NewContext doesn't return an error, which makes it usable in test matrices.
func requireSysContext(
@@ -776,27 +837,3 @@ func requireSysContext(
require.NoError(t, err)
return sysCtx
}
func TestCompiledCode_Close(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}}
var cs []*compiledModule
for i := 0; i < 10; i++ {
m := &wasm.Module{}
err := e.CompileModule(ctx, m)
require.NoError(t, err)
cs = append(cs, &compiledModule{module: m, compiledEngine: e})
}
// Before Close.
require.Equal(t, 10, len(e.cachedModules))
for _, c := range cs {
require.NoError(t, c.Close(ctx))
}
// After Close.
require.Zero(t, len(e.cachedModules))
}
}

View File

@@ -2,19 +2,39 @@ package platform
import (
"context"
"sync/atomic"
"time"
"github.com/tetratelabs/wazero/sys"
)
const FakeEpochNanos = int64(1640995200000000000) // midnight UTC 2022-01-01
const (
ms = int64(time.Millisecond)
// FakeEpochNanos is midnight UTC 2022-01-01 and exposed for testing
FakeEpochNanos = 1640995200000 * ms
)
// FakeWalltime implements sys.Walltime with FakeEpochNanos.
func FakeWalltime(context.Context) (sec int64, nsec int32) {
return FakeEpochNanos / 1e9, int32(FakeEpochNanos % 1e9)
// NewFakeWalltime implements sys.Walltime with FakeEpochNanos that increases by 1ms each reading.
// See /RATIONALE.md
func NewFakeWalltime() *sys.Walltime {
// AddInt64 returns the new value. Adjust so the first reading will be FakeEpochNanos
t := FakeEpochNanos - ms
var wt sys.Walltime = func(context.Context) (sec int64, nsec int32) {
wt := atomic.AddInt64(&t, ms)
return wt / 1e9, int32(wt % 1e9)
}
return &wt
}
// FakeNanotime implements sys.Nanotime with FakeEpochNanos.
func FakeNanotime(context.Context) int64 {
return FakeEpochNanos
// NewFakeNanotime implements sys.Nanotime that increases by 1ms each reading.
// See /RATIONALE.md
func NewFakeNanotime() *sys.Nanotime {
// AddInt64 returns the new value. Adjust so the first reading will be zero.
t := int64(0) - ms
var nt sys.Nanotime = func(context.Context) int64 {
return atomic.AddInt64(&t, ms)
}
return &nt
}
// Walltime implements sys.Walltime with time.Now.

View File

@@ -9,6 +9,28 @@ import (
"github.com/tetratelabs/wazero/sys"
)
func Test_NewFakeWalltime(t *testing.T) {
wt := NewFakeWalltime()
// Base should be the same as FakeEpochNanos
sec, nsec := (*wt)(context.Background())
ft := time.UnixMicro(FakeEpochNanos / time.Microsecond.Nanoseconds()).UTC()
require.Equal(t, ft, time.Unix(sec, int64(nsec)).UTC())
// next reading should increase by 1ms
sec, nsec = (*wt)(context.Background())
require.Equal(t, ft.Add(time.Millisecond), time.Unix(sec, int64(nsec)).UTC())
}
func Test_NewFakeNanotime(t *testing.T) {
nt := NewFakeNanotime()
require.Equal(t, int64(0), (*nt)(context.Background()))
// next reading should increase by 1ms
require.Equal(t, int64(time.Millisecond), (*nt)(context.Background()))
}
func Test_Walltime(t *testing.T) {
now := time.Now().Unix()
sec, nsec := Walltime(context.Background())

View File

@@ -121,6 +121,20 @@ func NewFSConfig() *FSConfig {
}
}
// Clone makes a deep copy of this FS config.
func (c *FSConfig) Clone() *FSConfig {
ret := *c // copy except maps which share a ref
ret.preopens = make(map[uint32]*FileEntry, len(c.preopens))
for key, value := range c.preopens {
ret.preopens[key] = value
}
ret.preopenPaths = make(map[string]uint32, len(c.preopenPaths))
for key, value := range c.preopenPaths {
ret.preopenPaths[key] = value
}
return &ret
}
// setFS maps a path to a file-system. This is only used for base paths: "/" and ".".
func (c *FSConfig) setFS(path string, fs fs.FS) {
// Check to see if this key already exists and update it.
@@ -135,15 +149,15 @@ func (c *FSConfig) setFS(path string, fs fs.FS) {
}
func (c *FSConfig) WithFS(fs fs.FS) *FSConfig {
ret := *c // copy
ret := c.Clone()
ret.setFS("/", fs)
return &ret
return ret
}
func (c *FSConfig) WithWorkDirFS(fs fs.FS) *FSConfig {
ret := *c // copy
ret := c.Clone()
ret.setFS(".", fs)
return &ret
return ret
}
func (c *FSConfig) Preopens() (map[uint32]*FileEntry, error) {

View File

@@ -57,3 +57,20 @@ func (f *testFile) Close() error { return f.closeErr }
func (f *testFile) Stat() (fs.FileInfo, error) { return nil, nil }
func (f *testFile) Read(_ []byte) (int, error) { return 0, nil }
func (f *testFile) Seek(_ int64, _ int) (int64, error) { return 0, nil }
func TestFSConfig_Clone(t *testing.T) {
fsc := NewFSConfig()
cloned := fsc.Clone()
fsc.preopens[2] = nil
fsc.preopenPaths["2"] = 2
cloned.preopens[1] = nil
cloned.preopenPaths["1"] = 1
// Ensure the maps are not shared
require.Equal(t, map[uint32]*FileEntry{2: nil}, fsc.preopens)
require.Equal(t, map[string]uint32{"2": 2}, fsc.preopenPaths)
require.Equal(t, map[uint32]*FileEntry{1: nil}, cloned.preopens)
require.Equal(t, map[string]uint32{"1": 1}, cloned.preopenPaths)
}

View File

@@ -136,8 +136,6 @@ func DefaultContext() *Context {
}
var _ = DefaultContext() // Force panic on bug.
var wt sys.Walltime = platform.FakeWalltime
var nt sys.Nanotime = platform.FakeNanotime
// NewContext is a factory function which helps avoid needing to know defaults or exporting all fields.
// Note: max is exposed for testing. max is only used for env/args validation.
@@ -192,7 +190,7 @@ func NewContext(
sysCtx.walltime = walltime
sysCtx.walltimeResolution = walltimeResolution
} else {
sysCtx.walltime = &wt
sysCtx.walltime = platform.NewFakeWalltime()
sysCtx.walltimeResolution = sys.ClockResolution(time.Microsecond.Nanoseconds())
}
@@ -203,7 +201,7 @@ func NewContext(
sysCtx.nanotime = nanotime
sysCtx.nanotimeResolution = nanotimeResolution
} else {
sysCtx.nanotime = &nt
sysCtx.nanotime = platform.NewFakeNanotime()
sysCtx.nanotimeResolution = sys.ClockResolution(time.Nanosecond)
}

View File

@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
@@ -33,9 +34,12 @@ func TestDefaultSysContext(t *testing.T) {
require.Equal(t, eofReader{}, sysCtx.Stdin())
require.Equal(t, io.Discard, sysCtx.Stdout())
require.Equal(t, io.Discard, sysCtx.Stderr())
require.Equal(t, &wt, sysCtx.walltime) // To compare functions, we can only compare pointers.
// To compare functions, we can only compare pointers, but the pointer will
// change. Hence, we have to compare the results instead.
sec, _ := sysCtx.Walltime(testCtx)
require.Equal(t, platform.FakeEpochNanos/time.Second.Nanoseconds(), sec)
require.Equal(t, sys.ClockResolution(1_000), sysCtx.WalltimeResolution())
require.Equal(t, &nt, sysCtx.nanotime) // To compare functions, we can only compare pointers.
require.Zero(t, sysCtx.Nanotime(testCtx)) // See above on functions.
require.Equal(t, sys.ClockResolution(1), sysCtx.NanotimeResolution())
require.Equal(t, rand.Reader, sysCtx.RandSource())
require.Equal(t, NewFSContext(map[uint32]*FileEntry{}), sysCtx.FS())
@@ -172,12 +176,12 @@ func TestNewContext_Walltime(t *testing.T) {
}{
{
name: "ok",
time: &wt,
time: platform.NewFakeWalltime(),
resolution: 3,
},
{
name: "invalid resolution",
time: &wt,
time: platform.NewFakeWalltime(),
resolution: 0,
expectedErr: "invalid Walltime resolution: 0",
},
@@ -219,12 +223,12 @@ func TestNewContext_Nanotime(t *testing.T) {
}{
{
name: "ok",
time: &nt,
time: platform.NewFakeNanotime(),
resolution: 3,
},
{
name: "invalid resolution",
time: &nt,
time: platform.NewFakeNanotime(),
resolution: 0,
expectedErr: "invalid Nanotime resolution: 0",
},

View File

@@ -19,7 +19,6 @@ import (
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/platform"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -520,90 +519,79 @@ func TestSnapshotPreview1_ClockResGet_Unsupported(t *testing.T) {
}
}
func TestSnapshotPreview1_ClockTimeGet_Realtime(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
expectedMemory := []byte{
'?', // resultTimestamp is after this
0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos
'?', // stopped after encoding
}
mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
tests := []struct {
name string
invocation func() Errno
}{
{
name: "wasi.ClockTimeGet",
invocation: func() Errno {
// invoke ClockTimeGet directly and check the memory side effects!
return a.ClockTimeGet(testCtx, mod, 0 /* REALTIME */, 0 /* TODO: precision */, resultTimestamp)
},
},
{
name: functionClockTimeGet,
invocation: func() Errno {
results, err := fn.Call(testCtx, 0 /* REALTIME */, 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
return errno
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory))
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
})
}
}
func TestSnapshotPreview1_ClockTimeGet_Monotonic(t *testing.T) {
func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
tests := []struct {
name string
invocation func() Errno
clocks := []struct {
clock string
id uint32
expectedMemory []byte
}{
{
name: "wasi.ClockTimeGet",
invocation: func() Errno {
return a.ClockTimeGet(testCtx, mod, 1 /* MONOTONIC */, 0 /* TODO: precision */, resultTimestamp)
clock: "Realtime",
id: clockIDRealtime,
expectedMemory: []byte{
'?', // resultTimestamp is after this
0x0, 0x0, 0x1f, 0xa6, 0x70, 0xfc, 0xc5, 0x16, // little endian-encoded epochNanos
'?', // stopped after encoding
},
},
{
name: functionClockTimeGet,
invocation: func() Errno {
results, err := fn.Call(testCtx, 1 /* MONOTONIC */, 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
return errno
clock: "Monotonic",
id: clockIDMonotonic,
expectedMemory: []byte{
'?', // resultTimestamp is after this
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // fake nanotime starts at zero
'?', // stopped after encoding
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
for _, c := range clocks {
cc := c
t.Run(cc.clock, func(t *testing.T) {
tests := []struct {
name string
invocation func() Errno
}{
{
name: "wasi.ClockTimeGet",
invocation: func() Errno {
return a.ClockTimeGet(testCtx, mod, cc.id, 0 /* TODO: precision */, resultTimestamp)
},
},
{
name: functionClockTimeGet,
invocation: func() Errno {
results, err := fn.Call(testCtx, uint64(cc.id), 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
return errno
},
},
}
tick, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
require.True(t, ok)
require.Equal(t, uint64(platform.FakeEpochNanos), tick)
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
// Reset the fake clock
sysCtx, err := newSysContext(nil, nil, nil)
require.NoError(t, err)
mod.(*wasm.CallContext).Sys = sysCtx
maskMemory(t, testCtx, mod, len(cc.expectedMemory))
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(cc.expectedMemory)))
require.True(t, ok)
require.Equal(t, cc.expectedMemory, actual)
})
}
})
}
}