use crate::arena::EffectId;
use indexmap::IndexSet;
use std::sync::mpsc::{self, Sender, TryRecvError};
use std::sync::{Arc, Condvar, Mutex, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
static EFFECT_NOTIFIER: OnceLock<Sender<()>> = OnceLock::new();
static BACKGROUND_WORKER: OnceLock<BackgroundWorker> = OnceLock::new();
pub fn notify_effect_loop() {
if let Some(sender) = EFFECT_NOTIFIER.get() {
let _ = sender.send(());
}
}
pub const DEFAULT_DEBOUNCE: Duration = Duration::from_millis(4);
pub const DEFAULT_MAX_DEBOUNCE: Duration = Duration::from_millis(16);
pub const DEFAULT_BUDGET: Duration = Duration::from_millis(16);
#[allow(clippy::type_complexity)]
pub struct EffectLoop {
debounce: Duration,
max_debounce: Duration,
budget: Duration,
spawn_fn: Option<Box<dyn FnOnce(Box<dyn FnOnce() + Send>) -> JoinHandle<()> + Send>>,
}
impl Default for EffectLoop {
fn default() -> Self {
Self::new()
}
}
impl EffectLoop {
pub fn new() -> Self {
Self {
debounce: DEFAULT_DEBOUNCE,
max_debounce: DEFAULT_MAX_DEBOUNCE,
budget: DEFAULT_BUDGET,
spawn_fn: None,
}
}
pub fn debounce(mut self, duration: Duration) -> Self {
self.debounce = duration;
self
}
pub fn max_debounce(mut self, duration: Duration) -> Self {
self.max_debounce = duration;
self
}
pub fn budget(mut self, duration: Duration) -> Self {
self.budget = duration;
self
}
pub fn spawn_fn<F>(mut self, f: F) -> Self
where
F: FnOnce(Box<dyn FnOnce() + Send>) -> JoinHandle<()> + Send + 'static,
{
self.spawn_fn = Some(Box::new(f));
self
}
pub fn spawn(self) -> JoinHandle<()> {
let (tx, rx) = mpsc::channel::<()>();
let _ = EFFECT_NOTIFIER.set(tx);
let _ = BACKGROUND_WORKER.get_or_init(|| BackgroundWorker::new(self.spawn_fn.is_some()));
let debounce = self.debounce;
let max_debounce = self.max_debounce;
let budget = self.budget;
let loop_fn: Box<dyn FnOnce() + Send> = Box::new(move || {
effect_loop(rx, debounce, max_debounce, budget);
});
match self.spawn_fn {
Some(spawn_fn) => spawn_fn(loop_fn),
None => thread::spawn(loop_fn),
}
}
}
fn effect_loop(
rx: mpsc::Receiver<()>,
debounce: Duration,
max_debounce: Duration,
budget: Duration,
) {
let mut must_run = Vec::with_capacity(64);
let mut skippable = Vec::with_capacity(64);
loop {
if rx.recv().is_err() {
break;
}
let debounce_start = Instant::now();
loop {
if debounce_start.elapsed() >= max_debounce {
break;
}
let remaining_max = max_debounce.saturating_sub(debounce_start.elapsed());
let timeout = debounce.min(remaining_max);
match rx.recv_timeout(timeout) {
Ok(()) => {
}
Err(mpsc::RecvTimeoutError::Timeout) => {
break;
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return;
}
}
}
loop {
match rx.try_recv() {
Ok(()) => continue,
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return,
}
}
crate::transaction::wait_for_transactions();
crate::effect::flush_effects_with_budget(budget, &mut must_run, &mut skippable);
if !skippable.is_empty() {
if let Some(worker) = BACKGROUND_WORKER.get() {
worker.submit(skippable.drain(..));
}
}
}
}
struct BackgroundWorker {
inner: Arc<BackgroundWorkerInner>,
#[allow(dead_code)]
handle: JoinHandle<()>,
}
struct BackgroundWorkerInner {
pending: Mutex<IndexSet<EffectId>>,
condvar: Condvar,
}
impl BackgroundWorker {
fn new(has_custom_spawn: bool) -> Self {
let inner = Arc::new(BackgroundWorkerInner {
pending: Mutex::new(IndexSet::new()),
condvar: Condvar::new(),
});
let worker_inner = Arc::clone(&inner);
let worker_fn: Box<dyn FnOnce() + Send> = Box::new(move || {
Self::worker_loop(worker_inner);
});
let handle = if has_custom_spawn {
thread::spawn(worker_fn)
} else {
thread::spawn(worker_fn)
};
BackgroundWorker { inner, handle }
}
fn submit(&self, effects: impl Iterator<Item = EffectId>) {
{
let mut pending = self.inner.pending.lock().unwrap();
pending.extend(effects);
}
self.inner.condvar.notify_one();
}
fn worker_loop(inner: Arc<BackgroundWorkerInner>) {
loop {
let effect_id = {
let mut pending = inner.pending.lock().unwrap();
loop {
if let Some(id) = pending.pop() {
break id;
}
pending = inner.condvar.wait(pending).unwrap();
}
};
effect_id.update_if_necessary(false);
}
}
}
pub fn spawn_effect_loop() -> JoinHandle<()> {
EffectLoop::new().spawn()
}