Skip to main content

gtether/util/
tick_loop.rs

1//! Utilities around custom tick loops.
2//!
3//! This module contains utilities for creating tick loops with custom configurations. A tick loop
4//! is a loop that attempts to "tick", or execute, at a consistent rate. A tick loop can be
5//! considered similar to an event loop, but it is driven by a timer rather than events.
6//!
7//! The primary way to create tick loops is via [TickLoopBuilder]; see that type for examples.
8use educe::Educe;
9use parking_lot::{Condvar, Mutex};
10use std::error::Error;
11use std::fmt::{Debug, Display, Formatter};
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14use std::thread::JoinHandle;
15use std::time::{Duration, Instant};
16use tracing::warn;
17
18#[derive(Debug)]
19enum TickType {
20    Rate(usize),
21    MinDuration(Duration),
22}
23
24impl Default for TickType {
25    #[inline]
26    fn default() -> Self {
27        Self::MinDuration(Duration::from_millis(10))
28    }
29}
30
31impl TickType {
32    fn next_tick(&self, last_tick: Instant, loop_name: &str) -> Option<Instant> {
33        let now = Instant::now();
34        match self {
35            Self::Rate(rate) => {
36                // Increment timeslot
37                let tick_size = Duration::from_secs_f32(1.0 / *rate as f32);
38                let mut next_tick = last_tick;
39                next_tick += tick_size;
40                let mut skipped_ticks = 0;
41                while next_tick < now {
42                    next_tick += tick_size;
43                    skipped_ticks += 1;
44                }
45                if skipped_ticks > 0 {
46                    let tick_time = now - last_tick;
47                    warn!(skipped_ticks, ?tick_time, loop_name, "Tick(s) took too long");
48                }
49                Some(next_tick)
50            },
51            Self::MinDuration(min_duration) => {
52                let next_tick = last_tick + *min_duration;
53                if next_tick > now {
54                    Some(next_tick)
55                } else {
56                    None
57                }
58            },
59        }
60    }
61}
62
63#[derive(Educe)]
64#[educe(Debug)]
65struct TickLoop<FI, FT, D, E>
66where
67    FI: FnOnce() -> Result<D, E>,
68    FT: FnMut(&mut D, Duration) -> bool,
69{
70    name: String,
71    tick_type: TickType,
72    #[educe(Debug(ignore))]
73    fn_init: FI,
74    #[educe(Debug(ignore))]
75    fn_tick: FT,
76}
77
78impl<FI, FT, D, E> TickLoop<FI, FT, D, E>
79where
80    FI: FnOnce() -> Result<D, E>,
81    FT: FnMut(&mut D, Duration) -> bool,
82{
83    fn run(
84        should_exit: Arc<AtomicBool>,
85        name: &str,
86        tick_type: TickType,
87        mut fn_tick: FT,
88        mut data: D,
89    ) {
90        let mut last_tick = Instant::now();
91        let mut next_tick = tick_type.next_tick(last_tick, name);
92        while !should_exit.load(Ordering::Relaxed) {
93            // Sleep until next timeslot
94            if let Some(next_tick) = next_tick {
95                let now = Instant::now();
96                if next_tick > now {
97                    std::thread::sleep(next_tick - now);
98                }
99            }
100
101            // Tick
102            let tick_start = Instant::now();
103            let delta = tick_start - last_tick;
104            if !(&mut fn_tick)(&mut data, delta) {
105                // Tick said to exit
106                break;
107            }
108
109            last_tick = tick_start;
110            next_tick = tick_type.next_tick(tick_start, name);
111        }
112    }
113
114    fn start(self) -> Result<(), E> {
115        match (self.fn_init)() {
116            Ok(data) => {
117                let should_exit = Arc::new(AtomicBool::new(false));
118                Self::run(
119                    should_exit,
120                    &self.name,
121                    self.tick_type,
122                    self.fn_tick,
123                    data,
124                );
125                Ok(())
126            },
127            Err(e) => Err(e),
128        }
129    }
130}
131
132impl<FI, FT, D, E> TickLoop<FI, FT, D, E>
133where
134    FI: (FnOnce() -> Result<D, E>) + Send + 'static,
135    FT: (FnMut(&mut D, Duration) -> bool) + Send + 'static,
136    D: 'static,
137    E: Send + 'static,
138{
139    fn spawn(self) -> Result<TickLoopHandle, E> {
140        let should_exit = Arc::new(AtomicBool::new(false));
141        let should_exit_thread = should_exit.clone();
142
143        let pair = Arc::new((Mutex::new(None), Condvar::new()));
144        let pair_thread = pair.clone();
145
146        let join_handle = Some(std::thread::Builder::new()
147            .name(self.name.clone())
148            .spawn(move || {
149                let &(ref lock, ref cvar) = &*pair_thread;
150                match (self.fn_init)() {
151                    Ok(data) => {
152                        {
153                            let mut result = lock.lock();
154                            *result = Some(Ok(()));
155                            cvar.notify_one();
156                        }
157                        Self::run(
158                            should_exit_thread,
159                            &self.name,
160                            self.tick_type,
161                            self.fn_tick,
162                            data,
163                        );
164                    },
165                    Err(e) => {
166                        let mut result = lock.lock();
167                        *result = Some(Err(e));
168                        cvar.notify_one();
169                    }
170                }
171            })
172            .unwrap());
173
174        let &(ref lock, ref cvar) = &*pair;
175        let mut result = lock.lock();
176        if !result.is_some() {
177            cvar.wait(&mut result);
178        }
179        result.take().unwrap()?;
180
181        Ok(TickLoopHandle {
182            join_handle,
183            should_exit,
184        })
185    }
186}
187
188/// Handle for a threaded tick loop.
189///
190/// This handle doesn't really do anything by itself, but represents a tick loop running in a
191/// separate thread. This handle is not cloneable, and will stop and join the tick loop thread when
192/// it is dropped.
193///
194/// ```no_run
195/// use gtether::util::tick_loop::TickLoopBuilder;
196/// # use gtether::util::tick_loop::TickLoopBuildError;
197///
198/// let join_handle = TickLoopBuilder::new()
199///     // These init()/tick() closures are noops
200///     .init(|| Ok::<(), ()>(()))
201///     .tick(|_, _| true)
202///     // Spawn a threaded tick loop
203///     .spawn()?;
204///
205/// // Drop the join_handle to stop and join the threaded tick loop
206/// drop(join_handle);
207/// #
208/// # Ok::<(), TickLoopBuildError<_>>(())
209/// ```
210#[derive(Educe)]
211#[educe(Debug)]
212pub struct TickLoopHandle {
213    #[educe(Debug(ignore))]
214    join_handle: Option<JoinHandle<()>>,
215    should_exit: Arc<AtomicBool>,
216}
217
218impl Drop for TickLoopHandle {
219    fn drop(&mut self) {
220        if let Some(join_handle) = self.join_handle.take() {
221            self.should_exit.store(true, Ordering::Relaxed);
222            join_handle.join().unwrap();
223        } else {
224            warn!("TickLoop internal thread already joined");
225        }
226    }
227}
228
229/// Error that can occur when building a tick loop.
230#[derive(Debug)]
231pub enum TickLoopBuildError<E> {
232    /// A required option was not specified.
233    MissingOption { name: String },
234    /// The init() callback yielded an error.
235    InitError(E),
236}
237
238impl<E: Display> Display for TickLoopBuildError<E> {
239    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
240        match self {
241            Self::MissingOption { name } => write!(f, "Missing required option: '{name}'"),
242            Self::InitError(err) => write!(f, "Initialization failed: {err}"),
243        }
244    }
245}
246
247impl<E: Debug + Display> Error for TickLoopBuildError<E> {}
248
249impl<E> TickLoopBuildError<E> {
250    fn missing_option(name: impl Into<String>) -> Self {
251        Self::MissingOption { name: name.into() }
252    }
253}
254
255/// Builder pattern for tick loops.
256///
257/// Tick loops are generally comprised of an [`init()`](TickLoopBuilder::init) closure, a
258/// [`tick()`](TickLoopBuilder::tick) closure, and several configuration options. When starting a
259/// loop, the `init()` closure will be executed to run any onetime initialization logic, and
260/// generate an [associated data structure](#associated-data-structure), if any. Afterward, the
261/// `tick()` closure will be repeatedly executed at configured intervals.
262///
263/// # Associated Data Structure
264///
265/// Sometimes data needs to initialized _within_ the context of the loop, and cannot be created
266/// ahead of time. For example, if a [threaded tick loop](#threaded-tick-loops) needs some data
267/// owned solely within the context of the loop that _isn't_ `Send`, it cannot be moved into the
268/// loop's `tick()` closure. For these situations, the `init()` closure can yield an arbitrary type.
269/// That type will be fed into every invocation of the `tick()` closure via a mutable borrow.
270///
271/// # Threaded Tick Loops
272///
273/// When building a tick loop, it can either be built threaded with
274/// [`spawn()`](TickLoopBuilder::spawn) or local with [`start()`](TickLoopBuilder::start). When
275/// building a threaded tick loop, a separate thread will be started to run the tick loop, and a
276/// [TickLoopHandle] will be yielded to keep track of it. The `init()` and `tick()` closures used
277/// to build a threaded tick loop must also be `Send` and `'static`, as they have to be sent to the
278/// started thread.
279///
280/// # Examples
281///
282/// Start a local tick loop with a given tick rate.
283/// ```no_run
284/// use gtether::util::tick_loop::TickLoopBuilder;
285/// # use gtether::util::tick_loop::TickLoopBuildError;
286///
287/// TickLoopBuilder::new()
288///     // 60 ticks per second
289///     .tick_rate(60)
290///     .init(|| Ok::<usize, ()>(0))
291///     .tick(|count, _| {
292///         *count += 1;
293///         // Will stop the tick loop after ~10 minutes
294///         *count < 600
295///     })
296///     .start()?;
297/// #
298/// # Ok::<(), TickLoopBuildError<_>>(())
299/// ```
300///
301/// Spawn a threaded tick loop
302/// ```no_run
303/// use gtether::util::tick_loop::TickLoopBuilder;
304/// # use gtether::util::tick_loop::TickLoopBuildError;
305///
306/// let join_handle = TickLoopBuilder::new()
307///     .init(|| Ok::<usize, ()>(0))
308///     .tick(|count, _| {
309///         *count += 1;
310///         // Will stop the tick loop after ~10 minutes
311///         *count < 600
312///     })
313///     .spawn()?;
314/// #
315/// # Ok::<(), TickLoopBuildError<_>>(())
316/// ```
317pub struct TickLoopBuilder<FI, FT, D: 'static = (), E = ()>
318where
319    FI: FnOnce() -> Result<D, E>,
320    FT: FnMut(&mut D, Duration) -> bool,
321{
322    name: Option<String>,
323    tick_type: Option<TickType>,
324    fn_init: Option<FI>,
325    fn_tick: Option<FT>,
326}
327
328impl<FI, FT, D, E> TickLoopBuilder<FI, FT, D, E>
329where
330    FI: FnOnce() -> Result<D, E>,
331    FT: FnMut(&mut D, Duration) -> bool,
332{
333    /// Create a new [TickLoopBuilder].
334    #[inline]
335    pub fn new() -> Self {
336        Self {
337            name: None,
338            tick_type: None,
339            fn_init: None,
340            fn_tick: None,
341        }
342    }
343
344    /// Set the name of the tick loop.
345    ///
346    /// This value is used for setting the thread name if building a threaded tick loop, and for any
347    /// logging or other diagnostics that may be emitted.
348    ///
349    /// Defaults to `"tick-loop"`.
350    #[inline]
351    pub fn name(mut self, name: impl Into<String>) -> Self {
352        self.name = Some(name.into());
353        self
354    }
355
356    /// Set the minimum duration for a single tick.
357    ///
358    /// When executing the tick loop, if a tick takes less time than this duration, the tick loop
359    /// will sleep until this duration is met. If a tick takes more time than this duration, the
360    /// tick loop will continue on and immediately execute the next tick.
361    ///
362    /// This option is mutually exclusive with [`tick_rate()`](Self::tick_rate), and setting one
363    /// will override the other.
364    ///
365    /// Defaults to 10ms.
366    #[inline]
367    pub fn min_tick_duration(mut self, min_tick_duration: Duration) -> Self {
368        self.tick_type = Some(TickType::MinDuration(min_tick_duration));
369        self
370    }
371
372    /// Set the tick rate per second.
373    ///
374    /// When executing the tick loop, will attempt to execute ticks at a rate that is consistent to
375    /// the given rate. If a tick takes less time than the calculated duration based on this rate,
376    /// the tick loop will sleep until that duration has passed. If a tick takes more time than the
377    /// calculated duration and would cause one or more ticks to be "skipped" in a single second,
378    /// the tick loop will sleep until the next timeslot - in multiples the calculated per-tick
379    /// duration - and log a warning that one or more ticks have been skipped.
380    ///
381    /// This option is mutually exclusive with [`min_tick_duration()`](Self::min_tick_duration), and
382    /// setting one will override the other.
383    ///
384    /// This setting is disabled by default, and a [`min_tick_duration`](Self::min_tick_duration)
385    /// of 10ms is used as the default instead.
386    #[inline]
387    pub fn tick_rate(mut self, tick_rate: usize) -> Self {
388        self.tick_type = Some(TickType::Rate(tick_rate));
389        self
390    }
391
392    /// Specify the initialization closure.
393    ///
394    /// This closure will be called before the tick loop starts executing, in the same context as
395    /// the tick loop. For example, if the tick loop is threaded, this will be called in the same
396    /// thread.
397    ///
398    /// This is a required option.
399    #[inline]
400    pub fn init(mut self, f: FI) -> Self {
401        self.fn_init = Some(f);
402        self
403    }
404
405    /// Specify the tick closure.
406    ///
407    /// This closure will be called once for each tick, and is where the majority of the tick loops
408    /// work is expected to occur.
409    ///
410    /// This is a required option.
411    #[inline]
412    pub fn tick(mut self, f: FT) -> Self {
413        self.fn_tick = Some(f);
414        self
415    }
416
417    fn build(self) -> Result<TickLoop<FI, FT, D, E>, TickLoopBuildError<E>> {
418        let name = self.name
419            .unwrap_or("tick-loop".to_owned());
420        let tick_type = self.tick_type.unwrap_or_default();
421        let fn_init = self.fn_init
422            .ok_or(TickLoopBuildError::missing_option("init"))?;
423        let fn_tick = self.fn_tick
424            .ok_or(TickLoopBuildError::missing_option("tick"))?;
425
426        Ok(TickLoop {
427            name,
428            tick_type,
429            fn_init,
430            fn_tick,
431        })
432    }
433
434    /// Start the tick loop locally.
435    ///
436    /// Build the tick loop, and start it locally, in the thread that called this method. This
437    /// method will not return until the tick loop has stopped executing.
438    ///
439    /// # Errors
440    ///
441    /// Errors if there was a problem while building the tick loop.
442    pub fn start(self) -> Result<(), TickLoopBuildError<E>> {
443        let tick_loop = self.build()?;
444        match tick_loop.start() {
445            Ok(_) => Ok(()),
446            Err(err) => Err(TickLoopBuildError::InitError(err)),
447        }
448    }
449}
450
451impl<FI, FT, D, E> TickLoopBuilder<FI, FT, D, E>
452where
453    FI: (FnOnce() -> Result<D, E>) + Send + 'static,
454    FT: (FnMut(&mut D, Duration) -> bool) + Send + 'static,
455    D: 'static,
456    E: Send + 'static,
457{
458    /// Start the tick loop in a thread.
459    ///
460    /// Build the tick loop, and spawn a thread to run it in. This method yields a [TickLoopHandle]
461    /// that can be used to keep track of the threaded tick loop. When this method returns, the tick
462    /// loop will be started (but possibly still initializing).
463    ///
464    /// # Errors
465    ///
466    /// Errors if there was a problem while building the tick loop.
467    pub fn spawn(self) -> Result<TickLoopHandle, TickLoopBuildError<E>> {
468        let tick_loop = self.build()?;
469        match tick_loop.spawn() {
470            Ok(tlh) => Ok(tlh),
471            Err(err) => Err(TickLoopBuildError::InitError(err)),
472        }
473    }
474}