use educe::Educe;
use parking_lot::{Condvar, Mutex};
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use tracing::warn;
#[derive(Debug)]
enum TickType {
Rate(usize),
MinDuration(Duration),
}
impl Default for TickType {
#[inline]
fn default() -> Self {
Self::MinDuration(Duration::from_millis(10))
}
}
impl TickType {
fn next_tick(&self, last_tick: Instant, loop_name: &str) -> Option<Instant> {
let now = Instant::now();
match self {
Self::Rate(rate) => {
let tick_size = Duration::from_secs_f32(1.0 / *rate as f32);
let mut next_tick = last_tick;
next_tick += tick_size;
let mut skipped_ticks = 0;
while next_tick < now {
next_tick += tick_size;
skipped_ticks += 1;
}
if skipped_ticks > 0 {
let tick_time = now - last_tick;
warn!(skipped_ticks, ?tick_time, loop_name, "Tick(s) took too long");
}
Some(next_tick)
},
Self::MinDuration(min_duration) => {
let next_tick = last_tick + *min_duration;
if next_tick > now {
Some(next_tick)
} else {
None
}
},
}
}
}
#[derive(Educe)]
#[educe(Debug)]
struct TickLoop<FI, FT, D, E>
where
FI: FnOnce() -> Result<D, E>,
FT: FnMut(&mut D, Duration) -> bool,
{
name: String,
tick_type: TickType,
#[educe(Debug(ignore))]
fn_init: FI,
#[educe(Debug(ignore))]
fn_tick: FT,
}
impl<FI, FT, D, E> TickLoop<FI, FT, D, E>
where
FI: FnOnce() -> Result<D, E>,
FT: FnMut(&mut D, Duration) -> bool,
{
fn run(
should_exit: Arc<AtomicBool>,
name: &str,
tick_type: TickType,
mut fn_tick: FT,
mut data: D,
) {
let mut last_tick = Instant::now();
let mut next_tick = tick_type.next_tick(last_tick, name);
while !should_exit.load(Ordering::Relaxed) {
if let Some(next_tick) = next_tick {
let now = Instant::now();
if next_tick > now {
std::thread::sleep(next_tick - now);
}
}
let tick_start = Instant::now();
let delta = tick_start - last_tick;
if !(&mut fn_tick)(&mut data, delta) {
break;
}
last_tick = tick_start;
next_tick = tick_type.next_tick(tick_start, name);
}
}
fn start(self) -> Result<(), E> {
match (self.fn_init)() {
Ok(data) => {
let should_exit = Arc::new(AtomicBool::new(false));
Self::run(
should_exit,
&self.name,
self.tick_type,
self.fn_tick,
data,
);
Ok(())
},
Err(e) => Err(e),
}
}
}
impl<FI, FT, D, E> TickLoop<FI, FT, D, E>
where
FI: (FnOnce() -> Result<D, E>) + Send + 'static,
FT: (FnMut(&mut D, Duration) -> bool) + Send + 'static,
D: 'static,
E: Send + 'static,
{
fn spawn(self) -> Result<TickLoopHandle, E> {
let should_exit = Arc::new(AtomicBool::new(false));
let should_exit_thread = should_exit.clone();
let pair = Arc::new((Mutex::new(None), Condvar::new()));
let pair_thread = pair.clone();
let join_handle = Some(std::thread::Builder::new()
.name(self.name.clone())
.spawn(move || {
let &(ref lock, ref cvar) = &*pair_thread;
match (self.fn_init)() {
Ok(data) => {
{
let mut result = lock.lock();
*result = Some(Ok(()));
cvar.notify_one();
}
Self::run(
should_exit_thread,
&self.name,
self.tick_type,
self.fn_tick,
data,
);
},
Err(e) => {
let mut result = lock.lock();
*result = Some(Err(e));
cvar.notify_one();
}
}
})
.unwrap());
let &(ref lock, ref cvar) = &*pair;
let mut result = lock.lock();
if !result.is_some() {
cvar.wait(&mut result);
}
result.take().unwrap()?;
Ok(TickLoopHandle {
join_handle,
should_exit,
})
}
}
#[derive(Educe)]
#[educe(Debug)]
pub struct TickLoopHandle {
#[educe(Debug(ignore))]
join_handle: Option<JoinHandle<()>>,
should_exit: Arc<AtomicBool>,
}
impl Drop for TickLoopHandle {
fn drop(&mut self) {
if let Some(join_handle) = self.join_handle.take() {
self.should_exit.store(true, Ordering::Relaxed);
join_handle.join().unwrap();
} else {
warn!("TickLoop internal thread already joined");
}
}
}
#[derive(Debug)]
pub enum TickLoopBuildError<E> {
MissingOption { name: String },
InitError(E),
}
impl<E: Display> Display for TickLoopBuildError<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingOption { name } => write!(f, "Missing required option: '{name}'"),
Self::InitError(err) => write!(f, "Initialization failed: {err}"),
}
}
}
impl<E: Debug + Display> Error for TickLoopBuildError<E> {}
impl<E> TickLoopBuildError<E> {
fn missing_option(name: impl Into<String>) -> Self {
Self::MissingOption { name: name.into() }
}
}
pub struct TickLoopBuilder<FI, FT, D: 'static = (), E = ()>
where
FI: FnOnce() -> Result<D, E>,
FT: FnMut(&mut D, Duration) -> bool,
{
name: Option<String>,
tick_type: Option<TickType>,
fn_init: Option<FI>,
fn_tick: Option<FT>,
}
impl<FI, FT, D, E> TickLoopBuilder<FI, FT, D, E>
where
FI: FnOnce() -> Result<D, E>,
FT: FnMut(&mut D, Duration) -> bool,
{
#[inline]
pub fn new() -> Self {
Self {
name: None,
tick_type: None,
fn_init: None,
fn_tick: None,
}
}
#[inline]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[inline]
pub fn min_tick_duration(mut self, min_tick_duration: Duration) -> Self {
self.tick_type = Some(TickType::MinDuration(min_tick_duration));
self
}
#[inline]
pub fn tick_rate(mut self, tick_rate: usize) -> Self {
self.tick_type = Some(TickType::Rate(tick_rate));
self
}
#[inline]
pub fn init(mut self, f: FI) -> Self {
self.fn_init = Some(f);
self
}
#[inline]
pub fn tick(mut self, f: FT) -> Self {
self.fn_tick = Some(f);
self
}
fn build(self) -> Result<TickLoop<FI, FT, D, E>, TickLoopBuildError<E>> {
let name = self.name
.unwrap_or("tick-loop".to_owned());
let tick_type = self.tick_type.unwrap_or_default();
let fn_init = self.fn_init
.ok_or(TickLoopBuildError::missing_option("init"))?;
let fn_tick = self.fn_tick
.ok_or(TickLoopBuildError::missing_option("tick"))?;
Ok(TickLoop {
name,
tick_type,
fn_init,
fn_tick,
})
}
pub fn start(self) -> Result<(), TickLoopBuildError<E>> {
let tick_loop = self.build()?;
match tick_loop.start() {
Ok(_) => Ok(()),
Err(err) => Err(TickLoopBuildError::InitError(err)),
}
}
}
impl<FI, FT, D, E> TickLoopBuilder<FI, FT, D, E>
where
FI: (FnOnce() -> Result<D, E>) + Send + 'static,
FT: (FnMut(&mut D, Duration) -> bool) + Send + 'static,
D: 'static,
E: Send + 'static,
{
pub fn spawn(self) -> Result<TickLoopHandle, TickLoopBuildError<E>> {
let tick_loop = self.build()?;
match tick_loop.spawn() {
Ok(tlh) => Ok(tlh),
Err(err) => Err(TickLoopBuildError::InitError(err)),
}
}
}