Skip to main content

proxima_core/time/
mod.rs

1//! `proxima_core::time` (re-exported as `proxima::time`) — runtime-agnostic
2//! timer primitives shaped like `tokio::time`. The active backend (host
3//! thread, embassy, prime's per-core wheel, deterministic mock, or a
4//! user-supplied hardware driver) is selected by the active
5//! `proxima_build::Profile` via the `timer` axis; `build.rs` bakes a
6//! `&'static dyn Driver` symbol into the build, so [`Sleep::poll`]'s only
7//! runtime cost is one virtual dispatch.
8//!
9//! Folded in from the former `proxima-time` satellite crate (single
10//! consumer: the workspace's timer surface) — `proxima_core::time::{sleep,
11//! timeout, interval, Driver, Instant}` is unchanged for callers that used
12//! to spell it `proxima_time::{..}`.
13//!
14//! [`Sleep::poll`]: core::future::Future::poll
15
16use core::future::Future;
17use core::pin::Pin;
18use core::task::{Context, Poll};
19use core::time::Duration;
20
21use futures::stream::Stream;
22use pin_project_lite::pin_project;
23
24pub mod drivers;
25mod missed_tick;
26mod types;
27
28// `Driver` + `Instant` live in the dependency-free `types` module so a
29// runtime crate (prime) can impl `Driver` + export `TIMER_DRIVER` without a
30// cycle back through this crate's std-tier machinery. Re-exported so
31// `proxima_core::time::{Driver, Instant}` is unchanged for callers.
32pub use missed_tick::MissedTickBehavior;
33pub use types::{Driver, Instant};
34
35include!(concat!(env!("OUT_DIR"), "/proxima_time_bound_driver.rs"));
36
37/// Capture the current monotonic time from the link-time-bound driver. (The
38/// driver-free `Instant` in the leaf cannot reach `BOUND_DRIVER`, so `now()`
39/// lives here.)
40#[must_use]
41pub fn now() -> Instant {
42    BOUND_DRIVER.now()
43}
44
45/// Returned by [`timeout`] when the supplied duration passes before the
46/// wrapped future completes. Matches `tokio::time::error::Elapsed`.
47#[derive(Debug, thiserror::Error)]
48#[error("deadline has elapsed")]
49pub struct Elapsed(());
50
51/// Future returned by [`sleep`] / [`sleep_until`]. Resolves when the
52/// active driver's `now()` reaches `deadline`.
53pub struct Sleep {
54    deadline: Instant,
55}
56
57impl Sleep {
58    /// Construct from an absolute deadline.
59    #[must_use]
60    pub fn until(deadline: Instant) -> Self {
61        Self { deadline }
62    }
63
64    /// The deadline this `Sleep` is waiting on.
65    #[must_use]
66    pub fn deadline(&self) -> Instant {
67        self.deadline
68    }
69}
70
71impl Future for Sleep {
72    type Output = ();
73    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
74        if BOUND_DRIVER.now() >= self.deadline {
75            Poll::Ready(())
76        } else {
77            BOUND_DRIVER.schedule_wake(self.deadline, context.waker().clone());
78            Poll::Pending
79        }
80    }
81}
82
83/// Resolves after `duration` has elapsed on the bound driver's clock.
84/// Shape-compatible with `tokio::time::sleep`.
85#[must_use]
86pub fn sleep(duration: Duration) -> Sleep {
87    Sleep::until(now() + duration)
88}
89
90/// Resolves at the given deadline. Shape-compatible with
91/// `tokio::time::sleep_until`. If the deadline is in the past, the
92/// returned future resolves on first poll.
93#[must_use]
94pub fn sleep_until(deadline: Instant) -> Sleep {
95    Sleep::until(deadline)
96}
97
98pin_project! {
99    /// Future returned by [`timeout`] / [`timeout_at`].
100    ///
101    /// First poll checks the inner future before scheduling any wake.
102    /// `deadline` is captured up front; on each `Pending` from the inner
103    /// future, we re-check the clock and schedule a wake.
104    struct Timeout<Fut> {
105        #[pin]
106        future: Fut,
107        deadline: Instant,
108    }
109}
110
111impl<Fut: Future> Future for Timeout<Fut> {
112    type Output = Result<Fut::Output, Elapsed>;
113    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
114        let this = self.project();
115
116        if let Poll::Ready(value) = this.future.poll(context) {
117            return Poll::Ready(Ok(value));
118        }
119
120        if BOUND_DRIVER.now() >= *this.deadline {
121            return Poll::Ready(Err(Elapsed(())));
122        }
123
124        BOUND_DRIVER.schedule_wake(*this.deadline, context.waker().clone());
125        Poll::Pending
126    }
127}
128
129/// Race `future` against a timer of `duration`. Returns `Err(Elapsed)`
130/// if the timer fires first.
131pub fn timeout<Fut>(
132    duration: Duration,
133    future: Fut,
134) -> impl Future<Output = Result<Fut::Output, Elapsed>>
135where
136    Fut: Future,
137{
138    Timeout {
139        future,
140        deadline: now() + duration,
141    }
142}
143
144/// Race `future` against a deadline. Shape-compatible with
145/// `tokio::time::timeout_at`.
146pub fn timeout_at<Fut>(
147    deadline: Instant,
148    future: Fut,
149) -> impl Future<Output = Result<Fut::Output, Elapsed>>
150where
151    Fut: Future,
152{
153    Timeout { future, deadline }
154}
155
156/// Stream + tick-future hybrid that yields every `period`, with the
157/// first item delivered immediately. Matches `tokio::time::interval`
158/// ergonomically (`.tick().await -> Instant`) AND exposes
159/// `Stream<Item = ()>` for callers that prefer that shape.
160#[must_use]
161pub fn interval(period: Duration) -> Interval {
162    Interval {
163        period,
164        next_deadline: None,
165        missed_tick_behavior: MissedTickBehavior::default(),
166    }
167}
168
169/// Like [`interval`] but the first tick fires at `start` rather than
170/// immediately.
171#[must_use]
172pub fn interval_at(start: Instant, period: Duration) -> Interval {
173    Interval {
174        period,
175        next_deadline: Some(start),
176        missed_tick_behavior: MissedTickBehavior::default(),
177    }
178}
179
180/// Periodic timer. `.tick().await` returns the [`Instant`] at which the
181/// tick fired (matching tokio); also implements `Stream<Item = ()>`.
182pub struct Interval {
183    period: Duration,
184    /// Absolute deadline of the next pending tick. `None` means
185    /// "first tick should fire immediately" (post-construction state).
186    next_deadline: Option<Instant>,
187    missed_tick_behavior: MissedTickBehavior,
188}
189
190impl Interval {
191    /// Set the missed-tick behavior. Default is `Burst`, matching tokio.
192    pub fn set_missed_tick_behavior(&mut self, behavior: MissedTickBehavior) {
193        self.missed_tick_behavior = behavior;
194    }
195
196    /// Current missed-tick behavior.
197    #[must_use]
198    pub fn missed_tick_behavior(&self) -> MissedTickBehavior {
199        self.missed_tick_behavior
200    }
201
202    /// Configured period.
203    #[must_use]
204    pub fn period(&self) -> Duration {
205        self.period
206    }
207
208    /// Future resolving to the [`Instant`] at which the tick fires.
209    pub fn tick(&mut self) -> Tick<'_> {
210        Tick { interval: self }
211    }
212}
213
214/// Future returned by [`Interval::tick`].
215pub struct Tick<'a> {
216    interval: &'a mut Interval,
217}
218
219impl Future for Tick<'_> {
220    type Output = Instant;
221    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Instant> {
222        let now = now();
223        let interval = &mut *self.interval;
224
225        // First tick fires immediately; arm the deadline for the next.
226        if interval.next_deadline.is_none() {
227            interval.next_deadline = Some(now + interval.period);
228            return Poll::Ready(now);
229        }
230
231        let Some(deadline) = interval.next_deadline else {
232            return Poll::Pending;
233        };
234
235        if now < deadline {
236            BOUND_DRIVER.schedule_wake(deadline, context.waker().clone());
237            return Poll::Pending;
238        }
239
240        // Tick has matured. Advance the deadline per missed-tick behavior.
241        let fired_at = deadline;
242        let next_deadline = match interval.missed_tick_behavior {
243            MissedTickBehavior::Burst => fired_at + interval.period,
244            MissedTickBehavior::Delay => now + interval.period,
245            MissedTickBehavior::Skip => {
246                let elapsed_periods = now
247                    .saturating_duration_since(fired_at)
248                    .as_nanos()
249                    .checked_div(interval.period.as_nanos().max(1))
250                    .unwrap_or(0);
251                let skip = u32::try_from(elapsed_periods).unwrap_or(u32::MAX);
252                fired_at + interval.period.saturating_mul(skip.saturating_add(1))
253            }
254        };
255        interval.next_deadline = Some(next_deadline);
256        Poll::Ready(fired_at)
257    }
258}
259
260impl Stream for Interval {
261    type Item = ();
262    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<()>> {
263        let interval: &mut Interval = &mut self;
264        let mut tick = Tick { interval };
265        match Pin::new(&mut tick).poll(context) {
266            Poll::Ready(_instant) => Poll::Ready(Some(())),
267            Poll::Pending => Poll::Pending,
268        }
269    }
270}
271
272// sleep/timeout/interval all dispatch through the bound driver (see
273// drivers/unbound.rs); these tests assert on real elapsed wall-clock
274// time, so they need time-driver-std-thread specifically, not just any
275// driver (time-driver-mock's virtual clock wouldn't actually delay).
276#[cfg(all(test, feature = "time-driver-std-thread"))]
277#[allow(clippy::unwrap_used, clippy::expect_used)]
278mod tests {
279    use super::*;
280    use futures::executor::block_on;
281    use futures::stream::StreamExt;
282
283    #[test]
284    fn sleep_resolves_after_duration() {
285        let start = now();
286        block_on(sleep(Duration::from_millis(30)));
287        assert!(now().saturating_duration_since(start) >= Duration::from_millis(25));
288    }
289
290    #[test]
291    fn timeout_returns_ok_when_future_completes_in_time() {
292        let outcome = block_on(timeout(Duration::from_millis(100), async { 7_u32 }));
293        assert_eq!(outcome.expect("ok"), 7);
294    }
295
296    #[test]
297    fn timeout_returns_elapsed_when_timer_fires_first() {
298        let outcome: Result<u32, Elapsed> = block_on(timeout(Duration::from_millis(10), async {
299            sleep(Duration::from_secs(1)).await;
300            7_u32
301        }));
302        assert!(outcome.is_err());
303    }
304
305    #[test]
306    fn interval_emits_first_tick_immediately_then_periodic() {
307        block_on(async {
308            let mut interval = interval(Duration::from_millis(20));
309            let start = now();
310            for _ in 0..3_u32 {
311                interval.next().await.expect("tick");
312            }
313            // first tick is immediate; remaining 2 take ~40ms
314            assert!(now().saturating_duration_since(start) >= Duration::from_millis(30));
315        });
316    }
317}