Skip to main content

unlit3dx/
thread.rs

1//! World threads: one thread owning one `World`, with pacing and stop control.
2
3use core::time::Duration;
4use std::panic::AssertUnwindSafe;
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread::JoinHandle;
7
8use unlit_ecs::World;
9use unlit_ecs::sync;
10use web_time::Instant;
11
12use crate::channel::{Snapshot, SnapshotReader};
13use crate::executor::LocalTasks;
14
15/// How long a dropped thread waits for its tick to notice the stop before it
16/// is left detached instead of blocking the dropper.
17const STOP_TIMEOUT: Duration = Duration::from_secs(2);
18
19/// The stop signal a thread and its owner share.
20#[derive(Debug, Default)]
21pub(crate) struct Stop {
22    /// Whether a stop has been requested, and whether the thread is over.
23    ///
24    /// Both are facts about the same thread and are waited on by the same
25    /// callers, so one lock and one condition variable carry them: a waiter
26    /// that takes the lock, finds nothing and waits holds the lock across the
27    /// wait, so it cannot miss a signal sent before it went to sleep.
28    requested: Mutex<StopState>,
29    /// Signalled whenever `requested` or `finished` turns true.
30    signal: Condvar,
31}
32
33/// What a stop signal has been told, and what it can tell.
34#[derive(Debug, Default)]
35struct StopState {
36    /// Whether a stop has been requested.
37    requested: bool,
38    /// Whether the thread that owns this stop has left its loop.
39    finished: bool,
40}
41
42impl Stop {
43    /// Request a stop and wake whoever is waiting on one.
44    fn request(&self) {
45        let mut state = sync::lock(&self.requested);
46        state.requested = true;
47        drop(state);
48        self.signal.notify_all();
49    }
50
51    /// Mark the thread as over and wake whoever is waiting on it.
52    fn finish(&self) {
53        let mut state = sync::lock(&self.requested);
54        state.finished = true;
55        drop(state);
56        self.signal.notify_all();
57    }
58
59    /// Wait up to `timeout` for the thread to finish.
60    ///
61    /// Returns whether it finished in time. A thread that already finished
62    /// returns at once, and a signal that arrives before the wait does not
63    /// get missed: the check and the wait happen under one lock.
64    fn wait_finished(&self, timeout: Duration) -> bool {
65        let deadline = Instant::now() + timeout;
66        let mut state = sync::lock(&self.requested);
67        loop {
68            if state.finished {
69                return true;
70            }
71            let now = Instant::now();
72            if now >= deadline {
73                return false;
74            }
75            state = match self.signal.wait_timeout(state, deadline - now) {
76                Ok((state, _)) => state,
77                Err(poisoned) => poisoned.into_inner().0,
78            };
79        }
80    }
81
82    /// Whether a stop has been requested.
83    fn requested(&self) -> bool {
84        sync::lock(&self.requested).requested
85    }
86}
87
88/// How a world's tick paces itself and observes a stop.
89///
90/// Every method may be called from inside the tick, which holds the control
91/// by shared reference only: the stop lives in a shared flag, so a tick that
92/// sleeps through the stop signal still sees it when it wakes.
93pub struct ThreadControl {
94    /// The stop signal the world's owner sends.
95    stop: Arc<Stop>,
96    /// Whether a thread drives the world.
97    ///
98    /// A world the host steps has no thread to sleep on: its waits return
99    /// at once, and the host paces it by stepping it when a step is due.
100    self_driven: bool,
101    /// When the previous tick started, for a self-driven world's elapsed time.
102    last_tick: Mutex<Option<Instant>>,
103}
104
105impl ThreadControl {
106    /// Whether a stop has been requested.
107    ///
108    /// Once true, stays true.
109    pub fn stop_requested(&self) -> bool {
110        self.stop.requested()
111    }
112
113    /// Whether a thread drives the world.
114    ///
115    /// A world the host steps is not self-driven: its waits return at once,
116    /// because the host is what decides when a step is due.
117    #[must_use]
118    pub fn self_driven(&self) -> bool {
119        self.self_driven
120    }
121
122    /// Seconds since the previous tick, measured once per turn.
123    pub(crate) fn elapsed_seconds(&self) -> f32 {
124        let now = Instant::now();
125        let mut last = sync::lock(&self.last_tick);
126        let elapsed = last.map_or(0.0, |last| now.duration_since(last).as_secs_f32());
127        *last = Some(now);
128        elapsed
129    }
130
131    /// Sleep for at most `timeout`, returning early when a stop is requested.
132    ///
133    /// A world the host steps has no thread to sleep on, so this returns at
134    /// once: the host owns the pacing, and one tick body then serves both
135    /// modes unchanged.
136    pub fn wait_timeout(&self, timeout: Duration) {
137        if !self.self_driven {
138            return;
139        }
140        let state = sync::lock(&self.stop.requested);
141        if state.requested {
142            return;
143        }
144        let _ = self.stop.signal.wait_timeout(state, timeout);
145    }
146}
147
148/// What a spawned world is doing.
149///
150/// A world runs until it stops or dies, and the state it published last is
151/// the one a reader finds: a reader that arrives after the end still learns
152/// how the world ended.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum WorldState {
155    /// The world is being driven.
156    Running,
157    /// The world ended: its tick asked to stop, or its owner stopped it.
158    Stopped,
159    /// The world died with a panic, and this is the message it carried.
160    Panicked(Arc<str>),
161}
162
163/// A cloneable view of a world's state.
164///
165/// A [`WorldThread`] owns the join handle and stops its thread on drop;
166/// this shares only the stop signal and the state, so a
167/// [`WorldHandle`](crate::handle::WorldHandle) can report a world's state and
168/// ask it to stop without owning the thread. A caller reads that state through
169/// the handle, so this stays an implementation detail of the spawn functions.
170#[derive(Clone)]
171pub(crate) struct ThreadStatus {
172    /// The stop signal, sent on drop.
173    stop: Arc<Stop>,
174    /// What the world is doing, and how it ended.
175    state: Snapshot<WorldState>,
176    /// This status's own reading of the state, for the tests that ask it
177    /// directly instead of taking a reader.
178    own: SnapshotReader<WorldState>,
179}
180
181impl ThreadStatus {
182    /// A status for a world on a thread of its own, running from here.
183    pub(crate) fn threaded(stop: Arc<Stop>) -> Self {
184        Self::at(stop)
185    }
186
187    /// A status for a world that shares its owner's thread.
188    ///
189    /// Such a world - a sub-world on the host, a runner of the caller's own -
190    /// ends when its owner says so, so the state stays
191    /// [`WorldState::Running`] until then, and the stop signal belongs to
192    /// that owner alone.
193    pub(crate) fn detached() -> Self {
194        Self::at(Arc::default())
195    }
196
197    fn at(stop: Arc<Stop>) -> Self {
198        let state = Snapshot::new();
199        let own = state.reader();
200        state.publish(WorldState::Running);
201        Self { stop, state, own }
202    }
203
204    /// A reader of the state this world publishes.
205    ///
206    /// Every reader sees the newest state, so one that arrives late still
207    /// learns how the world ended.
208    #[must_use]
209    pub(crate) fn state(&self) -> SnapshotReader<WorldState> {
210        self.state.reader()
211    }
212
213    /// Whether the world has stopped or died.
214    ///
215    /// A detached thread keeps reporting its own end, so this stays
216    /// meaningful after [`WorldThread::detach`].
217    #[must_use]
218    pub(crate) fn is_finished(&self) -> bool {
219        !matches!(
220            self.own.latest().as_deref(),
221            None | Some(WorldState::Running)
222        )
223    }
224
225    /// Ask the world to stop, without waiting for it to finish.
226    pub(crate) fn stop(&self) {
227        self.stop.request();
228    }
229
230    /// Whether the world has been asked to stop.
231    ///
232    /// A world on its own thread reads this from its tick loop; a world the
233    /// host drives reads it when the host drains it.
234    pub(crate) fn stop_requested(&self) -> bool {
235        self.stop.requested()
236    }
237
238    /// Mark the world as finished.
239    ///
240    /// A world on its own thread marks itself when its loop ends; a stepped
241    /// world's host calls this when its tick returns `false`, and a world
242    /// dropped by its owner marks itself as it goes. A world that has already
243    /// ended keeps the state it ended with: a panic stays the last word.
244    pub(crate) fn finish(&self) {
245        if self.is_finished() {
246            return;
247        }
248        self.state.publish(WorldState::Stopped);
249    }
250
251    /// Record the panic that ended the world, message and all.
252    pub(crate) fn record_panic(&self, message: String) {
253        self.state.publish(WorldState::Panicked(message.into()));
254    }
255}
256
257/// One thread owning one `World`.
258///
259/// Dropping the thread asks it to stop and waits for it to finish, so a
260/// threaded world's lifetime is exactly the proxy's. A tick that ignores the
261/// stop cannot hang the dropper: after a short grace period the thread is
262/// detached and the drop returns.
263pub struct WorldThread {
264    /// The shared state a handle reports.
265    status: ThreadStatus,
266    /// The thread handle, joined on drop.
267    joined: Option<JoinHandle<()>>,
268}
269
270impl WorldThread {
271    /// Spawn a thread that owns a fresh `World` and ticks it forever.
272    ///
273    /// The tick runs once per loop iteration and returns whether to keep
274    /// running; a `false` return ends the thread as cleanly as a stop. A
275    /// tick panic is caught, logged and published as the world's state, and
276    /// ends the thread: a world whose tick has panicked is not worth
277    /// restarting blindly.
278    pub fn spawn<T>(name: &str, mut tick: T) -> Self
279    where
280        T: FnMut(&mut World, &ThreadControl) -> bool + Send + 'static,
281    {
282        Self::spawn_with(name, |_| (), move |world, control, ()| tick(world, control))
283    }
284
285    /// Like [`spawn`](Self::spawn), with a setup step and the state it
286    /// produces.
287    ///
288    /// `init` runs once on the thread, against the fresh world, before the
289    /// first job is drained; whatever it returns is handed to every tick.
290    /// That ordering is what lets a bridge create the components its jobs
291    /// assume exist - the physics world with its configured gravity, say -
292    /// so a job sent immediately after spawning already finds them.
293    pub fn spawn_with<S, I, T>(name: &str, init: I, mut tick: T) -> Self
294    where
295        I: FnOnce(&mut World) -> S + Send + 'static,
296        T: FnMut(&mut World, &ThreadControl, &mut S) -> bool + Send + 'static,
297    {
298        let stop = Arc::new(Stop::default());
299        let status = ThreadStatus::threaded(Arc::clone(&stop));
300        let thread_status = status.clone();
301        let thread_stop = Arc::clone(&stop);
302        let name = name.to_owned();
303        let thread_name = name.clone();
304        let joined = std::thread::Builder::new()
305            .name(name)
306            .spawn(move || {
307                let control = ThreadControl {
308                    stop: Arc::clone(&thread_stop),
309                    self_driven: true,
310                    last_tick: Mutex::new(None),
311                };
312                let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| {
313                    let mut world = World::new();
314                    // The world's async tasks are its own from here on: the
315                    // setup may spawn one, and every tick drives them.
316                    LocalTasks::install(&mut world);
317                    let mut state = init(&mut world);
318                    while !control.stop_requested() {
319                        LocalTasks::pump(&mut world);
320                        if !tick(&mut world, &control, &mut state) {
321                            break;
322                        }
323                    }
324                }));
325                match outcome {
326                    Ok(()) => thread_status.finish(),
327                    Err(payload) => {
328                        let message = panic_message(&*payload);
329                        log::error!("the world thread {thread_name} panicked: {message}");
330                        thread_status.record_panic(message);
331                    }
332                }
333                // The state is the last word on how the world ended, so it is
334                // published before whoever waits on the thread is woken.
335                thread_stop.finish();
336            })
337            .expect("the world thread spawns");
338        Self {
339            status,
340            joined: Some(joined),
341        }
342    }
343
344    /// A cloneable view of the thread's state, for the handle the spawn hands
345    /// back.
346    #[must_use]
347    pub(crate) fn status(&self) -> ThreadStatus {
348        self.status.clone()
349    }
350
351    /// A reader of the state the world publishes.
352    #[must_use]
353    pub fn state(&self) -> SnapshotReader<WorldState> {
354        self.status.state()
355    }
356
357    /// Whether the thread has exited.
358    ///
359    /// A detached thread keeps reporting its own exit, so this stays
360    /// meaningful after [`detach`](Self::detach).
361    #[must_use]
362    pub fn is_finished(&self) -> bool {
363        self.status.is_finished()
364    }
365
366    /// Ask the thread to stop, without waiting for it to finish.
367    pub fn stop(&self) {
368        self.status.stop();
369    }
370
371    /// Wait up to `timeout` for the thread to finish.
372    ///
373    /// Returns whether it finished in time. A thread that already finished,
374    /// or was detached, counts as finished. The wait is woken by the thread
375    /// itself as it leaves its loop, so it costs nothing while the thread
376    /// runs.
377    pub fn join_timeout(&mut self, timeout: Duration) -> bool {
378        if self.joined.is_none() {
379            return true;
380        }
381        if !self.status.stop.wait_finished(timeout) {
382            return false;
383        }
384        self.join();
385        true
386    }
387
388    /// Leave the thread running without ever joining it.
389    ///
390    /// The thread stops when it notices the stop, but nothing waits for it.
391    pub fn detach(&mut self) {
392        self.joined.take();
393    }
394
395    /// Join the thread, clearing the handle.
396    fn join(&mut self) {
397        if let Some(joined) = self.joined.take() {
398            // A panic was already reported through the panic slot.
399            let _ = joined.join();
400        }
401    }
402}
403
404impl Drop for WorldThread {
405    fn drop(&mut self) {
406        self.status.stop();
407        if !self.join_timeout(STOP_TIMEOUT) {
408            log::warn!("a world thread did not stop within {STOP_TIMEOUT:?}; leaving it detached");
409            self.detach();
410        }
411    }
412}
413
414/// Paces a fixed-rate loop: sleeps until the next step boundary.
415///
416/// The boundary advances by exactly one step per wait, so a tick that
417/// finishes early does not shift the schedule; a tick that overruns a whole
418/// step resynchronizes to the present instead of catching up in a burst.
419///
420/// The clock paces a self-driven world; a world the host steps does not
421/// need it, because the host decides when each step is due.
422pub struct FixedClock {
423    /// Seconds per step.
424    step: Duration,
425    /// When the next step is due.
426    next: Instant,
427}
428
429impl FixedClock {
430    /// A clock stepping every `step`, starting now.
431    pub fn new(step: Duration) -> Self {
432        Self {
433            step,
434            next: Instant::now() + step,
435        }
436    }
437
438    /// Sleep until the next step is due - or a stop arrives, which the caller
439    /// sees through [`ThreadControl::stop_requested`] - then move to the
440    /// step after it.
441    pub fn wait(&mut self, control: &ThreadControl) {
442        let now = Instant::now();
443        if now < self.next {
444            control.wait_timeout(self.next - now);
445        }
446        self.next += self.step;
447        let now = Instant::now();
448        if self.next <= now {
449            self.next = now + self.step;
450        }
451    }
452
453    /// Change how long a step lasts, keeping the schedule where it is.
454    ///
455    /// The next step is due one new step from now when the current one has
456    /// already passed, so a shortened step never asks for a burst of waits
457    /// to catch up.
458    pub fn set_step(&mut self, step: Duration) {
459        self.step = step;
460        let now = Instant::now();
461        if self.next <= now {
462            self.next = now + step;
463        }
464    }
465}
466
467/// The most fixed steps one tick may run to catch up with the time that
468/// passed.
469pub const MAX_CATCH_UP_STEPS: u32 = 4;
470
471/// Turn elapsed time into a whole number of fixed steps, carrying the rest
472/// into the next tick as debt.
473///
474/// `debt` is what earlier ticks owed and did not spend; the returned count
475/// is how many steps the accumulated time now covers, and `debt` keeps the
476/// fraction of a step left over. Debt past [`MAX_CATCH_UP_STEPS`] steps is
477/// discarded rather than owed: a world that stalled for a long while - a
478/// suspended process, a debugger pause - would otherwise run a burst of
479/// steps to catch up, and simulating a pause that long is neither cheap nor
480/// wanted.
481///
482/// A negative `delta_seconds` - a clock that went backwards - adds no time.
483pub fn fixed_steps(debt: &mut f32, delta_seconds: f32, step_seconds: f32) -> u32 {
484    *debt = (*debt + delta_seconds.max(0.0)).min(step_seconds * MAX_CATCH_UP_STEPS as f32);
485    let steps = (*debt / step_seconds) as u32;
486    *debt -= steps as f32 * step_seconds;
487    steps
488}
489
490/// A control and status pair for a world the host steps.
491///
492/// Nothing is spawned here: the world has no thread, its waits return at
493/// once, and the host steps it whenever a step is due.
494pub(crate) fn stepped_control() -> (ThreadControl, ThreadStatus) {
495    let stop = Arc::new(Stop::default());
496    let status = ThreadStatus::threaded(Arc::clone(&stop));
497    let control = ThreadControl {
498        stop,
499        self_driven: false,
500        last_tick: Mutex::new(None),
501    };
502    (control, status)
503}
504
505/// The message a panic payload carries, whatever shape it arrived in.
506pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
507    if let Some(message) = payload.downcast_ref::<&'static str>() {
508        (*message).to_owned()
509    } else if let Some(message) = payload.downcast_ref::<String>() {
510        message.clone()
511    } else {
512        "no panic message".to_owned()
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
520
521    /// How long a test waits for a thread to reach the state it asserts on.
522    ///
523    /// Generous, because the wait is not what is being measured: on a loaded
524    /// runner a thread can go a long while without being scheduled at all,
525    /// and a short budget would fail a thread that was merely slow to start.
526    const TEST_TIMEOUT: Duration = Duration::from_secs(5);
527
528    /// Poll until `ready` yields a value, or give up when the timeout runs
529    /// out.
530    ///
531    /// Some of what a test waits for can only be read once - a panic message
532    /// is taken out of its slot - so the poll takes it rather than asking
533    /// twice. Returns `None` on giving up, so the caller asserts on it
534    /// instead of looping without end when a thread never gets to run.
535    fn wait_until<T>(ready: impl Fn() -> Option<T>) -> Option<T> {
536        let deadline = Instant::now() + TEST_TIMEOUT;
537        loop {
538            if let Some(value) = ready() {
539                return Some(value);
540            }
541            if Instant::now() >= deadline {
542                return None;
543            }
544            std::thread::sleep(Duration::from_millis(1));
545        }
546    }
547
548    /// The thread stops when its tick asks it to, and the stop is observable.
549    #[test]
550    fn a_false_tick_ends_the_thread() {
551        let thread = WorldThread::spawn("test", |_, _| false);
552        let state = thread.state();
553        assert!(
554            wait_until(|| thread.is_finished().then_some(())).is_some(),
555            "the thread never finished"
556        );
557        assert_eq!(state.latest().as_deref(), Some(&WorldState::Stopped));
558    }
559
560    /// A tick panic is reported through the state.
561    #[test]
562    fn a_tick_panic_is_reported() {
563        let thread = WorldThread::spawn("test", |_, _| panic!("the tick died"));
564        let state = thread.state();
565        assert!(
566            wait_until(|| thread.is_finished().then_some(())).is_some(),
567            "the thread never finished"
568        );
569        assert_eq!(
570            state.latest().as_deref(),
571            Some(&WorldState::Panicked("the tick died".into()))
572        );
573    }
574
575    /// A panic in the setup step is reported like a tick panic.
576    #[test]
577    fn a_setup_panic_is_reported() {
578        let thread = WorldThread::spawn_with(
579            "test",
580            |_| -> () { panic!("the setup died") },
581            |_, _, ()| false,
582        );
583        let state = thread.state();
584        assert!(
585            wait_until(|| thread.is_finished().then_some(())).is_some(),
586            "the thread never finished"
587        );
588        assert_eq!(
589            state.latest().as_deref(),
590            Some(&WorldState::Panicked("the setup died".into()))
591        );
592    }
593
594    /// A stop ends the tick's wait early instead of sleeping it out.
595    #[test]
596    fn a_stop_wakes_a_sleeping_tick() {
597        // The stop can only cut a wait short if a tick has begun one, so the
598        // drop waits for the tick to say it is about to wait rather than
599        // sleeping a while and hoping. A thread on a loaded runner has not
600        // reached its first tick ten milliseconds in, and a stop that lands
601        // before it does has nothing to wake.
602        let waiting = Arc::new(AtomicBool::new(false));
603        let about_to_wait = Arc::clone(&waiting);
604        let wakeups = Arc::new(AtomicUsize::new(0));
605        let observed = Arc::clone(&wakeups);
606        let thread = WorldThread::spawn("test", move |_, control| {
607            about_to_wait.store(true, Ordering::SeqCst);
608            let before = Instant::now();
609            control.wait_timeout(Duration::from_millis(1_000));
610            if before.elapsed() < Duration::from_millis(900) {
611                observed.fetch_add(1, Ordering::SeqCst);
612            }
613            true
614        });
615        let waited = wait_until(|| waiting.load(Ordering::SeqCst).then_some(()));
616        drop(thread);
617        assert!(waited.is_some(), "the tick never reached its wait");
618        assert!(
619            wakeups.load(Ordering::SeqCst) >= 1,
620            "the stop never woke the sleeping tick"
621        );
622    }
623
624    /// Setup runs before the first tick, and its state reaches every tick.
625    #[test]
626    fn setup_runs_before_the_first_tick() {
627        let ran = Arc::new(AtomicBool::new(false));
628        let saw = Arc::clone(&ran);
629        let mut thread = WorldThread::spawn_with(
630            "test",
631            move |_| {
632                saw.store(true, Ordering::SeqCst);
633                7u32
634            },
635            move |_, _, state| {
636                assert_eq!(*state, 7);
637                false
638            },
639        );
640        assert!(thread.join_timeout(Duration::from_secs(2)));
641        assert!(ran.load(Ordering::SeqCst), "the setup never ran");
642    }
643
644    /// Dropping a thread whose tick ignores the stop returns instead of
645    /// hanging, and leaves the thread detached.
646    #[test]
647    fn a_stuck_tick_does_not_hang_the_drop() {
648        let started = Arc::new(AtomicBool::new(false));
649        let flag = Arc::clone(&started);
650        let thread = WorldThread::spawn("test", move |_, _| {
651            flag.store(true, Ordering::SeqCst);
652            // Ignore the stop forever: the drop must give up on it.
653            std::thread::sleep(Duration::from_secs(30));
654            false
655        });
656        assert!(
657            wait_until(|| started.load(Ordering::SeqCst).then_some(())).is_some(),
658            "the tick never ran"
659        );
660        let before = Instant::now();
661        drop(thread);
662        assert!(
663            before.elapsed() < STOP_TIMEOUT + Duration::from_secs(1),
664            "the drop waited far longer than the grace period"
665        );
666    }
667
668    /// Waiting for a thread that is still ticking gives up on the timeout
669    /// instead of waiting for the thread.
670    #[test]
671    fn a_join_timeout_gives_up_on_a_stuck_tick() {
672        let mut thread = WorldThread::spawn("test", |_, _| {
673            // Sleep through the stop: nothing ends the tick but the sleep.
674            std::thread::sleep(Duration::from_secs(30));
675            false
676        });
677        let before = Instant::now();
678        assert!(!thread.join_timeout(Duration::from_millis(50)));
679        assert!(
680            before.elapsed() < Duration::from_secs(1),
681            "the wait outlasted its timeout"
682        );
683        thread.detach();
684    }
685
686    /// A detached thread is never joined.
687    #[test]
688    fn a_detached_thread_is_forgotten() {
689        let mut thread = WorldThread::spawn("test", |_, control| {
690            control.wait_timeout(Duration::from_millis(5));
691            true
692        });
693        thread.stop();
694        assert!(thread.join_timeout(Duration::from_secs(2)));
695        thread.detach();
696        assert!(thread.is_finished());
697    }
698
699    /// The step the fixed-step tests accumulate debt against.
700    const STEP: f32 = 1.0 / 60.0;
701
702    /// One step of time runs exactly one step and leaves nothing owed.
703    #[test]
704    fn a_step_of_time_runs_one_fixed_step() {
705        let mut debt = 0.0;
706        assert_eq!(fixed_steps(&mut debt, STEP, STEP), 1);
707        assert_eq!(debt, 0.0);
708    }
709
710    /// The fraction of a step a tick did not spend waits for the next one
711    /// instead of being rounded away.
712    #[test]
713    fn an_unspent_fraction_waits_for_the_next_tick() {
714        let mut debt = 0.0;
715        assert_eq!(fixed_steps(&mut debt, STEP * 0.5, STEP), 0);
716        assert!((debt - STEP * 0.5).abs() < STEP * 1e-4);
717        assert_eq!(fixed_steps(&mut debt, STEP * 0.6, STEP), 1);
718        assert!((debt - STEP * 0.1).abs() < STEP * 1e-4);
719    }
720
721    /// A stall past the catch-up limit runs the limit and drops the rest, so
722    /// a world that was paused resumes instead of replaying the pause.
723    #[test]
724    fn debt_past_the_catch_up_limit_is_discarded() {
725        let mut debt = 0.0;
726        assert_eq!(
727            fixed_steps(&mut debt, STEP * 100.0, STEP),
728            MAX_CATCH_UP_STEPS
729        );
730        assert_eq!(debt, 0.0);
731        // Just short of the limit still runs every whole step it covers.
732        assert_eq!(fixed_steps(&mut debt, STEP * 3.5, STEP), 3);
733    }
734
735    /// A clock that went backwards owes nothing.
736    #[test]
737    fn a_negative_interval_adds_no_time() {
738        let mut debt = 0.0;
739        assert_eq!(fixed_steps(&mut debt, -1.0, STEP), 0);
740        assert_eq!(debt, 0.0);
741    }
742}