Repository: tetratelabs/wazero
Pull request #605 (2022-05-30 05:30:07)

Implements clock ID support for getting time/res in wasi
Signed-off-by: Anuraag Agrawal <anuraaga@gmail.com>

#### Supported clock IDs

We only support clock IDs REALTIME and MONOTONIC, while some runtimes support all IDs, not all. The next version of WASI removes the other IDs anyways

https://github.com/WebAssembly/WASI/pull/197
https://github.com/bytecodealliance/wasmtime/blob/8b48ce7fb79c7e36748e2e1a90be3bdf7d65cbce/crates/wasi-common/src/snapshots/preview_1.rs#L249

#### Mockable

Monotonic is not mockable, unlike realtime. I had first toyed with the idea of having the mock return `time.Time` instead of the unixnow value itself to support both but realized that there is no way to create a static monotonic `time.Time` in Go anyways. So allowing that would probably require instead to use the current function but provide it the `clockID` parameter. This didn't feel very useful so I left it out but happy to add it if it seems to make sense.

#### clock_time_get

`precision` is ignored, I looked at the other implementations and they seem to all ignore it. This is understandable since despite the WASI docs saying this is meant to be similar to POSIX `clock_gettime`, that function doesn't accept a precision. I suppose the main target of the parameter is mobile, not server-side. Let me know if it makes sense to commit to that understanding and remove the `TODO`

https://github.com/WasmEdge/WasmEdge/blob/bee4ad9831dec02d3224b9005d69cf30e3e973f6/lib/host/wasi/clock-linux.cpp#L26
https://github.com/wasmerio/wasmer/blob/3604debecc753c1a13701559035d75b30b77d912/lib/wasi/src/syscalls/unix/mod.rs#L36
https://github.com/bytecodealliance/cap-std/blob/024a7491b28de27d4a2dc6eea9e4372ac6001508/cap-time-ext/src/monotonic_clock.rs#L30

#### clock_get_res

`clock_get_res` should return a "real" resolution of a clock, which reflects the actual hardware and OS running the code. As such, it requires a syscall, but Go doesn't provide any function for doing this.

The other runtimes use libc or direct syscall

https://github.com/bytecodealliance/cap-std/blob/024a7491b28de27d4a2dc6eea9e4372ac6001508/cap-time-ext/src/monotonic_clock.rs#L2
https://github.com/WasmEdge/WasmEdge/blob/f77e67dfb298576b41eb7874df4f399a0de0e808/test/host/wasi/wasi.cpp#L17
https://github.com/wasmerio/wasmer/blob/3604debecc753c1a13701559035d75b30b77d912/lib/wasi/src/syscalls/unix/mod.rs#L3

For now, we just return a static value, 1 micro for realtime, and 1 nano for monotonic time, assuming realtime tends to have lower precision than monotonic. I couldn't find any actual usage in stdlib of tinygo or assemblyscript to better inform this decision.


Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
index 7a0d7a63..d971bab5 100644
--- a/RATIONALE.md
+++ b/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).
diff --git a/wasi/wasi.go b/wasi/wasi.go
index 5cff3564..0c50ff4f 100644
--- a/wasi/wasi.go
+++ b/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,17 +702,29 @@ 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
-	clock := timeNowUnixNano
-	if clockVal := ctx.Value(sys.TimeNowUnixNanoKey{}); clockVal != nil {
-		clockCtx, ok := clockVal.(func() uint64)
-		if !ok {
-			panic(fmt.Errorf("unsupported clock key: %v", clockVal))
+	// 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 {
+				panic(fmt.Errorf("unsupported clock key: %v", clockVal))
+			}
+			clock = clockCtx
 		}
-		clock = clockCtx
+		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, clock()) {
+
+	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())
 }


Test patch:
diff --git a/wasi/wasi_test.go b/wasi/wasi_test.go
index 23c1543f..30ca444d 100644
--- a/wasi/wasi_test.go
+++ b/wasi/wasi_test.go
@@ -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)
-	})
+	expectedMemoryNano := []byte{
+		'?',                                    // resultResolution is after this
+		0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // little endian-encoded resolution (fixed to 1000).
+		'?', // stopped after encoding
+	}
 
-	t.Run(functionClockResGet, func(t *testing.T) {
-		maskMemory(t, testCtx, mod, len(expectedMemory))
+	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
+			},
+		},
+	}
 
-		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))
+	for _, tt := range tests {
+		tc := tt
 
-		actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
-		require.True(t, ok)
-		require.Equal(t, expectedMemory, actual)
-	})
+		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_ClockTimeGet(t *testing.T) {
+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,30 +527,125 @@ 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!
+				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
+			},
+		},
+	}
 
-		// 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))
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			maskMemory(t, testCtx, mod, len(expectedMemory))
 
-		actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
-		require.True(t, ok)
-		require.Equal(t, expectedMemory, actual)
-	})
+			errno := tc.invocation()
+			require.Zero(t, errno, ErrnoName(errno))
 
-	t.Run(functionClockTimeGet, func(t *testing.T) {
-		maskMemory(t, testCtx, mod, len(expectedMemory))
+			actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
+			require.True(t, ok)
+			require.Equal(t, expectedMemory, actual)
+		})
+	}
+}
 
-		results, err := fn.Call(testCtx, 0 /* TODO: id */, 0 /* TODO: precision */, uint64(resultTimestamp))
-		require.NoError(t, err)
-		errno := Errno(results[0]) // results[0] is the errno
-		require.Zero(t, errno, ErrnoName(errno))
+func TestSnapshotPreview1_ClockTimeGet_Monotonic(t *testing.T) {
+	resultTimestamp := uint32(1) // arbitrary offset
 
-		actual, ok := mod.Memory().Read(testCtx, 0, uint32(len(expectedMemory)))
-		require.True(t, ok)
-		require.Equal(t, expectedMemory, actual)
-	})
+	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) {