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:
20
RATIONALE.md
20
RATIONALE.md
@@ -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).
|
||||
|
||||
43
wasi/wasi.go
43
wasi/wasi.go
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user