Skip to main content

crabshell_ui/
timer.rs

1//! Timers: callbacks that run once after a delay or again every period,
2//! from a table the platform wakes for only while a timer runs.
3
4use std::cell::RefCell;
5use std::collections::BTreeMap;
6use std::marker::PhantomData;
7use std::panic::{self, AssertUnwindSafe, Location};
8use std::rc::Rc;
9use std::time::{Duration, Instant};
10
11use crabshell_reactive::{Owner, Signal, untrack};
12
13use crate::cx::Cx;
14use crate::input::panic_message;
15
16/// The shortest interval, so a period of 0 can't spin the loop.
17const MIN_PERIOD: Duration = Duration::from_millis(1);
18/// The longest delay or period, so adding it to now can't overflow.
19const MAX_PERIOD: Duration = Duration::from_secs(u32::MAX as u64);
20
21/// A timer made by [`Cx::after`] or [`Cx::interval`]. It is a `Copy`
22/// handle, like a signal, so closures capture it without cloning. Calls on
23/// a timer whose context is gone do nothing.
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub struct Timer {
26  id: u64,
27  /// The table is per thread, like the reactive runtime.
28  _not_send: PhantomData<*const ()>,
29}
30
31struct Entry {
32  owner: Owner,
33  /// Names the timer in log lines, as `cx.after at src/main.rs:12`.
34  label: Rc<str>,
35  period: Duration,
36  repeats: bool,
37  /// When it fires next. `None` while stopped.
38  deadline: Option<Instant>,
39  /// Whether it is running, for bindings to read.
40  running: Signal<bool>,
41  /// `None` while the callback runs.
42  callback: Option<Box<dyn FnMut()>>,
43}
44
45thread_local! {
46  /// Ids are never reused, so a stale handle never reaches a newer timer.
47  static TIMERS: RefCell<(u64, BTreeMap<u64, Entry>)> = const { RefCell::new((0, BTreeMap::new())) };
48}
49
50impl Cx {
51  /// Runs `f` once, `delay` from now. The timer belongs to this context
52  /// and goes with it; [`Timer::restart`] runs it again.
53  ///
54  /// ```
55  /// use std::time::Duration;
56  /// use crabshell_ui::Cx;
57  ///
58  /// fn osd(cx: &Cx) {
59  ///     let hide = cx.after(Duration::from_millis(1500), || {});
60  ///     // Show the OSD while `hide.running()`, and call `hide.restart()`
61  ///     // on each change.
62  /// #   let _ = hide;
63  /// }
64  /// ```
65  #[track_caller]
66  pub fn after(&self, delay: Duration, f: impl FnMut() + 'static) -> Timer {
67    let label = format!("cx.after at {}", Location::caller());
68    add(self, label, delay.min(MAX_PERIOD), false, Box::new(f))
69  }
70
71  /// Runs `f` every `period`, the first time `period` from now. A late
72  /// wakeup fires once and keeps the schedule; after a stall longer than
73  /// a period the next fire is a period from then, never a burst. The
74  /// timer belongs to this context and goes with it.
75  #[track_caller]
76  pub fn interval(&self, period: Duration, f: impl FnMut() + 'static) -> Timer {
77    let label = format!("cx.interval at {}", Location::caller());
78    let period = period.clamp(MIN_PERIOD, MAX_PERIOD);
79    add(self, label, period, true, Box::new(f))
80  }
81}
82
83fn add(cx: &Cx, label: String, period: Duration, repeats: bool, f: Box<dyn FnMut()>) -> Timer {
84  let running = cx.signal(true);
85  let entry = Entry {
86    owner: cx.owner(),
87    label: label.into(),
88    period,
89    repeats,
90    deadline: Some(Instant::now() + period),
91    running,
92    callback: Some(f),
93  };
94  let id = TIMERS.with(|timers| {
95    let (next, table) = &mut *timers.borrow_mut();
96    *next += 1;
97    table.insert(*next, entry);
98    *next
99  });
100  Timer {
101    id,
102    _not_send: PhantomData,
103  }
104}
105
106impl Timer {
107  /// Stops the timer before it fires again.
108  pub fn stop(&self) {
109    self.set_deadline(|_| None);
110  }
111
112  /// Counts the delay or period again from now, so a timer restarted on
113  /// every change fires only once the changes stop. It also starts a
114  /// one-shot that fired or was stopped.
115  pub fn restart(&self) {
116    self.set_deadline(|period| Some(Instant::now() + period));
117  }
118
119  /// Whether the timer will fire. Read in a binding, it reruns the binding
120  /// when that changes.
121  pub fn running(&self) -> bool {
122    let running = TIMERS.with(|timers| timers.borrow().1.get(&self.id).map(|entry| entry.running));
123    running
124      .and_then(|running| running.try_get())
125      .unwrap_or(false)
126  }
127
128  fn set_deadline(&self, deadline: impl FnOnce(Duration) -> Option<Instant>) {
129    let changed = TIMERS.with(|timers| {
130      let mut timers = timers.borrow_mut();
131      let entry = timers.1.get_mut(&self.id)?;
132      if entry.owner.is_disposed() {
133        return None;
134      }
135      entry.deadline = deadline(entry.period);
136      Some((entry.running, entry.deadline.is_some()))
137    });
138    if let Some((signal, running)) = changed {
139      set_running(signal, running);
140    }
141  }
142}
143
144/// Writes `running` only when it changes, since a signal notifies on
145/// every write.
146fn set_running(signal: Signal<bool>, running: bool) {
147  if untrack(|| signal.try_with(|&was| was != running)) == Some(true) {
148    signal.try_set(running);
149  }
150}
151
152/// When the earliest running timer is due, for the platform to wake then.
153/// `None` means no timer runs and nothing needs waking. Timers whose
154/// context is gone are freed first, so they never wake the loop.
155pub fn next_timer() -> Option<Instant> {
156  sweep();
157  TIMERS.with(|timers| {
158    let timers = timers.borrow();
159    timers.1.values().filter_map(|entry| entry.deadline).min()
160  })
161}
162
163/// Runs every timer due at `now`, earliest first. A timer started while
164/// they run first fires at a later call. One whose callback panics is
165/// logged and stops; the others still fire.
166pub fn fire_timers(now: Instant) {
167  sweep();
168  let mut due: Vec<(Instant, u64)> = TIMERS.with(|timers| {
169    let timers = timers.borrow();
170    let due = timers.1.iter().filter_map(|(&id, entry)| {
171      let deadline = entry.deadline.filter(|&deadline| deadline <= now)?;
172      Some((deadline, id))
173    });
174    due.collect()
175  });
176  due.sort_unstable();
177  for (_, id) in due {
178    fire(id, now);
179  }
180}
181
182fn fire(id: u64, now: Instant) {
183  // The callback runs with the table released, so it may start, stop or
184  // restart any timer, itself included.
185  let taken = TIMERS.with(|timers| {
186    let mut timers = timers.borrow_mut();
187    let entry = timers.1.get_mut(&id)?;
188    // An earlier callback may have stopped it, restarted it or dropped
189    // its context.
190    let deadline = entry.deadline.filter(|&deadline| deadline <= now)?;
191    if entry.owner.is_disposed() {
192      return None;
193    }
194    let callback = entry.callback.take()?;
195    entry.deadline = entry.repeats.then(|| {
196      let next = deadline + entry.period;
197      if next > now { next } else { now + entry.period }
198    });
199    let entry = &*entry;
200    Some((
201      callback,
202      Rc::clone(&entry.label),
203      entry.owner,
204      entry.running,
205      entry.deadline.is_some(),
206    ))
207  });
208  let Some((mut callback, label, owner, running, again)) = taken else {
209    return;
210  };
211  if !again {
212    set_running(running, false);
213  }
214  let panicked = panic::catch_unwind(AssertUnwindSafe(&mut callback)).err();
215  if let Some(payload) = &panicked {
216    let place = owner
217      .name()
218      .map(|name| format!(" in {name}"))
219      .unwrap_or_default();
220    tracing::error!(
221      "timer {label}{place} panicked and stops: {}",
222      panic_message(payload.as_ref())
223    );
224  }
225  let orphan = TIMERS.with(|timers| {
226    let mut timers = timers.borrow_mut();
227    let Some(entry) = timers.1.get_mut(&id) else {
228      return Some(callback);
229    };
230    if panicked.is_some() {
231      entry.deadline = None;
232    }
233    entry.callback = Some(callback);
234    None
235  });
236  if panicked.is_some() {
237    set_running(running, false);
238  }
239  // Freed while it ran, by a sweep inside it.
240  if let Some(callback) = orphan {
241    drop_isolated(&label, callback);
242  }
243}
244
245/// Frees the timers whose context is gone, dropping their callbacks with
246/// the table released.
247fn sweep() {
248  let gone: Vec<Entry> = TIMERS.with(|timers| {
249    let mut timers = timers.borrow_mut();
250    let ids: Vec<u64> = timers
251      .1
252      .iter()
253      .filter(|(_, entry)| entry.owner.is_disposed())
254      .map(|(&id, _)| id)
255      .collect();
256    ids.iter().filter_map(|id| timers.1.remove(id)).collect()
257  });
258  for entry in gone {
259    if let Some(callback) = entry.callback {
260      drop_isolated(&entry.label, callback);
261    }
262  }
263}
264
265/// Drops a callback, and the user's values it holds, isolated.
266fn drop_isolated(label: &str, callback: Box<dyn FnMut()>) {
267  if let Err(payload) = panic::catch_unwind(AssertUnwindSafe(move || drop(callback))) {
268    tracing::error!(
269      "dropping timer {label} panicked: {}",
270      panic_message(payload.as_ref())
271    );
272  }
273}