Adds a BinaryHeap of timer entries on SchedulerState. sleep() inserts an entry and parks; schedule_loop pops due entries each iteration and unparks them. When the run queue is empty but timers are pending, the OS thread sleeps until the soonest deadline. Single-threaded only; thread::sleep is fine because no other thread can wake us. The IO thread coming next will need a Condvar or pipe wakeup to break this OS-sleep early.
117 lines
3.5 KiB
Rust
117 lines
3.5 KiB
Rust
//! Timer / sleep tests. These are time-sensitive and use generous
|
|
//! tolerances — we care about ordering and "didn't return instantly /
|
|
//! didn't take forever," not microsecond-precise scheduling.
|
|
|
|
use smarm::{run, sleep, spawn};
|
|
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[test]
|
|
fn sleep_returns_after_at_least_the_requested_duration() {
|
|
run(|| {
|
|
let t0 = Instant::now();
|
|
sleep(Duration::from_millis(50));
|
|
let elapsed = t0.elapsed();
|
|
assert!(
|
|
elapsed >= Duration::from_millis(45),
|
|
"slept only {:?}, expected ≥ ~50ms",
|
|
elapsed
|
|
);
|
|
// Loose upper bound — anything wildly slow indicates a bug.
|
|
assert!(
|
|
elapsed < Duration::from_millis(500),
|
|
"slept {:?}, far longer than the 50ms request",
|
|
elapsed
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn shorter_sleep_wakes_first() {
|
|
let log: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
|
|
let l1 = log.clone();
|
|
let l2 = log.clone();
|
|
|
|
run(move || {
|
|
let h1 = spawn(move || {
|
|
sleep(Duration::from_millis(60));
|
|
l1.lock().unwrap().push(1);
|
|
});
|
|
let h2 = spawn(move || {
|
|
sleep(Duration::from_millis(20));
|
|
l2.lock().unwrap().push(2);
|
|
});
|
|
h1.join().unwrap();
|
|
h2.join().unwrap();
|
|
});
|
|
|
|
// 2 (shorter sleep) wakes before 1.
|
|
assert_eq!(*log.lock().unwrap(), vec![2, 1]);
|
|
}
|
|
|
|
#[test]
|
|
fn one_sleeping_actor_does_not_block_other_runnable_actors() {
|
|
let log: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
|
|
let l1 = log.clone();
|
|
let l2 = log.clone();
|
|
|
|
run(move || {
|
|
let h1 = spawn(move || {
|
|
sleep(Duration::from_millis(100));
|
|
l1.lock().unwrap().push(1);
|
|
});
|
|
let h2 = spawn(move || {
|
|
// Doesn't sleep. Should be able to run while h1 is parked.
|
|
for _ in 0..3 {
|
|
l2.lock().unwrap().push(2);
|
|
smarm::yield_now();
|
|
}
|
|
});
|
|
h2.join().unwrap();
|
|
h1.join().unwrap();
|
|
});
|
|
|
|
let v = log.lock().unwrap();
|
|
// h2 finishes long before h1's 100ms timer.
|
|
let h2_count = v.iter().filter(|&&x| x == 2).count();
|
|
let h1_pos = v.iter().position(|&x| x == 1);
|
|
assert_eq!(h2_count, 3);
|
|
// h1's push should land after h2 is fully done.
|
|
if let Some(p) = h1_pos {
|
|
assert!(p >= h2_count, "h1 woke before h2 finished: log = {:?}", *v);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_duration_sleep_yields_but_does_not_park_forever() {
|
|
// A zero-duration sleep should behave like yield_now: control returns
|
|
// promptly without hanging.
|
|
run(|| {
|
|
let t0 = Instant::now();
|
|
sleep(Duration::from_millis(0));
|
|
assert!(t0.elapsed() < Duration::from_millis(100));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn many_concurrent_sleepers_all_wake() {
|
|
let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
|
|
let c = counter.clone();
|
|
run(move || {
|
|
let mut handles = Vec::new();
|
|
for i in 0..20u64 {
|
|
let cc = c.clone();
|
|
handles.push(spawn(move || {
|
|
// Stagger so they don't all coalesce to the same wake.
|
|
sleep(Duration::from_millis(5 + i * 2));
|
|
cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
}));
|
|
}
|
|
for h in handles {
|
|
h.join().unwrap();
|
|
}
|
|
});
|
|
assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 20);
|
|
}
|