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}