wasi: support monotonic / perf time (#8385)

Assisted-by: Codex:GPT-5.6 Sol

Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>
This commit is contained in:
Jiwoo Ahn authored and GitHub committed 2026-07-29 20:33:32 +09:00
1 parent bae642c196
commit 71a406898c
4 files changed
+107 -40

No files matched your search

+3 -1
View File
@@ -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"
+20 -3
View File
@@ -227,11 +227,13 @@ pub fn process_times() -> std::io::Result<ProcessTimes> {
})
}
#[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<Duration> {
.map_err(std::io::Error::from)
}
#[cfg(target_os = "wasi")]
pub fn clock_gettime(id: ClockId) -> std::io::Result<Duration> {
let mut ts = core::mem::MaybeUninit::<libc::timespec>::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<Duration> {
nix::time::clock_getres(nix_clock_id(id))
+19 -4
View File
@@ -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<Duration> {
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<Duration> {
get_clock_time(ClockId::CLOCK_MONOTONIC, vm)
}
#[cfg(target_os = "wasi")]
fn get_perf_time(vm: &VirtualMachine) -> PyResult<Duration> {
get_clock_time(ClockId::CLOCK_MONOTONIC, vm)
}
#[cfg(not(any(unix, windows, target_os = "wasi")))]
fn get_monotonic_time(vm: &VirtualMachine) -> PyResult<Duration> {
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> {
duration_since_system_now(vm)
}
+65 -32
View File
@@ -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