use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Instant;
use wasmtime::{Caller, Linker};
use crate::async_dispatch::DEADLINE_NEAR_WINDOW;
pub const SCHEDULER_MODULE: &str = "wasi:scheduler/host@0.1.0";
pub const FN_YIELD: &str = "yield";
pub const FN_DEADLINE_REMAINING_MS: &str = "deadline-remaining-ms";
pub const YIELD_CODE_CONTINUE: u32 = 0;
pub const YIELD_CODE_DEADLINE_APPROACHING: u32 = 1;
pub const YIELD_CODE_STOP: u32 = 2;
pub const SUGGESTED_YIELD_THRESHOLD_MS: u64 = 10;
#[derive(Debug, Clone)]
pub struct SchedulerContext {
started_at: Instant,
deadline_ms: Option<u32>,
yield_count: Arc<AtomicU32>,
bp_deadline_instant: Option<Instant>,
}
impl SchedulerContext {
pub fn new(deadline_ms: Option<u32>) -> Self {
Self {
started_at: Instant::now(),
deadline_ms,
yield_count: Arc::new(AtomicU32::new(0)),
bp_deadline_instant: None,
}
}
pub fn with_bp_deadline_instant(mut self, deadline: Option<Instant>) -> Self {
self.bp_deadline_instant = deadline;
self
}
pub fn set_bp_deadline_instant(&mut self, deadline: Option<Instant>) {
self.bp_deadline_instant = deadline;
}
pub fn bp_deadline_instant(&self) -> Option<Instant> {
self.bp_deadline_instant
}
pub fn unbounded() -> Self {
Self::new(None)
}
pub fn rearm(&mut self) {
self.started_at = Instant::now();
}
pub fn rearm_with_instant(&mut self, deadline: Option<Instant>) {
self.started_at = Instant::now();
self.bp_deadline_instant = deadline;
}
pub fn set_deadline_ms(&mut self, deadline_ms: Option<u32>) {
self.deadline_ms = deadline_ms;
}
pub fn yield_now(&self) -> u32 {
self.yield_count.fetch_add(1, Ordering::Relaxed);
if let Some(d) = self.bp_deadline_instant {
let now = Instant::now();
return if now >= d {
YIELD_CODE_STOP
} else if d.saturating_duration_since(now) <= DEADLINE_NEAR_WINDOW {
YIELD_CODE_DEADLINE_APPROACHING
} else {
YIELD_CODE_CONTINUE
};
}
match self.deadline_ms {
None => YIELD_CODE_CONTINUE,
Some(total) => {
let elapsed_ms = self.started_at.elapsed().as_millis() as u64;
let total_u64 = total as u64;
let remaining = total_u64.saturating_sub(elapsed_ms);
if remaining == 0 {
YIELD_CODE_STOP
} else if remaining < SUGGESTED_YIELD_THRESHOLD_MS {
YIELD_CODE_DEADLINE_APPROACHING
} else {
YIELD_CODE_CONTINUE
}
}
}
}
pub fn deadline_remaining_ms(&self) -> u32 {
if let Some(d) = self.bp_deadline_instant {
let now = Instant::now();
let remaining = d.saturating_duration_since(now);
let remaining_ms = remaining.as_millis();
return u32::try_from(remaining_ms).unwrap_or(u32::MAX.saturating_sub(1));
}
match self.deadline_ms {
None => u32::MAX,
Some(total) => {
let elapsed_ms = self.started_at.elapsed().as_millis() as u64;
let remaining = (total as u64).saturating_sub(elapsed_ms);
u32::try_from(remaining).unwrap_or(u32::MAX.saturating_sub(1))
}
}
}
pub fn yield_count(&self) -> u32 {
self.yield_count.load(Ordering::Relaxed)
}
pub fn deadline_ms(&self) -> Option<u32> {
self.deadline_ms
}
}
impl Default for SchedulerContext {
fn default() -> Self {
Self::unbounded()
}
}
pub fn add_scheduler_to_linker<T>(
linker: &mut Linker<T>,
get_ctx: impl Fn(&T) -> &SchedulerContext + Send + Sync + Copy + 'static,
) -> wasmtime::Result<()>
where
T: 'static,
{
linker.func_wrap(
SCHEDULER_MODULE,
FN_YIELD,
move |caller: Caller<'_, T>| -> u32 {
get_ctx(caller.data()).yield_now()
},
)?;
linker.func_wrap(
SCHEDULER_MODULE,
FN_DEADLINE_REMAINING_MS,
move |caller: Caller<'_, T>| -> u32 { get_ctx(caller.data()).deadline_remaining_ms() },
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn unbounded_yield_always_continue() {
let ctx = SchedulerContext::unbounded();
for _ in 0..16 {
assert_eq!(ctx.yield_now(), YIELD_CODE_CONTINUE);
}
}
#[test]
fn unbounded_remaining_is_max() {
let ctx = SchedulerContext::unbounded();
assert_eq!(ctx.deadline_remaining_ms(), u32::MAX);
}
#[test]
fn yield_count_increments() {
let ctx = SchedulerContext::new(Some(1_000_000));
assert_eq!(ctx.yield_count(), 0);
for i in 1..=5 {
ctx.yield_now();
assert_eq!(ctx.yield_count(), i);
}
}
#[test]
fn yield_count_shared_across_clones() {
let a = SchedulerContext::new(Some(1_000_000));
let b = a.clone();
a.yield_now();
b.yield_now();
a.yield_now();
assert_eq!(a.yield_count(), 3);
assert_eq!(b.yield_count(), 3);
}
#[test]
fn expired_deadline_returns_stop() {
let ctx = SchedulerContext::new(Some(1));
thread::sleep(Duration::from_millis(15));
assert_eq!(ctx.yield_now(), YIELD_CODE_STOP);
}
#[test]
fn near_deadline_returns_approaching() {
let ctx = SchedulerContext::new(Some(5));
let code = ctx.yield_now();
assert!(
code == YIELD_CODE_DEADLINE_APPROACHING || code == YIELD_CODE_STOP,
"expected APPROACHING or STOP, got {code}"
);
}
#[test]
fn fresh_long_deadline_returns_continue() {
let ctx = SchedulerContext::new(Some(60_000));
assert_eq!(ctx.yield_now(), YIELD_CODE_CONTINUE);
}
#[test]
fn deadline_remaining_ms_decreases() {
let ctx = SchedulerContext::new(Some(200));
let before = ctx.deadline_remaining_ms();
thread::sleep(Duration::from_millis(50));
let after = ctx.deadline_remaining_ms();
assert!(
after < before,
"remaining should strictly decrease over a 50 ms sleep: before={before}, after={after}"
);
assert!(after < 200);
}
#[test]
fn deadline_remaining_ms_saturates_to_zero() {
let ctx = SchedulerContext::new(Some(1));
thread::sleep(Duration::from_millis(15));
assert_eq!(ctx.deadline_remaining_ms(), 0);
}
#[test]
fn rearm_resets_elapsed_window() {
let mut ctx = SchedulerContext::new(Some(50));
thread::sleep(Duration::from_millis(30));
ctx.rearm();
let remaining = ctx.deadline_remaining_ms();
assert!(
remaining > 30,
"after rearm remaining should be near full budget, got {remaining}"
);
}
#[test]
fn set_deadline_ms_swaps_budget() {
let mut ctx = SchedulerContext::new(None);
assert_eq!(ctx.deadline_remaining_ms(), u32::MAX);
ctx.set_deadline_ms(Some(100));
assert!(ctx.deadline_remaining_ms() <= 100);
ctx.set_deadline_ms(None);
assert_eq!(ctx.deadline_remaining_ms(), u32::MAX);
}
#[test]
fn default_is_unbounded() {
let ctx = SchedulerContext::default();
assert_eq!(ctx.deadline_ms(), None);
assert_eq!(ctx.yield_now(), YIELD_CODE_CONTINUE);
}
#[test]
fn bp_deadline_instant_drives_yield_now() {
let ctx = SchedulerContext::new(None)
.with_bp_deadline_instant(Some(Instant::now() + Duration::from_secs(60)));
assert_eq!(ctx.yield_now(), YIELD_CODE_CONTINUE);
let ctx = SchedulerContext::new(None)
.with_bp_deadline_instant(Some(Instant::now() + Duration::from_millis(30)));
let code = ctx.yield_now();
assert!(
code == YIELD_CODE_DEADLINE_APPROACHING || code == YIELD_CODE_STOP,
"expected APPROACHING or STOP near the deadline, got {code}"
);
let ctx = SchedulerContext::new(None)
.with_bp_deadline_instant(Some(Instant::now() - Duration::from_millis(5)));
assert_eq!(ctx.yield_now(), YIELD_CODE_STOP);
}
#[test]
fn bp_deadline_instant_drives_remaining_ms() {
let ctx = SchedulerContext::new(None)
.with_bp_deadline_instant(Some(Instant::now() + Duration::from_millis(200)));
let r = ctx.deadline_remaining_ms();
assert!(r > 0 && r <= 200, "remaining {r} not in (0, 200]");
let ctx = SchedulerContext::new(None)
.with_bp_deadline_instant(Some(Instant::now() - Duration::from_millis(50)));
assert_eq!(ctx.deadline_remaining_ms(), 0);
}
#[test]
fn bp_deadline_instant_takes_precedence_over_ms() {
let ctx = SchedulerContext::new(Some(1_000_000))
.with_bp_deadline_instant(Some(Instant::now() - Duration::from_millis(5)));
assert_eq!(ctx.yield_now(), YIELD_CODE_STOP);
assert_eq!(ctx.deadline_remaining_ms(), 0);
}
#[test]
fn rearm_with_instant_swaps_both_fields() {
let mut ctx = SchedulerContext::new(Some(50));
assert!(ctx.bp_deadline_instant().is_none());
let d = Instant::now() + Duration::from_millis(500);
ctx.rearm_with_instant(Some(d));
assert_eq!(ctx.bp_deadline_instant(), Some(d));
assert_eq!(ctx.yield_now(), YIELD_CODE_CONTINUE);
}
}