use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::future::Future;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, Weak};
use std::task::{Context, Poll, Wake, Waker};
use std::thread::ThreadId;
use std::time::Duration;
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
const ROOT: u64 = 0;
const DEFAULT_MAX_STEPS: u64 = 10_000_000;
const DEFAULT_SPIN_LIMIT: u64 = 10_000;
const SPIN_QUANTUM: Duration = Duration::from_millis(1);
const CREEPS_PER_HALVING: u64 = 100;
const DEFAULT_POLL_TIMEOUT_SECS: u64 = 60;
pub mod explore;
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(|p| p.into_inner())
}
fn splitmix(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn by_rank(ready: &[u64], rank: usize) -> usize {
let mut ids = ready.to_vec();
let (_, id, _) = ids.select_nth_unstable(rank);
let id = *id;
ready.iter().position(|r| *r == id).unwrap_or(0)
}
mod watchdog {
use super::{lock, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use std::time::{Duration, Instant};
pub(super) struct Watch {
seed: u64,
limit: u64,
since: AtomicU64,
task: AtomicU64,
}
fn base() -> Instant {
static BASE: OnceLock<Instant> = OnceLock::new();
*BASE.get_or_init(Instant::now)
}
fn limit() -> u64 {
std::env::var("SPAWNED_SIM_POLL_TIMEOUT")
.ok()
.and_then(|v| v.trim().parse().ok())
.unwrap_or(super::DEFAULT_POLL_TIMEOUT_SECS)
}
static WATCHED: Mutex<Vec<Weak<Watch>>> = Mutex::new(Vec::new());
impl Watch {
pub(super) fn register(seed: u64) -> Arc<Self> {
let watch = Arc::new(Self {
seed,
limit: limit(),
since: AtomicU64::new(0),
task: AtomicU64::new(0),
});
if watch.limit > 0 {
static STARTED: OnceLock<()> = OnceLock::new();
STARTED.get_or_init(|| {
base();
std::thread::Builder::new()
.name("spawned-sim-watchdog".into())
.spawn(run)
.expect("spawned_sim: cannot start the poll watchdog");
});
let mut watched = lock(&WATCHED);
watched.retain(|w| w.strong_count() > 0);
watched.push(Arc::downgrade(&watch));
}
watch
}
pub(super) fn busy(&self, task: u64) -> Busy<'_> {
if self.limit > 0 {
self.task.store(task, Ordering::Relaxed);
let now = base().elapsed().as_millis() as u64;
self.since.store(now.max(1), Ordering::Release);
}
Busy(self)
}
}
pub(super) struct Busy<'a>(&'a Watch);
impl Drop for Busy<'_> {
fn drop(&mut self) {
if self.0.limit > 0 {
self.0.since.store(0, Ordering::Release);
}
}
}
fn run() {
loop {
std::thread::sleep(Duration::from_millis(200));
let now = base().elapsed().as_millis() as u64;
for watch in lock(&WATCHED).iter().filter_map(Weak::upgrade) {
let since = watch.since.load(Ordering::Acquire);
let task = watch.task.load(Ordering::Relaxed);
let same = watch.since.load(Ordering::Acquire) == since;
if since != 0 && same && now.saturating_sub(since) > watch.limit * 1000 {
eprintln!(
"spawned_sim: task {task} has been inside one poll for over {} s \
(seed {}): a loop with no await point never returns to the \
scheduler; aborting (SPAWNED_SIM_POLL_TIMEOUT sets the limit, \
0 disables the watchdog)",
watch.limit, watch.seed
);
std::process::abort();
}
}
}
}
}
struct State {
seed: u64,
rng: u64,
now: Duration,
next_id: u64,
tasks: BTreeMap<u64, Arc<Mutex<Option<BoxTask>>>>,
ready: Vec<u64>,
queued: BTreeSet<u64>,
timers: BTreeMap<(Duration, u64), Waker>,
next_timer: u64,
steps: u64,
spin: u64,
spin_limit: u64,
streak: u64,
hash: u64,
trace: Option<Vec<(u64, Duration)>>,
tape: Option<Vec<Decision>>,
script: Option<Script>,
plan: Option<explore::Plan>,
entropy: u64,
foreign: Option<String>,
running: bool,
runtimes: usize,
tokio_seed: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Canonical(Option<u64>),
Poll(u64),
Fire(Duration),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayMode {
Strict,
Permissive,
}
struct Script {
decisions: Vec<Decision>,
pos: usize,
mode: ReplayMode,
misses: u64,
}
enum Action {
Poll(usize),
Fire(Duration),
}
fn scripted(state: &mut State) -> Result<Option<Action>, String> {
let State {
script,
ready,
timers,
now,
..
} = state;
let now = *now;
let script = script.as_mut().expect("strict replay without a script");
let at = script.pos;
let mut ids = ready.clone();
ids.sort_unstable();
let action = match script.decisions.get(at).copied() {
None if ready.is_empty() && timers.is_empty() => return Ok(None),
None => {
return Err(format!(
"the tape ended after {at} decisions but the run continues (ready: {ids:?})"
))
}
Some(Decision::Fire(deadline)) => {
let earliest = timers.keys().next().map(|(at, _)| *at);
match earliest {
None => {
return Err(format!(
"decision {at} advances the clock to {deadline:?}, but no timer is pending"
))
}
Some(_) if deadline < now => {
return Err(format!(
"decision {at} moves the clock back to {deadline:?} from {now:?}"
))
}
Some(earliest) if deadline > earliest => {
return Err(format!(
"decision {at} advances the clock to {deadline:?}, past the earliest \
pending timer at {earliest:?}"
))
}
Some(earliest) if ready.is_empty() && deadline != earliest => {
return Err(format!(
"decision {at} creeps the clock to {deadline:?} while no task is \
ready; the recorded run would have jumped to {earliest:?}"
))
}
Some(_) => Action::Fire(deadline),
}
}
Some(_) if ready.is_empty() => {
return Err(format!(
"decision {at} polls a task but none is ready (the recorded run had one ready)"
))
}
Some(Decision::Canonical(id)) => {
let index = canonical(ready);
match id {
Some(id) if ready[index] != id => {
return Err(format!(
"decision {at} polls task {id} as the canonical choice, but the ready \
tasks are {ids:?}"
))
}
_ => Action::Poll(index),
}
}
Some(Decision::Poll(id)) => match ready.iter().position(|r| *r == id) {
Some(index) if index != canonical(ready) => Action::Poll(index),
_ => {
return Err(format!(
"decision {at} polls task {id} as a non-canonical choice, but the ready \
tasks are {ids:?}"
))
}
},
};
script.pos += 1;
Ok(Some(action))
}
fn canonical(ready: &[u64]) -> usize {
ready
.iter()
.enumerate()
.min_by_key(|(_, id)| **id)
.map_or(0, |(index, _)| index)
}
fn choose(state: &mut State) -> usize {
let State {
script,
plan,
ready,
rng,
..
} = state;
match script {
Some(script) => {
while let Some(Decision::Fire(_)) = script.decisions.get(script.pos) {
script.pos += 1;
}
let pick = script.decisions.get(script.pos).copied();
script.pos += 1;
match pick {
Some(Decision::Poll(id)) => {
ready.iter().position(|r| *r == id).unwrap_or_else(|| {
script.misses += 1;
canonical(ready)
})
}
_ => canonical(ready),
}
}
None => match plan {
Some(plan) => plan.pick(ready),
None => by_rank(ready, (splitmix(rng) % ready.len() as u64) as usize),
},
}
}
struct Shared {
owner: ThreadId,
max_steps: u64,
state: Mutex<State>,
tokio: Mutex<Option<Arc<::tokio::runtime::Runtime>>>,
watch: Arc<watchdog::Watch>,
}
impl Drop for Shared {
fn drop(&mut self) {
self.shutdown_tokio();
}
}
impl Shared {
fn new(seed: u64) -> Arc<Self> {
let max_steps = std::env::var("SPAWNED_SIM_MAX_STEPS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MAX_STEPS);
Arc::new(Self {
owner: std::thread::current().id(),
max_steps,
tokio: Mutex::new(None),
watch: watchdog::Watch::register(seed),
state: Mutex::new(State {
spin_limit: std::env::var("SPAWNED_SIM_SPIN")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_SPIN_LIMIT),
streak: 0,
seed,
rng: seed,
now: Duration::ZERO,
next_id: ROOT + 1,
tasks: BTreeMap::new(),
ready: Vec::new(),
queued: BTreeSet::new(),
timers: BTreeMap::new(),
next_timer: 0,
steps: 0,
spin: 0,
hash: 0xcbf2_9ce4_8422_2325,
trace: None,
tape: None,
script: None,
plan: None,
entropy: 0,
foreign: None,
running: false,
runtimes: 0,
tokio_seed: seed,
}),
})
}
fn run_poll<R>(&self, id: u64, f: impl FnOnce() -> R) -> R {
let runtime = self.tokio_runtime();
let spawned = runtime.metrics().spawned_tasks_count();
let escaped = |runtime: &::tokio::runtime::Runtime| {
let metrics = runtime.metrics();
metrics.spawned_tasks_count() != spawned
|| metrics.num_blocking_threads() > 0
|| metrics.blocking_queue_depth() > 0
};
struct Check<'a, E: Fn(&::tokio::runtime::Runtime) -> bool> {
shared: &'a Shared,
runtime: &'a ::tokio::runtime::Runtime,
escaped: E,
id: u64,
}
impl<E: Fn(&::tokio::runtime::Runtime) -> bool> Drop for Check<'_, E> {
fn drop(&mut self) {
if (self.escaped)(self.runtime) {
let mut state = lock(&self.shared.state);
if state.foreign.is_none() {
state.foreign = Some(format!(
"task {} handed work to Tokio directly (tokio::spawn or \
tokio::task::spawn_blocking); use spawned_rt::tasks::spawn",
self.id
));
}
}
}
}
let _check = Check {
shared: self,
runtime: &runtime,
escaped,
id,
};
let mut f = Some(f);
let mut out = None;
let _busy = self.watch.busy(id);
runtime.block_on(std::future::poll_fn(|cx| match f.take() {
Some(f) => {
out = Some(f());
if escaped(&runtime) {
Poll::Ready(())
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
None => Poll::Ready(()),
}));
out.expect("spawned_sim: poll did not run")
}
fn tokio_runtime(&self) -> Arc<::tokio::runtime::Runtime> {
let seed = lock(&self.state).tokio_seed;
lock(&self.tokio)
.get_or_insert_with(|| {
Arc::new(
::tokio::runtime::Builder::new_current_thread()
.rng_seed(::tokio::runtime::RngSeed::from_bytes(&seed.to_le_bytes()))
.build()
.expect("spawned_sim: cannot build the per-world Tokio runtime"),
)
})
.clone()
}
fn shutdown_tokio(&self) {
let runtime = lock(&self.tokio).take();
if let Some(runtime) = runtime {
if let Ok(runtime) = Arc::try_unwrap(runtime) {
runtime.shutdown_background();
}
}
}
fn check_tape_used(&self) {
let left = {
let state = lock(&self.state);
match &state.script {
Some(script) if script.mode == ReplayMode::Strict => {
script.decisions.len().saturating_sub(script.pos)
}
_ => 0,
}
};
if left > 0 {
panic!(
"spawned_sim: replay diverged: the run finished with {left} tape decisions unused"
);
}
}
fn check_leaks(&self) {
let foreign = lock(&self.state).foreign.take();
if let Some(foreign) = foreign {
self.shutdown_tokio();
panic!("spawned_sim: determinism leak: {foreign}");
}
}
fn schedule(&self, id: u64) {
let mut state = lock(&self.state);
if std::thread::current().id() != self.owner && state.foreign.is_none() {
let thread = std::thread::current();
state.foreign = Some(format!(
"task {id} woken from foreign thread {:?} ({})",
thread.id(),
thread.name().unwrap_or("unnamed")
));
}
if (id == ROOT || state.tasks.contains_key(&id)) && state.queued.insert(id) {
state.ready.push(id);
}
}
fn spawn_boxed(self: &Arc<Self>, id: u64, task: BoxTask) {
lock(&self.state)
.tasks
.insert(id, Arc::new(Mutex::new(Some(task))));
self.schedule(id);
}
fn reserve_id(&self) -> u64 {
let mut state = lock(&self.state);
let id = state.next_id;
state.next_id += 1;
id
}
fn next(&self) -> Option<u64> {
loop {
self.check_leaks();
let mut state = lock(&self.state);
let strict = matches!(&state.script, Some(s) if s.mode == ReplayMode::Strict);
let action = if strict {
match scripted(&mut state) {
Ok(action) => action,
Err(reason) => {
let (seed, now) = (state.seed, state.now);
drop(state);
panic!(
"spawned_sim: replay diverged: {reason} (seed {seed}, virtual time {now:?})"
);
}
}
} else {
let halvings = (state.streak / CREEPS_PER_HALVING).min(63) as u32;
let threshold = (state.spin_limit >> halvings).max(1);
let starving = state.spin >= threshold && !state.timers.is_empty();
if !state.ready.is_empty() && !starving {
Some(Action::Poll(choose(&mut state)))
} else if let Some(&(earliest, _)) = state.timers.keys().next() {
Some(Action::Fire(if starving {
state.streak += 1;
earliest.min(state.now + SPIN_QUANTUM)
} else {
state.streak = 0;
earliest
}))
} else {
None
}
};
match action? {
Action::Poll(index) => {
state.spin += 1;
let canonical = index == canonical(&state.ready);
let id = state.ready.swap_remove(index);
if let Some(tape) = &mut state.tape {
tape.push(if canonical {
Decision::Canonical(Some(id))
} else {
Decision::Poll(id)
});
}
state.queued.remove(&id);
state.steps += 1;
let now = state.now;
state.hash = (state.hash ^ id).wrapping_mul(0x0100_0000_01b3);
state.hash =
(state.hash ^ now.as_nanos() as u64).wrapping_mul(0x0100_0000_01b3);
if let Some(trace) = &mut state.trace {
trace.push((id, now));
}
if state.steps > self.max_steps {
let (seed, steps) = (state.seed, state.steps);
drop(state);
panic!(
"spawned_sim: livelock: {steps} polls without finishing (seed {seed}); \
raise SPAWNED_SIM_MAX_STEPS if this is expected"
);
}
return Some(id);
}
Action::Fire(deadline) => {
state.spin = 0;
state.now = state.now.max(deadline);
if let Some(tape) = &mut state.tape {
tape.push(Decision::Fire(deadline));
}
let mut due = Vec::new();
while let Some(entry) = state.timers.first_entry() {
if entry.key().0 > deadline {
break;
}
due.push(entry.remove());
}
drop(state);
for waker in due {
waker.wake();
}
}
}
}
}
fn poll_task(self: &Arc<Self>, id: u64) {
let Some(slot) = lock(&self.state).tasks.get(&id).cloned() else {
return;
};
let Some(mut task) = lock(&slot).take() else {
return;
};
let waker = Waker::from(Arc::new(TaskWaker {
id,
shared: Arc::downgrade(self),
}));
let done = self.run_poll(id, || {
task.as_mut()
.poll(&mut Context::from_waker(&waker))
.is_ready()
});
if done {
self.retire(id);
drop(task);
} else {
*lock(&slot) = Some(task);
}
}
fn retire(&self, id: u64) {
let mut state = lock(&self.state);
state.tasks.remove(&id);
if state.queued.remove(&id) {
state.ready.retain(|ready| *ready != id);
}
}
fn block_on<F: Future>(self: &Arc<Self>, future: F) -> F::Output {
assert_eq!(
std::thread::current().id(),
self.owner,
"spawned_sim: block_on called from a thread that does not own the simulation"
);
{
let mut state = lock(&self.state);
assert!(
!state.running,
"spawned_sim: nested block_on (for example ChildHandle::wait_exit_blocking \
or Handle::block_on inside a task) cannot run on the single simulation thread"
);
state.running = true;
}
struct Running<'a>(&'a Shared);
impl Drop for Running<'_> {
fn drop(&mut self) {
lock(&self.0.state).running = false;
}
}
let _running = Running(self);
let _context = enter(self.clone());
let mut future = Some(Box::pin(future));
let waker = Waker::from(Arc::new(TaskWaker {
id: ROOT,
shared: Arc::downgrade(self),
}));
self.schedule(ROOT);
loop {
let Some(id) = self.next() else {
let state = lock(&self.state);
let (seed, now, tasks) = (state.seed, state.now, state.tasks.len());
drop(state);
panic!(
"spawned_sim: deadlock: no ready task and no pending timer \
(seed {seed}, virtual time {now:?}, {tasks} parked tasks)"
);
};
if id == ROOT {
let polled = self.run_poll(ROOT, || {
let root = future.as_mut().expect("root polled after completion");
match root.as_mut().poll(&mut Context::from_waker(&waker)) {
Poll::Ready(output) => {
future = None;
Poll::Ready(output)
}
Poll::Pending => Poll::Pending,
}
});
if let Poll::Ready(output) = polled {
self.retire(ROOT);
self.check_leaks();
self.check_tape_used();
return output;
}
} else {
self.poll_task(id);
}
}
}
fn drain(self: &Arc<Self>) {
let _context = enter(self.clone());
for _ in 0..64 {
let tasks = std::mem::take(&mut lock(&self.state).tasks);
if tasks.is_empty() {
break;
}
for slot in tasks.into_values() {
let task = lock(&slot).take();
drop(task);
}
}
self.shutdown_tokio();
let mut state = lock(&self.state);
state.ready.clear();
state.queued.clear();
state.timers.clear();
}
}
struct TaskWaker {
id: u64,
shared: Weak<Shared>,
}
impl Wake for TaskWaker {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
if let Some(shared) = self.shared.upgrade() {
shared.schedule(self.id);
}
}
}
thread_local! {
static AMBIENT: RefCell<Weak<Shared>> = const { RefCell::new(Weak::new()) };
static CURRENT: RefCell<Vec<Arc<Shared>>> = const { RefCell::new(Vec::new()) };
}
fn ambient_or_new() -> Arc<Shared> {
AMBIENT.with(|ambient| {
if let Some(shared) = ambient.borrow().upgrade() {
return shared;
}
let seed = std::env::var("SPAWNED_SIM_SEED")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let shared = Shared::new(seed);
*ambient.borrow_mut() = Arc::downgrade(&shared);
shared
})
}
fn current() -> Option<Arc<Shared>> {
CURRENT.with(|c| c.borrow().last().cloned())
}
fn current_or_ambient() -> Arc<Shared> {
current().unwrap_or_else(ambient_or_new)
}
pub struct EnterGuard {
_private: (),
}
impl Drop for EnterGuard {
fn drop(&mut self) {
CURRENT.with(|c| c.borrow_mut().pop());
}
}
fn enter(shared: Arc<Shared>) -> EnterGuard {
CURRENT.with(|c| c.borrow_mut().push(shared));
EnterGuard { _private: () }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RuntimeFlavor {
CurrentThread,
MultiThread,
}
#[derive(Debug)]
pub struct TryCurrentError;
impl fmt::Display for TryCurrentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("no simulation is running on this thread")
}
}
impl std::error::Error for TryCurrentError {}
#[derive(Clone)]
pub struct Handle {
shared: Arc<Shared>,
}
impl fmt::Debug for Handle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("sim::Handle").finish_non_exhaustive()
}
}
impl Handle {
pub fn current() -> Self {
Self::try_current().expect("spawned_sim: Handle::current() outside the simulation")
}
pub fn try_current() -> Result<Self, TryCurrentError> {
current()
.map(|shared| Self { shared })
.ok_or(TryCurrentError)
}
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn_on(&self.shared, future)
}
pub fn spawn_blocking<F, R>(&self, _f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
unsupported("Handle::spawn_blocking")
}
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
self.shared.block_on(future)
}
pub fn runtime_flavor(&self) -> RuntimeFlavor {
RuntimeFlavor::CurrentThread
}
pub fn enter(&self) -> EnterGuard {
enter(self.shared.clone())
}
pub fn metrics(&self) -> RuntimeMetrics {
RuntimeMetrics(())
}
}
#[derive(Clone, Debug)]
pub struct RuntimeMetrics(());
impl RuntimeMetrics {
pub fn num_workers(&self) -> usize {
1
}
}
pub use ::tokio::task::unconstrained;
pub struct Runtime {
handle: Handle,
}
impl Runtime {
pub fn new() -> std::io::Result<Self> {
Ok(Self::attach(ambient_or_new()))
}
pub fn with_seed(seed: u64) -> Self {
let shared = Shared::new(seed);
AMBIENT.with(|ambient| *ambient.borrow_mut() = Arc::downgrade(&shared));
Self::attach(shared)
}
fn attach(shared: Arc<Shared>) -> Self {
lock(&shared.state).runtimes += 1;
Self {
handle: Handle { shared },
}
}
pub fn handle(&self) -> &Handle {
&self.handle
}
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
self.handle.block_on(future)
}
pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
self.handle.spawn(future)
}
pub fn enter(&self) -> EnterGuard {
self.handle.enter()
}
pub fn record_trace(&self) {
lock(&self.handle.shared.state).trace = Some(Vec::new());
}
pub fn trace(&self) -> Vec<(u64, Duration)> {
lock(&self.handle.shared.state)
.trace
.clone()
.unwrap_or_default()
}
pub fn record_tape(&self) {
lock(&self.handle.shared.state).tape = Some(Vec::new());
}
pub fn tape(&self) -> Tape {
let state = lock(&self.handle.shared.state);
Tape {
seed: state.seed,
spin: Some(state.spin_limit),
decisions: state.tape.clone().unwrap_or_default(),
}
}
pub fn replay(&self, tape: &Tape) {
self.replay_with(tape, ReplayMode::Strict);
}
pub fn replay_with(&self, tape: &Tape, mode: ReplayMode) {
let mut state = lock(&self.handle.shared.state);
if state.tokio_seed != tape.seed {
state.tokio_seed = tape.seed;
drop(state);
self.handle.shared.shutdown_tokio();
state = lock(&self.handle.shared.state);
}
if let Some(spin) = tape.spin {
state.spin_limit = spin;
}
state.script = Some(Script {
decisions: tape.decisions.clone(),
pos: 0,
mode,
misses: 0,
});
}
pub fn set_spin_limit(&self, polls: u64) {
lock(&self.handle.shared.state).spin_limit = polls.max(1);
}
pub fn stats(&self) -> Stats {
let state = lock(&self.handle.shared.state);
Stats {
seed: state.seed,
steps: state.steps,
schedule_hash: state.hash,
now: state.now,
tasks: state.tasks.len(),
ready: state.ready.len(),
tape_misses: state.script.as_ref().map_or(0, |s| s.misses),
tape_left: state
.script
.as_ref()
.map_or(0, |s| s.decisions.len().saturating_sub(s.pos)),
}
}
}
impl Drop for Runtime {
fn drop(&mut self) {
let last = {
let mut state = lock(&self.handle.shared.state);
state.runtimes -= 1;
state.runtimes == 0 && !state.running
};
if last {
self.handle.shared.drain();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stats {
pub seed: u64,
pub steps: u64,
pub schedule_hash: u64,
pub now: Duration,
pub tasks: usize,
pub ready: usize,
pub tape_misses: u64,
pub tape_left: usize,
}
pub fn ambient_handle() -> Handle {
Handle {
shared: current_or_ambient(),
}
}
pub fn now() -> Duration {
lock(¤t_or_ambient().state).now
}
fn unsupported(call: &str) -> ! {
panic!(
"spawned_sim: {call} is not supported under the simulation backend \
(it needs a real OS thread); use Backend::Async and async groups"
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Id(u64);
impl fmt::Display for Id {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
thread_local! {
static TASK: std::cell::Cell<u64> = const { std::cell::Cell::new(ROOT) };
}
pub fn task_id() -> Id {
Id(TASK.with(|t| t.get()))
}
pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn_on(¤t_or_ambient(), future)
}
pub fn spawn_blocking<F, R>(_f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
unsupported("spawn_blocking")
}
pub fn block_in_place<F, R>(_f: F) -> R
where
F: FnOnce() -> R,
{
unsupported("block_in_place")
}
fn spawn_on<F>(shared: &Arc<Shared>, future: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let id = shared.reserve_id();
let join = Arc::new(JoinState {
inner: Mutex::new(JoinInner {
result: None,
waker: None,
aborted: false,
finished: false,
taken: false,
detached: false,
}),
});
let task = TaskFuture {
id,
future: Some(Box::pin(future)),
join: join.clone(),
};
shared.spawn_boxed(id, Box::pin(task));
JoinHandle {
id,
join,
shared: Arc::downgrade(shared),
}
}
enum JoinErrorKind {
Cancelled,
Panic(String, Mutex<Box<dyn std::any::Any + Send>>),
}
pub struct JoinError {
id: u64,
kind: JoinErrorKind,
}
impl JoinError {
pub fn is_cancelled(&self) -> bool {
matches!(self.kind, JoinErrorKind::Cancelled)
}
pub fn is_panic(&self) -> bool {
matches!(self.kind, JoinErrorKind::Panic(..))
}
pub fn into_panic(self) -> Box<dyn std::any::Any + Send> {
self.try_into_panic()
.expect("`JoinError` reason is not a panic.")
}
pub fn try_into_panic(self) -> Result<Box<dyn std::any::Any + Send>, JoinError> {
match self.kind {
JoinErrorKind::Panic(_, payload) => {
Ok(payload.into_inner().unwrap_or_else(|p| p.into_inner()))
}
kind => Err(JoinError { id: self.id, kind }),
}
}
}
impl fmt::Debug for JoinError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
JoinErrorKind::Cancelled => write!(f, "JoinError::Cancelled({})", self.id),
JoinErrorKind::Panic(message, _) => {
write!(f, "JoinError::Panic({}, {message:?})", self.id)
}
}
}
}
impl fmt::Display for JoinError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
JoinErrorKind::Cancelled => write!(f, "task {} was cancelled", self.id),
JoinErrorKind::Panic(message, _) => {
write!(f, "task {} panicked with message {message:?}", self.id)
}
}
}
}
impl std::error::Error for JoinError {}
struct JoinInner<T> {
result: Option<Result<T, JoinError>>,
waker: Option<Waker>,
aborted: bool,
finished: bool,
taken: bool,
detached: bool,
}
fn discard<T>(result: Option<Result<T, JoinError>>) {
let _ = catch_unwind(AssertUnwindSafe(move || drop(result)));
}
struct JoinState<T> {
inner: Mutex<JoinInner<T>>,
}
impl<T> JoinState<T> {
fn complete(&self, result: Result<T, JoinError>) {
let waker = {
let mut inner = lock(&self.inner);
if inner.finished {
drop(inner);
discard(Some(result));
return;
}
inner.finished = true;
if inner.detached {
drop(inner);
discard(Some(result));
return;
}
inner.result = Some(result);
inner.waker.take()
};
if let Some(waker) = waker {
waker.wake();
}
}
}
pub struct JoinHandle<T> {
id: u64,
join: Arc<JoinState<T>>,
shared: Weak<Shared>,
}
impl<T> fmt::Debug for JoinHandle<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("sim::JoinHandle")
.field("id", &self.id)
.finish()
}
}
impl<T> JoinHandle<T> {
pub fn abort(&self) {
let finished = {
let mut inner = lock(&self.join.inner);
inner.aborted = true;
inner.finished
};
if !finished {
if let Some(shared) = self.shared.upgrade() {
shared.schedule(self.id);
}
}
}
pub fn is_finished(&self) -> bool {
lock(&self.join.inner).finished
}
pub fn id(&self) -> Id {
Id(self.id)
}
}
impl<T> Drop for JoinHandle<T> {
fn drop(&mut self) {
let result = {
let mut inner = lock(&self.join.inner);
inner.detached = true;
inner.result.take()
};
discard(result);
}
}
impl<T> Future for JoinHandle<T> {
type Output = Result<T, JoinError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut inner = lock(&self.join.inner);
match inner.result.take() {
Some(result) => {
inner.taken = true;
Poll::Ready(result)
}
None if inner.taken => {
drop(inner);
panic!("JoinHandle polled after completion")
}
None => {
inner.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
struct TaskFuture<F: Future> {
id: u64,
future: Option<Pin<Box<F>>>,
join: Arc<JoinState<F::Output>>,
}
fn panic_error(id: u64, payload: Box<dyn std::any::Any + Send>) -> JoinError {
let message = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<dyn Any>".into());
JoinError {
id,
kind: JoinErrorKind::Panic(message, Mutex::new(payload)),
}
}
impl<F: Future> TaskFuture<F> {
fn drop_future(&mut self) -> Result<(), JoinError> {
let Some(future) = self.future.take() else {
return Ok(());
};
let previous = TASK.with(|t| t.replace(self.id));
let dropped = catch_unwind(AssertUnwindSafe(move || drop(future)));
TASK.with(|t| t.set(previous));
dropped.map_err(|payload| panic_error(self.id, payload))
}
fn cancel(&mut self) {
let result = match self.drop_future() {
Ok(()) => Err(JoinError {
id: self.id,
kind: JoinErrorKind::Cancelled,
}),
Err(panic) => Err(panic),
};
self.join.complete(result);
}
}
impl<F: Future> Future for TaskFuture<F> {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let id = self.id;
if lock(&self.join.inner).aborted {
self.cancel();
return Poll::Ready(());
}
let Some(future) = self.future.as_mut() else {
return Poll::Ready(());
};
let previous = TASK.with(|t| t.replace(id));
let polled = catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(cx)));
TASK.with(|t| t.set(previous));
let result = match polled {
Ok(Poll::Pending) if lock(&self.join.inner).aborted => {
self.cancel();
return Poll::Ready(());
}
Ok(Poll::Pending) => return Poll::Pending,
Ok(Poll::Ready(value)) => self.drop_future().map(|()| value),
Err(payload) => {
let _ = self.drop_future();
Err(panic_error(id, payload))
}
};
self.join.complete(result);
Poll::Ready(())
}
}
impl<F: Future> Drop for TaskFuture<F> {
fn drop(&mut self) {
self.cancel();
}
}
pub struct Sleep {
shared: Arc<Shared>,
deadline: Duration,
key: Option<(Duration, u64)>,
yielded: bool,
}
impl fmt::Debug for Sleep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("sim::Sleep")
.field("deadline", &self.deadline)
.finish()
}
}
impl Future for Sleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = &mut *self;
let mut state = lock(&this.shared.state);
if state.now >= this.deadline {
if let Some(key) = this.key.take() {
state.timers.remove(&key);
} else if !this.yielded {
this.yielded = true;
drop(state);
cx.waker().wake_by_ref();
return Poll::Pending;
}
return Poll::Ready(());
}
let key = match this.key {
Some(key) => key,
None => {
let key = (this.deadline, state.next_timer);
state.next_timer += 1;
this.key = Some(key);
key
}
};
state.timers.insert(key, cx.waker().clone());
Poll::Pending
}
}
impl Drop for Sleep {
fn drop(&mut self) {
if let Some(key) = self.key.take() {
lock(&self.shared.state).timers.remove(&key);
}
}
}
pub fn sleep(duration: Duration) -> Sleep {
let shared = current_or_ambient();
let deadline = lock(&shared.state).now.saturating_add(duration);
Sleep {
shared,
deadline,
key: None,
yielded: false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Elapsed(());
impl fmt::Display for Elapsed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("deadline has elapsed")
}
}
impl std::error::Error for Elapsed {}
pub struct Timeout<F> {
future: Pin<Box<F>>,
sleep: Sleep,
}
impl<F: Future> Future for Timeout<F> {
type Output = Result<F::Output, Elapsed>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Poll::Ready(value) = self.future.as_mut().poll(cx) {
return Poll::Ready(Ok(value));
}
match Pin::new(&mut self.sleep).poll(cx) {
Poll::Ready(()) => Poll::Ready(Err(Elapsed(()))),
Poll::Pending => Poll::Pending,
}
}
}
pub fn timeout<F: Future>(duration: Duration, future: F) -> Timeout<F> {
Timeout {
future: Box::pin(future),
sleep: sleep(duration),
}
}
pub async fn yield_now() {
let mut yielded = false;
std::future::poll_fn(|cx| {
if yielded {
Poll::Ready(())
} else {
yielded = true;
cx.waker().wake_by_ref();
Poll::Pending
}
})
.await
}
pub async fn ctrl_c() {
std::future::pending::<()>().await
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Tape {
pub seed: u64,
pub spin: Option<u64>,
pub decisions: Vec<Decision>,
}
impl Tape {
pub fn deviations(&self) -> usize {
self.decisions
.iter()
.filter(|d| matches!(d, Decision::Poll(_)))
.count()
}
pub fn polls(&self) -> usize {
self.decisions
.iter()
.filter(|d| !matches!(d, Decision::Fire(_)))
.count()
}
fn picks(&self) -> Vec<Option<u64>> {
self.decisions
.iter()
.filter_map(|d| match d {
Decision::Canonical(_) => Some(None),
Decision::Poll(id) => Some(Some(*id)),
Decision::Fire(_) => None,
})
.collect()
}
pub fn to_text(&self) -> String {
let mut out = format!("# spawned-sim tape v2\nseed {}\n", self.seed);
if let Some(spin) = self.spin {
out.push_str(&format!("spin {spin}\n"));
}
for d in &self.decisions {
match d {
Decision::Canonical(Some(id)) => out.push_str(&format!("c {id}\n")),
Decision::Canonical(None) => out.push_str("c\n"),
Decision::Poll(id) => out.push_str(&format!("p {id}\n")),
Decision::Fire(at) => out.push_str(&format!("t {}\n", at.as_nanos())),
}
}
out
}
pub fn parse(text: &str) -> Result<Self, String> {
let mut tape = Tape::default();
for (n, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (op, arg) = line.split_once(' ').unwrap_or((line, ""));
let number = || {
arg.trim()
.parse::<u64>()
.map_err(|e| format!("tape line {}: {e}: {line:?}", n + 1))
};
match op {
"seed" => tape.seed = number()?,
"spin" => tape.spin = Some(number()?),
"c" if arg.trim().is_empty() => tape.decisions.push(Decision::Canonical(None)),
"c" => tape.decisions.push(Decision::Canonical(Some(number()?))),
"p" => tape.decisions.push(Decision::Poll(number()?)),
"t" => {
let nanos = arg
.trim()
.parse::<u128>()
.map_err(|e| format!("tape line {}: {e}: {line:?}", n + 1))?;
let secs = u64::try_from(nanos / 1_000_000_000)
.map_err(|e| format!("tape line {}: {e}: {line:?}", n + 1))?;
let at = Duration::new(secs, (nanos % 1_000_000_000) as u32);
tape.decisions.push(Decision::Fire(at));
}
_ => return Err(format!("tape line {}: unknown entry {line:?}", n + 1)),
}
}
Ok(tape)
}
pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(path, self.to_text())
}
pub fn load(path: &std::path::Path) -> std::io::Result<Self> {
let text = std::fs::read_to_string(path)?;
Self::parse(&text).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
}
#[derive(Debug, Clone)]
pub struct Outcome {
pub failure: Option<String>,
pub tape: Tape,
pub stats: Stats,
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<dyn Any>".into())
}
pub fn run_once<F, Fut>(seed: u64, script: Option<&Tape>, body: &F) -> Outcome
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
run_with(seed, script.map(|tape| (tape, ReplayMode::Strict)), body)
}
pub fn run_with<F, Fut>(seed: u64, script: Option<(&Tape, ReplayMode)>, body: &F) -> Outcome
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
run_planned(seed, script, None, body).0
}
fn run_planned<F, Fut>(
seed: u64,
script: Option<(&Tape, ReplayMode)>,
plan: Option<explore::Plan>,
body: &F,
) -> (Outcome, Option<explore::Plan>)
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
let rt = Runtime::with_seed(seed);
rt.record_tape();
let strict = matches!(script, Some((_, ReplayMode::Strict)));
if let Some((tape, mode)) = script {
rt.replay_with(tape, mode);
}
lock(&rt.handle.shared.state).plan = plan;
let result = catch_unwind(AssertUnwindSafe(|| rt.block_on(body(seed))));
let tape = rt.tape();
let stats = rt.stats();
let plan = lock(&rt.handle.shared.state).plan.take();
let shared = rt.handle.shared.clone();
let dropped = catch_unwind(AssertUnwindSafe(move || {
shared.drain();
drop(rt)
}));
let failure = match (result, dropped) {
(Err(payload), _) | (Ok(()), Err(payload)) => Some(panic_message(&*payload)),
(Ok(()), Ok(())) => None,
};
let failure = match failure {
Some(failure) if strict && stats.tape_left > 0 && !failure.contains("replay diverged") => {
Some(format!(
"spawned_sim: replay diverged: the run stopped with {} tape decisions unused \
(it failed with: {failure})",
stats.tape_left
))
}
failure => failure,
};
(
Outcome {
failure,
tape,
stats,
},
plan,
)
}
thread_local! {
static QUIET: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn quietly<R>(f: impl FnOnce() -> R) -> R {
static HOOK: std::sync::Once = std::sync::Once::new();
HOOK.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if !QUIET.with(|q| q.get()) {
previous(info);
}
}));
});
struct Reset(bool);
impl Drop for Reset {
fn drop(&mut self) {
QUIET.with(|q| q.set(self.0));
}
}
let _reset = Reset(QUIET.with(|q| q.replace(true)));
f()
}
fn script_tape(seed: u64, spin: Option<u64>, picks: &[Option<u64>]) -> Tape {
Tape {
seed,
spin,
decisions: picks
.iter()
.map(|p| p.map_or(Decision::Canonical(None), Decision::Poll))
.collect(),
}
}
pub fn shrink<F, Fut>(failing: &Tape, failure: &str, body: &F, budget: usize) -> (Tape, usize)
where
F: Fn(u64) -> Fut,
Fut: Future<Output = ()>,
{
let seed = failing.seed;
let runs = std::cell::Cell::new(0);
let attempt = |picks: &[Option<u64>]| -> Option<Tape> {
if runs.get() + 2 > budget {
return None;
}
runs.set(runs.get() + 1);
let candidate = script_tape(seed, failing.spin, picks);
let outcome = quietly(|| run_with(seed, Some((&candidate, ReplayMode::Permissive)), body));
if outcome.failure.as_deref() != Some(failure) {
return None;
}
runs.set(runs.get() + 1);
let check = quietly(|| run_once(seed, Some(&outcome.tape), body));
(check.failure.as_deref() == Some(failure)).then_some(outcome.tape)
};
let mut best = failing.clone();
let picks = best.picks();
let (mut lo, mut hi) = (0, picks.len());
while lo < hi {
let mid = (lo + hi) / 2;
match attempt(&picks[..mid]) {
Some(tape) => {
hi = mid;
if (tape.deviations(), tape.decisions.len())
<= (best.deviations(), best.decisions.len())
{
best = tape;
}
}
None => lo = mid + 1,
}
}
let mut chunk = best.deviations().div_ceil(2).max(1);
loop {
let mut progressed = false;
let mut start = 0;
loop {
let picks = best.picks();
let deviations: Vec<usize> = picks
.iter()
.enumerate()
.filter_map(|(i, p)| p.map(|_| i))
.collect();
if start >= deviations.len() {
break;
}
let mut candidate = picks.clone();
for &i in &deviations[start..(start + chunk).min(deviations.len())] {
candidate[i] = None;
}
match attempt(&candidate) {
Some(tape) if tape.deviations() < best.deviations() => {
best = tape;
progressed = true;
}
_ => start += chunk,
}
if runs.get() + 2 > budget {
return (best, runs.get());
}
}
if chunk == 1 && !progressed {
break;
}
chunk = (chunk / 2).max(1);
}
(best, runs.get())
}
fn running_world() -> Option<Arc<Shared>> {
current().or_else(|| AMBIENT.with(|a| a.borrow().upgrade()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Instant(Duration);
impl Instant {
pub fn now() -> Self {
match running_world() {
Some(shared) => Self(lock(&shared.state).now),
None => {
static BASE: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
Self(BASE.get_or_init(std::time::Instant::now).elapsed())
}
}
}
pub fn elapsed(&self) -> Duration {
Self::now().saturating_duration_since(*self)
}
pub fn duration_since(&self, earlier: Self) -> Duration {
self.saturating_duration_since(earlier)
}
pub fn saturating_duration_since(&self, earlier: Self) -> Duration {
self.0.saturating_sub(earlier.0)
}
pub fn checked_duration_since(&self, earlier: Self) -> Option<Duration> {
self.0.checked_sub(earlier.0)
}
pub fn checked_add(&self, duration: Duration) -> Option<Self> {
self.0.checked_add(duration).map(Self)
}
pub fn checked_sub(&self, duration: Duration) -> Option<Self> {
self.0.checked_sub(duration).map(Self)
}
}
impl std::ops::Add<Duration> for Instant {
type Output = Self;
fn add(self, rhs: Duration) -> Self {
Self(self.0 + rhs)
}
}
impl std::ops::AddAssign<Duration> for Instant {
fn add_assign(&mut self, rhs: Duration) {
self.0 += rhs;
}
}
impl std::ops::Sub<Duration> for Instant {
type Output = Self;
fn sub(self, rhs: Duration) -> Self {
Self(self.0.saturating_sub(rhs))
}
}
impl std::ops::SubAssign<Duration> for Instant {
fn sub_assign(&mut self, rhs: Duration) {
self.0 = self.0.saturating_sub(rhs);
}
}
impl std::ops::Sub<Instant> for Instant {
type Output = Duration;
fn sub(self, rhs: Instant) -> Duration {
self.saturating_duration_since(rhs)
}
}
pub fn entropy() -> u64 {
match running_world() {
Some(shared) => {
let mut state = lock(&shared.state);
state.entropy += 1;
let mut x = state.seed ^ state.entropy.wrapping_mul(0xD1B5_4A32_D192_ED03);
splitmix(&mut x)
}
None => {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let mut x = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
splitmix(&mut x)
}
}
}