future_timed/
lib.rs

1//! Future timing instrumentation.
2//!
3//! Provides instrumentation to record the time taken by a future. This includes the busy time and
4//! the idle time. The busy time of a future is the sum of all the time consumed during calls to
5//! [`Future::poll`] on that future. On the other hand, The idle time of a future is the sum of all
6//! the time between calls to [`Future::poll`]. The time before the first poll is not included.
7//!
8//! # Usage
9//!
10//! Add `future-timed` to your Cargo.toml `dependencies`:
11//!
12//! ```toml
13//! future-timed = "0.1"
14//! ```
15//!
16//! Use the [`TimedFutureExt`] extension trait to add an instrumentation closure
17//! on a future:
18//!
19//! ```
20//! # use future_timed::{TimedFutureExt, Timing};
21//! # use std::time::Duration;
22//! # async fn some_async_fn() -> u64 {
23//! #   tokio::time::sleep(Duration::from_micros(10)).await;
24//! #   42
25//! # }
26//! # fn do_something_with_output(_: u64) {}
27//! # #[tokio::main]
28//! # async fn main() {
29//!     let output = some_async_fn()
30//!         .timed(|Timing { idle, busy }| {
31//!             assert!(!idle.is_zero());
32//!             assert!(!busy.is_zero());
33//!         })
34//!         .await;
35//!
36//!     do_something_with_output(output);
37//! # }
38//! ```
39//!
40//! # Comparison with similar crates
41//!
42//! This work is based almost entirely on the [future-timing] crate but sports a different API.
43//! While future-timing requires destructuring the future's output into the timing data and the
44//! futures output itself, fuiture-timed allows to report inline and compose the output with
45//! subsequent future combinators, for example from the [futures] crate:
46//!
47//! ```
48//! # use future_timed::{TimedFutureExt, Timing};
49//! # use futures::future::FutureExt;
50//! # use std::time::Duration;
51//! # #[tokio::main]
52//! # async fn main() {
53//! let output = async {
54//!         tokio::time::sleep(Duration::from_micros(10)).await;
55//!         21
56//!     }
57//!     .timed(|Timing { busy, .. }| {
58//!         println!("busy for {busy:?}");
59//!     })
60//!     .map(|n| 2 * n)
61//!     .await;
62//!
63//! assert_eq!(output, 42);
64//! # }
65//! ```
66//!
67//! Note that in that case you measure the combined time for all wrapped futures.
68//!
69//! # License
70//!
71//! This project is licensed under the [MIT license].
72//!
73//! [MIT license]: https://github.com/matze/future-timed/blob/main/LICENSE
74//! [future-timing]: https://docs.rs/future-timing/latest/future_timing/
75//! [futures]: https://docs.rs/futures/latest/futures/index.html
76
77use std::future::Future;
78use std::pin::Pin;
79use std::task::{Context, Poll};
80use std::time::{Duration, Instant};
81
82use pin_project_lite::pin_project;
83
84/// Instrument a future to record its timing.
85///
86/// The busy and idle time for the future will be passed as an argument to the provided closure.
87/// See the documentation for [`Timing`] for more details. In general, it is more straightforward
88/// to use the [`TimedFutureExt`] extension trait to attach instrument a future directly.
89///
90/// # Examples
91///
92/// ```
93/// use future_timed::{timed, Timing};
94/// # async fn some_async_fn() -> u64 {
95/// #   tokio::time::sleep(std::time::Duration::from_micros(10)).await;
96/// #   42
97/// # }
98/// # fn do_something_with_output(_: u64) {}
99/// # #[tokio::main]
100/// # async fn main() {
101///     let output = timed(some_async_fn(), |Timing { idle, busy }| {
102///         assert!(!idle.is_zero());
103///         assert!(!busy.is_zero());
104///     })
105///     .await;
106///
107///     do_something_with_output(output);
108/// # }
109pub fn timed<Fut, F>(fut: Fut, f: F) -> Timed<Fut, F>
110where
111    Fut: Future,
112    F: FnOnce(Timing),
113{
114    Timed::new(fut, f)
115}
116
117pin_project! {
118    /// Future for the [`timed`] function and [`timed`](TimedFutureExt::timed) method.
119    pub struct Timed<Fut, F> where Fut: Future, F: FnOnce(Timing) {
120        last_poll_end: Option<Instant>,
121        timing: Timing,
122        op: Option<F>,
123        #[pin]
124        inner: Fut,
125    }
126}
127
128impl<Fut, F> Timed<Fut, F>
129where
130    Fut: Future,
131    F: FnOnce(Timing),
132{
133    fn new(inner: Fut, op: F) -> Self {
134        let timing = Timing {
135            idle: Duration::ZERO,
136            busy: Duration::ZERO,
137        };
138
139        Self {
140            last_poll_end: None,
141            timing,
142            op: Some(op),
143            inner,
144        }
145    }
146}
147
148impl<Fut, F> Future for Timed<Fut, F>
149where
150    Fut: Future,
151    F: FnOnce(Timing),
152{
153    type Output = Fut::Output;
154
155    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
156        let start = Instant::now();
157        let mut this = self.project();
158        let result = this.inner.as_mut().poll(cx);
159        let end = Instant::now();
160
161        if let Some(last_poll_end) = this.last_poll_end.take() {
162            this.timing.idle += start - last_poll_end;
163        }
164
165        this.timing.busy += end - start;
166        *this.last_poll_end = Some(end);
167
168        match result {
169            Poll::Pending => Poll::Pending,
170            Poll::Ready(output) => {
171                if let Some(op) = this.op.take() {
172                    op(*this.timing);
173                }
174                Poll::Ready(output)
175            }
176        }
177    }
178}
179
180/// An extension trait for `Future`s that adds the [`timed`] method.
181pub trait TimedFutureExt: Future {
182    /// Instrument a future to record its timing
183    ///
184    /// The busy and idle time for the future will be passed as an argument to the provided
185    /// closure. See the documentation for [`Timing`] for more details.
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// use future_timed::{TimedFutureExt, Timing};
191    /// # use std::time::Duration;
192    /// # #[tokio::main]
193    /// # async fn main() {
194    ///
195    /// let output = async {
196    ///     // Block the executor
197    ///     std::thread::sleep(Duration::from_micros(200));
198    ///     tokio::time::sleep(Duration::from_micros(10)).await;
199    ///     42
200    /// }.timed(|Timing { idle, busy }| {
201    ///     assert!(idle > Duration::from_micros(10));
202    ///     assert!(busy > Duration::from_micros(200));
203    /// })
204    /// . await;
205    ///
206    /// assert_eq!(output, 42);
207    /// # }
208    fn timed<F>(self, f: F) -> Timed<Self, F>
209    where
210        Self: Sized,
211        F: FnOnce(Timing),
212    {
213        Timed::new(self, f)
214    }
215}
216
217impl<T: Future> TimedFutureExt for T {}
218
219/// Timing information for an instrumented future.
220#[derive(Clone, Copy, Debug, Hash, PartialEq)]
221pub struct Timing {
222    /// The idle time of a future is the sum of all the time between calls to [`Future::poll`]. The
223    /// time before the first poll is not included.
224    pub idle: Duration,
225    /// The busy time of a future is the sum of all the time consumed during calls to [`Future::poll`]
226    /// on that future.
227    pub busy: Duration,
228}