use alloc::vec::Vec;
use core::task::Waker;
#[cfg(target_arch = "wasm32")]
use crate::time::{Driver, Instant};
#[cfg(target_arch = "wasm32")]
use core::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_arch = "wasm32")]
struct Registry {
lock: AtomicBool,
entries: core::cell::UnsafeCell<Vec<(u64, Waker)>>,
}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for Registry {}
#[cfg(target_arch = "wasm32")]
impl Registry {
const fn new() -> Self {
Self {
lock: AtomicBool::new(false),
entries: core::cell::UnsafeCell::new(Vec::new()),
}
}
fn with<R>(&self, body: impl FnOnce(&mut Vec<(u64, Waker)>) -> R) -> R {
while self
.lock
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
core::hint::spin_loop();
}
let entries = unsafe { &mut *self.entries.get() };
let result = body(entries);
self.lock.store(false, Ordering::Release);
result
}
}
#[cfg(target_arch = "wasm32")]
static REGISTRY: Registry = Registry::new();
#[cfg_attr(
not(target_arch = "wasm32"),
allow(dead_code) // exercised by host unit tests + the wasm-gated Driver impl
)]
fn drain_due(entries: &mut Vec<(u64, Waker)>, now_micros: u64) -> Vec<Waker> {
let mut due = Vec::new();
entries.retain(|(deadline, waker)| {
if *deadline <= now_micros {
due.push(waker.clone());
false
} else {
true
}
});
due
}
#[cfg_attr(
not(target_arch = "wasm32"),
allow(dead_code) // see drain_due
)]
fn next_deadline(entries: &[(u64, Waker)]) -> Option<u64> {
entries.iter().map(|(deadline, _)| *deadline).min()
}
#[cfg(target_arch = "wasm32")]
unsafe extern "C" {
fn proxima_time_now_micros() -> u64;
fn proxima_time_request_wake(delay_micros: u64);
}
#[cfg(target_arch = "wasm32")]
pub static DRIVER: WasmDriver = WasmDriver;
#[cfg(target_arch = "wasm32")]
pub struct WasmDriver;
#[cfg(target_arch = "wasm32")]
impl Driver for WasmDriver {
fn now(&self) -> Instant {
let micros = unsafe { proxima_time_now_micros() };
Instant::from_monotonic(core::time::Duration::from_micros(micros))
}
fn schedule_wake(&self, deadline: Instant, waker: Waker) {
let deadline_micros = deadline.into_monotonic().as_micros() as u64;
let now_micros = unsafe { proxima_time_now_micros() };
REGISTRY.with(|entries| entries.push((deadline_micros, waker)));
unsafe { proxima_time_request_wake(deadline_micros.saturating_sub(now_micros)) };
}
}
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub extern "C" fn proxima_time_fire() {
let now_micros = unsafe { proxima_time_now_micros() };
let due = REGISTRY.with(|entries| drain_due(entries, now_micros));
for waker in due {
waker.wake();
}
if let Some(next) = REGISTRY.with(|entries| next_deadline(entries)) {
unsafe { proxima_time_request_wake(next.saturating_sub(now_micros)) };
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::task::{RawWaker, RawWakerVTable, Waker};
fn noop_waker() -> Waker {
fn clone(_: *const ()) -> RawWaker {
RawWaker::new(core::ptr::null(), &VTABLE)
}
fn noop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) }
}
#[test]
fn drain_due_returns_elapsed_and_keeps_pending() {
let mut entries = alloc::vec![(100_u64, noop_waker()), (300, noop_waker())];
let woken = drain_due(&mut entries, 200);
assert_eq!(woken.len(), 1);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, 300);
}
#[test]
fn drain_due_at_exact_deadline_is_elapsed() {
let mut entries = alloc::vec![(250_u64, noop_waker())];
let woken = drain_due(&mut entries, 250);
assert_eq!(woken.len(), 1);
assert!(entries.is_empty());
}
#[test]
fn next_deadline_is_the_minimum_pending() {
let entries = alloc::vec![
(900_u64, noop_waker()),
(400, noop_waker()),
(700, noop_waker())
];
assert_eq!(next_deadline(&entries), Some(400));
}
#[test]
fn next_deadline_is_none_when_empty() {
let entries: Vec<(u64, Waker)> = Vec::new();
assert_eq!(next_deadline(&entries), None);
}
}