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}