wasi: implements sched_yield with sys.Osyield (#1131)

This implements WASI `sched_yield` with `sys.Osyield` that defaults to
return immediately. This is intentionally left without a built-in
alternative as common platforms such as darwin implement
`runtime.osyield` by sleeping for a microsecond. If we implemented that,
user code would be slowed down without a clear reason why.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2023-02-15 15:57:24 -10:00
committed by GitHub
parent e54e0c7b52
commit 3d72f2cb90
10 changed files with 142 additions and 11 deletions

View File

@@ -843,6 +843,32 @@ used in the common case, even if it isn't used by Go, because this gives an
easy and efficient closure over a common program function. We also documented
`sys.Nanotime` to warn users that some compilers don't optimize sleep.
## sys.Osyield
We expose `sys.Osyield`, to allow users to control the behavior of WASI's
`sched_yield` without a new build of wazero. This is mainly for parity with
all other related features which we allow users to implement, including
`sys.Nanosleep`. Unlike others, we don't provide an out-of-box implementation
primarily because it will cause performance problems when accessed.
For example, the below implementation uses CGO, which might result in a 1us
delay per invocation depending on the platform.
See https://github.com/golang/go/issues/19409#issuecomment-284788196
```go
//go:noescape
//go:linkname osyield runtime.osyield
func osyield()
```
In practice, a request to customize this is unlikely to happen until other
thread based functions are implemented. That said, as of early 2023, there are
a few signs of implementation interest and cross-referencing:
See https://github.com/WebAssembly/stack-switching/discussions/38
See https://github.com/WebAssembly/wasi-threads#what-can-be-skipped
See https://slinkydeveloper.com/Kubernetes-controllers-A-New-Hope/
## Signed encoding of integer global constant initializers
wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For

View File

@@ -554,7 +554,7 @@ type ModuleConfig interface {
//
// This example uses a custom sleep function:
// moduleConfig = moduleConfig.
// WithNanosleep(func(ctx context.Context, ns int64) {
// WithNanosleep(func(ns int64) {
// rel := unix.NsecToTimespec(ns)
// remain := unix.Timespec{}
// for { // loop until no more time remaining
@@ -569,6 +569,14 @@ type ModuleConfig interface {
// - Use WithSysNanosleep for a usable implementation.
WithNanosleep(sys.Nanosleep) ModuleConfig
// WithOsyield yields the processor, typically to implement spin-wait
// loops. Defaults to return immediately.
//
// # Notes:
// - This primarily supports `sched_yield` in WASI
// - This does not default to runtime.osyield as that violates sandboxing.
WithOsyield(sys.Osyield) ModuleConfig
// WithSysNanosleep uses time.Sleep for sys.Nanosleep.
//
// See WithNanosleep
@@ -598,6 +606,7 @@ type moduleConfig struct {
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
nanosleep *sys.Nanosleep
osyield *sys.Osyield
args [][]byte
// environ is pair-indexed to retain order similar to os.Environ.
environ [][]byte
@@ -740,6 +749,13 @@ func (c *moduleConfig) WithNanosleep(nanosleep sys.Nanosleep) ModuleConfig {
return &ret
}
// WithOsyield implements ModuleConfig.WithOsyield
func (c *moduleConfig) WithOsyield(osyield sys.Osyield) ModuleConfig {
ret := *c // copy
ret.osyield = &osyield
return &ret
}
// WithSysNanosleep implements ModuleConfig.WithSysNanosleep
func (c *moduleConfig) WithSysNanosleep() ModuleConfig {
return c.WithNanosleep(platform.Nanosleep)
@@ -796,7 +812,7 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.randSource,
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep,
c.nanosleep, c.osyield,
fs,
)
}

View File

@@ -176,6 +176,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -193,6 +194,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -210,6 +212,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -227,6 +230,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -244,6 +248,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -261,6 +266,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -278,6 +284,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -295,6 +302,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -312,6 +320,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -329,6 +338,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
sysfs.Adapt(testFS),
),
},
@@ -346,6 +356,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
sysfs.Adapt(testFS2), // fs
),
},
@@ -363,6 +374,7 @@ func TestModuleConfig_toSysContext(t *testing.T) {
&wt, 1, // walltime, walltimeResolution
&nt, 1, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // fs
),
},
@@ -522,6 +534,19 @@ func TestModuleConfig_toSysContext_WithNanosleep(t *testing.T) {
sysCtx.Nanosleep(2)
}
// TestModuleConfig_toSysContext_WithOsyield has to test differently because
// we can't compare function pointers when functions are passed by value.
func TestModuleConfig_toSysContext_WithOsyield(t *testing.T) {
var yielded bool
sysCtx, err := NewModuleConfig().
WithOsyield(func() {
yielded = true
}).(*moduleConfig).toSysContext()
require.NoError(t, err)
sysCtx.Osyield()
require.True(t, yielded)
}
func TestModuleConfig_toSysContext_Errors(t *testing.T) {
tests := []struct {
name string
@@ -703,6 +728,7 @@ func requireSysContext(
walltime *sys.Walltime, walltimeResolution sys.ClockResolution,
nanotime *sys.Nanotime, nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
osyield *sys.Osyield,
fs sysfs.FS,
) *internalsys.Context {
sysCtx, err := internalsys.NewContext(
@@ -715,7 +741,7 @@ func requireSysContext(
randSource,
walltime, walltimeResolution,
nanotime, nanotimeResolution,
nanosleep,
nanosleep, osyield,
fs,
)
require.NoError(t, err)

View File

@@ -1,11 +1,21 @@
package wasi_snapshot_preview1
import (
"context"
"github.com/tetratelabs/wazero/api"
. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
"github.com/tetratelabs/wazero/internal/wasm"
)
// schedYield is the WASI function named SchedYieldName which temporarily
// yields execution of the calling thread.
//
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno
var schedYield = stubFunction(SchedYieldName, nil)
var schedYield = newHostFunc(SchedYieldName, schedYieldFn, nil)
func schedYieldFn(_ context.Context, mod api.Module, _ []uint64) Errno {
sysCtx := mod.(*wasm.CallContext).Sys
sysCtx.Osyield()
return ErrnoSuccess
}

View File

@@ -3,15 +3,22 @@ package wasi_snapshot_preview1_test
import (
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/internal/testing/require"
. "github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
)
// Test_schedYield only tests it is stubbed for GrainLang per #271
func Test_schedYield(t *testing.T) {
log := requireErrnoNosys(t, SchedYieldName)
var yielded bool
mod, r, log := requireProxyModule(t, wazero.NewModuleConfig().
WithOsyield(func() {
yielded = true
}))
defer r.Close(testCtx)
requireErrno(t, ErrnoSuccess, mod, SchedYieldName)
require.Equal(t, `
--> wasi_snapshot_preview1.sched_yield()
<-- errno=ENOSYS
`, log)
==> wasi_snapshot_preview1.sched_yield()
<== errno=ESUCCESS
`, "\n"+log.String())
require.True(t, yielded)
}

View File

@@ -39,6 +39,9 @@ func NewFakeNanotime() *sys.Nanotime {
// FakeNanosleep implements sys.Nanosleep by returning without sleeping.
func FakeNanosleep(int64) {}
// FakeOsyield implements sys.Osyield by returning without yielding.
func FakeOsyield() {}
// Walltime implements sys.Walltime with time.Now.
//
// Note: This is only notably less efficient than it could be is reading

View File

@@ -24,6 +24,7 @@ type Context struct {
nanotime *sys.Nanotime
nanotimeResolution sys.ClockResolution
nanosleep *sys.Nanosleep
osyield *sys.Osyield
randSource io.Reader
fsc *FSContext
}
@@ -93,6 +94,11 @@ func (c *Context) Nanosleep(ns int64) {
(*(c.nanosleep))(ns)
}
// Osyield implements sys.Osyield.
func (c *Context) Osyield() {
(*(c.osyield))()
}
// FS returns the possibly empty (sysfs.UnimplementedFS) file system context.
func (c *Context) FS() *FSContext {
return c.fsc
@@ -115,7 +121,7 @@ func (eofReader) Read([]byte) (int, error) {
// DefaultContext returns Context with no values set except a possible nil fs.FS
func DefaultContext(fs sysfs.FS) *Context {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, fs); err != nil {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, fs); err != nil {
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
} else {
return sysCtx
@@ -125,6 +131,7 @@ func DefaultContext(fs sysfs.FS) *Context {
var (
_ = DefaultContext(nil) // Force panic on bug.
ns sys.Nanosleep = platform.FakeNanosleep
oy sys.Osyield = platform.FakeOsyield
)
// NewContext is a factory function which helps avoid needing to know defaults or exporting all fields.
@@ -140,6 +147,7 @@ func NewContext(
nanotime *sys.Nanotime,
nanotimeResolution sys.ClockResolution,
nanosleep *sys.Nanosleep,
osyield *sys.Osyield,
rootFS sysfs.FS,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
@@ -186,6 +194,12 @@ func NewContext(
sysCtx.nanosleep = &ns
}
if osyield != nil {
sysCtx.osyield = osyield
} else {
sysCtx.osyield = &oy
}
if rootFS != nil {
sysCtx.fsc, err = NewFSContext(stdin, stdout, stderr, rootFS)
} else {

View File

@@ -41,6 +41,7 @@ func TestDefaultSysContext(t *testing.T) {
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
testFS, // rootFS
)
require.NoError(t, err)
@@ -126,6 +127,7 @@ func TestNewContext_Args(t *testing.T) {
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // rootFS
)
if tc.expectedErr == "" {
@@ -188,6 +190,7 @@ func TestNewContext_Environ(t *testing.T) {
nil, 0, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // rootFS
)
if tc.expectedErr == "" {
@@ -236,6 +239,7 @@ func TestNewContext_Walltime(t *testing.T) {
tc.time, tc.resolution, // walltime, walltimeResolution
nil, 0, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // rootFS
)
if tc.expectedErr == "" {
@@ -284,6 +288,7 @@ func TestNewContext_Nanotime(t *testing.T) {
nil, 0, // nanotime, nanotimeResolution
tc.time, tc.resolution, // nanotime, nanotimeResolution
nil, // nanosleep
nil, // osyield
nil, // rootFS
)
if tc.expectedErr == "" {
@@ -341,8 +346,29 @@ func TestNewContext_Nanosleep(t *testing.T) {
nil, 0, // Nanosleep, NanosleepResolution
nil, 0, // Nanosleep, NanosleepResolution
&aNs, // nanosleep
nil, // osyield
nil, // rootFS
)
require.Nil(t, err)
require.Equal(t, &aNs, sysCtx.nanosleep)
}
func TestNewContext_Osyield(t *testing.T) {
var oy sys.Osyield = func() {}
sysCtx, err := NewContext(
0, // max
nil, // args
nil,
nil, // stdin
nil, // stdout
nil, // stderr
nil, // randSource
nil, 0, // Nanosleep, NanosleepResolution
nil, 0, // Nanosleep, NanosleepResolution
nil, // nanosleep
&oy, // osyield
nil, // rootFS
)
require.Nil(t, err)
require.Equal(t, &oy, sysCtx.osyield)
}

View File

@@ -124,7 +124,7 @@ Notes:
| poll_oneoff | ✅ | Rust,TinyGo,Zig |
| proc_exit | ✅ | Rust,TinyGo,Zig |
| proc_raise | 💀 | |
| sched_yield | | |
| sched_yield | | Rust |
| random_get | ✅ | Rust,TinyGo,Zig |
| sock_accept | ❌ | |
| sock_recv | ❌ | |

View File

@@ -20,3 +20,6 @@ type Nanotime func() int64
// Nanosleep puts the current goroutine to sleep for at least ns nanoseconds.
type Nanosleep func(ns int64)
// Osyield yields the processor, typically to implement spin-wait loops.
type Osyield func()