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:
26
RATIONALE.md
26
RATIONALE.md
@@ -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
|
||||
|
||||
20
config.go
20
config.go
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 | ❌ | |
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user