Implements clock ID support for getting time/res in wasi (#605)

* Implements clock ID support for getting time/res in wasi

Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>
This commit is contained in:
Anuraag Agrawal
2022-05-31 12:11:52 +09:00
committed by GitHub
parent 062bee4845
commit d54a1348c4
3 changed files with 281 additions and 50 deletions

View File

@@ -351,6 +351,26 @@ See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6a
Their semantics match when `pathLen` == the length of `path`, so in practice this difference won't matter match.
### ClockResGet
A clock's resolution is hardware and OS dependent so requires a system call to retrieve an accurate value.
Go does not provide a function for getting resolution, so without CGO we don't have an easy way to get an actual
value. For now, we return fixed values of 1us for realtime and 1ns for monotonic, assuming that realtime clocks are
often lower precision than monotonic clocks. In the future, this could be improved by having OS+arch specific assembly
to make syscalls.
For example, Go implements time.Now for linux-amd64 with this [assembly](https://github.com/golang/go/blob/f19e4001808863d2ebfe9d1975476513d030c381/src/runtime/time_linux_amd64.s).
Because retrieving resolution is not generally called often, unlike getting time, it could be appropriate to only
implement the fallback logic that does not use VDSO (executing syscalls in user mode). The syscall for clock_getres
is 229 and should be usable. https://pkg.go.dev/syscall#pkg-constants.
If implementing similar for Windows, [mingw](https://github.com/mirror/mingw-w64/blob/6a0e9165008f731bccadfc41a59719cf7c8efc02/mingw-w64-libraries/winpthreads/src/clock.c#L77
) is often a good source to find the Windows API calls that correspond
to a POSIX method.
Writing assembly would allow making syscalls without CGO, but comes with the cost that it will require implementations
across many combinations of OS and architecture.
## Signed encoding of integer global constant initializers
wazero treats integer global constant initializers signed as their interpretation is not known at declaration time. For
example, there is no signed integer [value type](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#value-types%E2%91%A0).

View File

@@ -659,7 +659,21 @@ func (a *snapshotPreview1) EnvironSizesGet(ctx context.Context, m api.Module, re
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_res_getid-clockid---errno-timestamp
// See https://linux.die.net/man/3/clock_getres
func (a *snapshotPreview1) ClockResGet(ctx context.Context, m api.Module, id uint32, resultResolution uint32) Errno {
const resolution uint64 = 1000 // ns
// We choose arbitrary resolutions here because there's no perfect alternative. For example, according to the
// source in time.go, windows monotonic resolution can be 15ms. This chooses arbitrarily 1us for wall time and
// 1ns for monotonic. See RATIONALE.md for more context.
var resolution uint64 // ns
switch id {
case clockIDRealtime:
resolution = 1000 // 1us
case clockIDMonotonic:
resolution = 1 // 1ns
default:
// Similar to many other runtimes, we only support realtime and monotonic clocks. Other types
// are slated to be removed from the next version of WASI.
return ErrnoNosys
}
// fixed for GrainLang per #271 and Swift per https://github.com/tetratelabs/wazero/issues/526#issuecomment-1134034760
if !m.Memory().WriteUint64Le(ctx, resultResolution, resolution) {
return ErrnoFault
@@ -688,9 +702,12 @@ func (a *snapshotPreview1) ClockResGet(ctx context.Context, m api.Module, id uin
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clock_time_getid-clockid-precision-timestamp---errno-timestamp
// See https://linux.die.net/man/3/clock_gettime
func (a *snapshotPreview1) ClockTimeGet(ctx context.Context, m api.Module, id uint32, precision uint64, resultTimestamp uint32) Errno {
// TODO: id and precision are currently ignored.
// Override Context when it is passed via context
// TODO: precision is currently ignored.
var val uint64
switch id {
case clockIDRealtime:
clock := timeNowUnixNano
// Override Context when it is passed via context
if clockVal := ctx.Value(sys.TimeNowUnixNanoKey{}); clockVal != nil {
clockCtx, ok := clockVal.(func() uint64)
if !ok {
@@ -698,7 +715,16 @@ func (a *snapshotPreview1) ClockTimeGet(ctx context.Context, m api.Module, id ui
}
clock = clockCtx
}
if !m.Memory().WriteUint64Le(ctx, resultTimestamp, clock()) {
val = clock()
case clockIDMonotonic:
val = uint64(time.Since(monotonicClockBase))
default:
// Similar to many other runtimes, we only support realtime and monotonic clocks. Other types
// are slated to be removed from the next version of WASI.
return ErrnoNosys
}
if !m.Memory().WriteUint64Le(ctx, resultTimestamp, val) {
return ErrnoFault
}
return ErrnoSuccess
@@ -1368,6 +1394,15 @@ const (
fdStderr = 2
)
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-clockid-enumu32
const (
clockIDRealtime = 0
clockIDMonotonic = 1
)
// monotonicClockBase uses time.Now to ensure a monotonic clock reading on all platforms via time.Since.
var monotonicClockBase = time.Now()
func timeNowUnixNano() uint64 {
return uint64(time.Now().UnixNano())
}

View File

@@ -15,6 +15,7 @@ import (
"testing"
"testing/fstest"
"testing/iotest"
"time"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
@@ -406,36 +407,116 @@ func TestSnapshotPreview1_ClockResGet(t *testing.T) {
a, mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil)
defer mod.Close(testCtx)
resultResultion := uint32(1) // arbitrary offset
expectedMemory := []byte{
'?', // resultResultion is after this
resultResolution := uint32(1) // arbitrary offset
expectedMemoryMicro := []byte{
'?', // resultResolution is after this
0xe8, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // little endian-encoded resolution (fixed to 1000).
'?', // stopped after encoding
}
t.Run("snapshotPreview1.ClockResGet", func(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory))
require.Equal(t, ErrnoSuccess, a.ClockResGet(testCtx, mod, 0, resultResultion))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
})
t.Run(functionClockResGet, func(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory))
results, err := fn.Call(testCtx, 0, uint64(resultResultion))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
require.True(t, ok)
require.Equal(t, expectedMemory, actual)
})
expectedMemoryNano := []byte{
'?', // resultResolution is after this
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // little endian-encoded resolution (fixed to 1000).
'?', // stopped after encoding
}
func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
tests := []struct {
name string
clockID uint64
expectedMemory []byte
invocation func(clockID uint64) Errno
}{
{
name: "snapshotPreview1.ClockResGet",
clockID: 0,
expectedMemory: expectedMemoryMicro,
invocation: func(clockID uint64) Errno {
return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution)
},
},
{
name: "snapshotPreview1.ClockResGet",
clockID: 1,
expectedMemory: expectedMemoryNano,
invocation: func(clockID uint64) Errno {
return a.ClockResGet(testCtx, mod, uint32(clockID), resultResolution)
},
},
{
name: functionClockResGet,
clockID: 0,
expectedMemory: expectedMemoryMicro,
invocation: func(clockID uint64) Errno {
results, err := fn.Call(testCtx, clockID, uint64(resultResolution))
require.NoError(t, err)
return Errno(results[0]) // results[0] is the errno
},
},
{
name: functionClockResGet,
clockID: 1,
expectedMemory: expectedMemoryNano,
invocation: func(clockID uint64) Errno {
results, err := fn.Call(testCtx, clockID, uint64(resultResolution))
require.NoError(t, err)
return Errno(results[0]) // results[0] is the errno
},
},
}
for _, tt := range tests {
tc := tt
t.Run(fmt.Sprintf("%v/clockID=%v", tc.name, tc.clockID), func(t *testing.T) {
maskMemory(t, testCtx, mod, len(tc.expectedMemory))
errno := tc.invocation(tc.clockID)
require.Equal(t, ErrnoSuccess, errno, ErrnoName(errno))
actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(tc.expectedMemory)))
require.True(t, ok)
require.Equal(t, tc.expectedMemory, actual)
})
}
}
func TestSnapshotPreview1_ClockResGet_Unsupported(t *testing.T) {
resultResolution := uint32(1) // arbitrary offset
_, mod, fn := instantiateModule(testCtx, t, functionClockResGet, importClockResGet, nil)
defer mod.Close(testCtx)
tests := []struct {
name string
clockID uint64
}{
{
name: "process cputime",
clockID: 2,
},
{
name: "thread cputime",
clockID: 3,
},
{
name: "undefined",
clockID: 100,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := fn.Call(testCtx, tc.clockID, uint64(resultResolution))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, ErrnoNosys, errno, ErrnoName(errno))
})
}
}
func TestSnapshotPreview1_ClockTimeGet_Realtime(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
expectedMemory := []byte{
'?', // resultTimestamp is after this
@@ -446,24 +527,34 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
a, mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
t.Run("snapshotPreview1.ClockTimeGet", func(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory))
tests := []struct {
name string
invocation func() Errno
}{
{
name: "snapshotPreview1.ClockTimeGet",
invocation: func() Errno {
// invoke ClockTimeGet directly and check the memory side effects!
errno := a.ClockTimeGet(testCtx, mod, 0 /* TODO: id */, 0 /* TODO: precision */, resultTimestamp)
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)
})
t.Run(functionClockTimeGet, func(t *testing.T) {
maskMemory(t, testCtx, mod, len(expectedMemory))
results, err := fn.Call(testCtx, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp))
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)))
@@ -471,6 +562,91 @@ func TestSnapshotPreview1_ClockTimeGet(t *testing.T) {
require.Equal(t, expectedMemory, actual)
})
}
}
func TestSnapshotPreview1_ClockTimeGet_Monotonic(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
a, mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
tests := []struct {
name string
invocation func() Errno
}{
{
name: "snapshotPreview1.ClockTimeGet",
invocation: func() Errno {
return a.ClockTimeGet(testCtx, mod, 1 /* MONOTONIC */, 0 /* TODO: precision */, resultTimestamp)
},
},
{
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
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
errno := tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
start, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
require.True(t, ok)
time.Sleep(1 * time.Millisecond)
errno = tc.invocation()
require.Zero(t, errno, ErrnoName(errno))
end, ok := mod.Memory().ReadUint64Le(testCtx, resultTimestamp)
require.True(t, ok)
// Time is monotonic
require.True(t, end > start)
})
}
}
func TestSnapshotPreview1_ClockTimeGet_Unsupported(t *testing.T) {
resultTimestamp := uint32(1) // arbitrary offset
_, mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)
defer mod.Close(testCtx)
tests := []struct {
name string
clockID uint64
}{
{
name: "process cputime",
clockID: 2,
},
{
name: "thread cputime",
clockID: 3,
},
{
name: "undefined",
clockID: 100,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := fn.Call(testCtx, tc.clockID, 0 /* TODO: precision */, uint64(resultTimestamp))
require.NoError(t, err)
errno := Errno(results[0]) // results[0] is the errno
require.Equal(t, ErrnoNosys, errno, ErrnoName(errno))
})
}
}
func TestSnapshotPreview1_ClockTimeGet_Errors(t *testing.T) {
_, mod, fn := instantiateModule(testCtx, t, functionClockTimeGet, importClockTimeGet, nil)