From bf8bca8039afaa7011881a5183797cbeef1c7f98 Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Wed, 29 Jul 2026 20:33:32 +0900 Subject: [PATCH] wasi: support monotonic / perf time (#8385) Assisted-by: Codex:GPT-5.6 Sol Signed-off-by: Jiwoo Ahn --- .github/workflows/ci.yaml | 4 +- crates/host_env/src/time.rs | 23 ++++++- crates/vm/src/stdlib/time.rs | 23 +++++-- extra_tests/snippets/stdlib_time.py | 97 +++++++++++++++++++---------- 4 files changed, 107 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2498abf1..10541d1b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -801,7 +801,9 @@ jobs: - name: build rustpython run: cargo build --profile wasm-release --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib,stdio,importlib,host_env --verbose - name: run snippets - run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" + run: | + wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" + wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_time.py" - name: run cpython unittest run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/Lib/test/test_int.py" diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index 451e48840..cc0b7d30c 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -227,11 +227,13 @@ pub fn process_times() -> std::io::Result { }) } -#[cfg(unix)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[cfg(any(unix, target_os = "wasi"))] +#[derive(Copy, Clone, Debug)] +// WASI libc represents clockid_t as an opaque pointer type without Eq or PartialEq. +#[cfg_attr(unix, derive(Eq, PartialEq))] pub struct ClockId(libc::clockid_t); -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] impl ClockId { pub const fn from_raw(raw: libc::clockid_t) -> Self { Self(raw) @@ -259,6 +261,7 @@ impl ClockId { target_os = "solaris", target_os = "openbsd", target_os = "redox", + target_os = "wasi", )))] pub const CLOCK_THREAD_CPUTIME_ID: Self = Self(libc::CLOCK_THREAD_CPUTIME_ID); } @@ -275,6 +278,20 @@ pub fn clock_gettime(id: ClockId) -> std::io::Result { .map_err(std::io::Error::from) } +#[cfg(target_os = "wasi")] +pub fn clock_gettime(id: ClockId) -> std::io::Result { + let mut ts = core::mem::MaybeUninit::::uninit(); + + let ret = unsafe { libc::clock_gettime(id.as_raw(), ts.as_mut_ptr()) }; + if ret != 0 { + return Err(std::io::Error::last_os_error()); + } + + let ts = unsafe { ts.assume_init() }; + + Ok(Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)) +} + #[cfg(all(unix, not(target_os = "redox")))] pub fn clock_getres(id: ClockId) -> std::io::Result { nix::time::clock_getres(nix_clock_id(id)) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index aaac706bf..4c9fd70c4 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -31,6 +31,8 @@ mod decl { naive::{NaiveDate, NaiveDateTime, NaiveTime}, }; use core::time::Duration; + #[cfg(target_os = "wasi")] + use rustpython_host_env::time::ClockId; #[cfg(any(unix, windows))] use rustpython_host_env::time::asctime_from_tm; use rustpython_host_env::time::{self as host_time}; @@ -60,14 +62,27 @@ mod decl { #[pyattr] pub const _STRUCT_TM_ITEMS: usize = 11; - // TODO: implement proper monotonic time for wasm/wasi. - #[cfg(not(any(unix, windows)))] + #[cfg(target_os = "wasi")] + fn get_clock_time(id: ClockId, vm: &VirtualMachine) -> PyResult { + host_time::clock_gettime(id).map_err(|err| vm.new_os_error(err.to_string())) + } + + #[cfg(target_os = "wasi")] + fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { + get_clock_time(ClockId::CLOCK_MONOTONIC, vm) + } + + #[cfg(target_os = "wasi")] + fn get_perf_time(vm: &VirtualMachine) -> PyResult { + get_clock_time(ClockId::CLOCK_MONOTONIC, vm) + } + + #[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_monotonic_time(vm: &VirtualMachine) -> PyResult { duration_since_system_now(vm) } - // TODO: implement proper perf time for wasm/wasi. - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] fn get_perf_time(vm: &VirtualMachine) -> PyResult { duration_since_system_now(vm) } diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index 1629443a9..68ceab895 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -1,3 +1,4 @@ +import sys import time x = time.gmtime(1000) @@ -11,41 +12,73 @@ s = time.strftime("%Y-%m-%d-%H-%M-%S", x) # print(s) assert s == "1970-01-01-00-16-40" -x2 = time.strptime(s, "%Y-%m-%d-%H-%M-%S") -assert x2.tm_min == 16 +if sys.platform != "wasi": + # _strptime depends on time.tzname, which is not available on WASI yet. + x2 = time.strptime(s, "%Y-%m-%d-%H-%M-%S") + assert x2.tm_min == 16 + + # TODO: WASI currently does not raise OverflowError for some out-of-range + # struct_time values in asctime() and strftime(). + # Re-enable this regression on WASI once the non-Unix time conversion path is fixed. + + # Regression test for RustPython issue #4938: + # struct_time field overflow should raise OverflowError (matching CPython), + # not TypeError. Covers mktime, asctime, and strftime. + I32_MAX_PLUS_1 = 2147483648 + overflow_cases = [ + (I32_MAX_PLUS_1, 1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in year + (2024, I32_MAX_PLUS_1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in month + (2024, 1, I32_MAX_PLUS_1, 0, 0, 0, 0, 0, 0), # i32 overflow in mday + (2024, 1, 1, 0, 0, I32_MAX_PLUS_1, 0, 0, 0), # i32 overflow in sec + (88888888888,) * 9, # multi-field i32 overflow + ] + + for case in overflow_cases: + for func_name, call in [ + ("mktime", lambda c=case: time.mktime(c)), + ("asctime", lambda c=case: time.asctime(c)), + ("strftime", lambda c=case: time.strftime("%Y", c)), + ]: + try: + call() + except OverflowError: + pass # expected, matches CPython + except TypeError as e: + raise AssertionError( + f"{func_name}({case}) raised TypeError (should be OverflowError): {e}" + ) from e + else: + raise AssertionError( + f"{func_name}({case}) did not raise — expected OverflowError" + ) s = time.asctime(x) -# print(s) assert s == "Thu Jan 1 00:16:40 1970" +# Monotonic and performance clocks should advance with elapsed time. +monotonic_before = time.monotonic() +monotonic_ns = time.monotonic_ns() +monotonic_after = time.monotonic() -# Regression test for RustPython issue #4938: -# struct_time field overflow should raise OverflowError (matching CPython), -# not TypeError. Covers mktime, asctime, and strftime. -I32_MAX_PLUS_1 = 2147483648 -overflow_cases = [ - (I32_MAX_PLUS_1, 1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in year - (2024, I32_MAX_PLUS_1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in month - (2024, 1, I32_MAX_PLUS_1, 0, 0, 0, 0, 0, 0), # i32 overflow in mday - (2024, 1, 1, 0, 0, I32_MAX_PLUS_1, 0, 0, 0), # i32 overflow in sec - (88888888888,) * 9, # multi-field i32 overflow -] +assert isinstance(monotonic_before, float) +assert isinstance(monotonic_ns, int) +assert monotonic_before <= monotonic_ns / 1_000_000_000 <= monotonic_after -for case in overflow_cases: - for func_name, call in [ - ("mktime", lambda c=case: time.mktime(c)), - ("asctime", lambda c=case: time.asctime(c)), - ("strftime", lambda c=case: time.strftime("%Y", c)), - ]: - try: - call() - except OverflowError: - pass # expected, matches CPython - except TypeError as e: - raise AssertionError( - f"{func_name}({case}) raised TypeError (should be OverflowError): {e}" - ) from e - else: - raise AssertionError( - f"{func_name}({case}) did not raise — expected OverflowError" - ) +perf_before = time.perf_counter() +perf_ns = time.perf_counter_ns() +perf_after = time.perf_counter() + +assert isinstance(perf_before, float) +assert isinstance(perf_ns, int) +assert perf_before <= perf_ns / 1_000_000_000 <= perf_after + +monotonic_start = time.monotonic() +perf_start = time.perf_counter() + +time.sleep(0.02) + +monotonic_elapsed = time.monotonic() - monotonic_start +perf_elapsed = time.perf_counter() - perf_start + +assert monotonic_elapsed >= 0.01 +assert perf_elapsed >= 0.01