yield_progress/
lib.rs

1//! This library provides the `YieldProgress` type, which allows a long-running async task
2//! to report its progress, while also yielding to the scheduler (e.g. for the
3//! single-threaded web/Wasm environment) and introducing cancellation points.
4//!
5//! These things go together because the rate at which it makes sense to yield (to avoid
6//! event loop hangs) is similar to the rate at which it makes sense to report progress.
7//!
8//! `YieldProgress` is executor-independent; when it is constructed, the caller provides a
9//! function for yielding.
10//!
11//! # Crate feature flags
12//!
13//! * `sync` (default): Implements `YieldProgress: Send + Sync` for use with multi-threaded executors.
14//!
15//!   Requires `std` to be available for the compilation target.
16//!
17//! * `log_hiccups`: Log intervals between yields longer than 100 ms, via the [`log`] library.
18//!
19//!   Requires `std` to be available for the compilation target.
20//!   This might be removed in favor of something more configurable in future versions,
21//!   in which case the feature flag may still exist but do nothing.
22//!
23//! [`log`]: https://docs.rs/log/0.4/
24
25#![no_std]
26#![deny(elided_lifetimes_in_paths)]
27#![forbid(unsafe_code)]
28#![warn(clippy::cast_lossless)]
29#![warn(clippy::exhaustive_enums)]
30#![warn(clippy::exhaustive_structs)]
31#![warn(clippy::missing_panics_doc)]
32#![warn(clippy::return_self_not_must_use)]
33#![warn(clippy::wrong_self_convention)]
34#![warn(missing_docs)]
35#![warn(unused_lifetimes)]
36#![warn(unused_qualifications)]
37
38extern crate alloc;
39
40#[cfg(any(test, feature = "sync"))]
41#[cfg_attr(test, macro_use)]
42extern crate std;
43
44use core::fmt;
45use core::future::Future;
46use core::iter::FusedIterator;
47use core::panic::Location;
48use core::pin::Pin;
49
50use alloc::boxed::Box;
51use alloc::string::ToString as _;
52
53#[cfg(doc)]
54use core::task::Poll;
55
56#[cfg(feature = "log_hiccups")]
57use web_time::Instant;
58
59mod basic_yield;
60pub use basic_yield::basic_yield_now;
61
62mod builder;
63pub use builder::Builder;
64
65mod concurrent;
66use concurrent::ConcurrentProgress;
67
68mod maybe_sync;
69use maybe_sync::*;
70
71mod info;
72pub use info::{ProgressInfo, YieldInfo};
73
74/// We could import this alias from `futures-core` but that would be another non-dev dependency.
75type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
76
77// We allow !Sync progress functions but only internally; this type doesn't control the API.
78#[cfg(feature = "sync")]
79type ProgressFn = dyn for<'a> Fn(&'a ProgressInfo<'a>) + Send + Sync + 'static;
80#[cfg(not(feature = "sync"))]
81type ProgressFn = dyn for<'a> Fn(&'a ProgressInfo<'a>) + 'static;
82
83type YieldFn = dyn for<'a> Fn(&'a YieldInfo<'a>) -> BoxFuture<'static, ()> + Send + Sync;
84
85/// Allows a long-running async task to report its progress, while also yielding to the
86/// scheduler (e.g. for single-threaded web environment) and introducing cancellation
87/// points.
88///
89/// These things go together because the rate at which it makes sense to yield (to avoid event
90/// loop hangs) is similar to the rate at which it makes sense to report progress.
91///
92/// Note that while a [`YieldProgress`] is [`Send`] and [`Sync`] in order to be used within tasks
93/// that may be moved between threads, it does not currently support meaningfully being used from
94/// multiple threads or futures at once — only within a fully sequential operation. Future versions
95/// may include a “parallel split” operation but the current one does not.
96///
97/// ---
98///
99/// To construct a [`YieldProgress`], use the [`Builder`], or [`noop()`](YieldProgress::noop).
100pub struct YieldProgress {
101    start: f32,
102    end: f32,
103
104    /// Name given to this specific portion of work. Inherited from the parent if not
105    /// overridden.
106    ///
107    /// TODO: Eventually we will want to have things like "label this segment as a
108    /// fallback if it has no better label", which will require some notion of distinguishing
109    /// inheritance from having been explicitly set.
110    label: Option<MaRc<str>>,
111
112    yielding: BoxYielding,
113    // TODO: change progress reporting interface to support efficient handling of
114    // the label string being the same as last time.
115    progressor: MaRc<ProgressFn>,
116}
117
118/// Piggyback on the `Arc` we need to store the `dyn Fn` anyway to also store some state.
119struct Yielding<F: ?Sized> {
120    state: StateCell<YieldState>,
121
122    yielder: F,
123}
124
125type BoxYielding = MaRc<Yielding<YieldFn>>;
126
127#[derive(Clone)]
128struct YieldState {
129    /// The most recent instant at which `yielder`'s future completed.
130    /// Used to detect overlong time periods between yields.
131    #[cfg(feature = "log_hiccups")]
132    last_finished_yielding: Instant,
133
134    last_yield_location: &'static Location<'static>,
135
136    last_yield_label: Option<MaRc<str>>,
137}
138
139impl fmt::Debug for YieldProgress {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        f.debug_struct("YieldProgress")
142            .field("start", &self.start)
143            .field("end", &self.end)
144            .field("label", &self.label)
145            .finish_non_exhaustive()
146    }
147}
148
149impl YieldProgress {
150    /// Construct a new [`YieldProgress`], which will call `yielder` to yield and
151    /// `progressor` to report progress.
152    ///
153    /// * `yielder` should return a `Future` that returns [`Poll::Pending`] at least once,
154    ///   and may perform other executor-specific actions to assist with scheduling other tasks.
155    /// * `progressor` is called with the progress fraction (a number between 0 and 1) and a
156    ///   label for the current portion of work (which will be `""` if no label has been set).
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use yield_progress::YieldProgress;
162    /// # struct Pb;
163    /// # impl Pb { fn set_value(&self, _value: f32) {} }
164    /// # let some_progress_bar = Pb;
165    /// // let some_progress_bar = ...;
166    ///
167    /// let progress = YieldProgress::new(
168    ///     tokio::task::yield_now,
169    ///     move |fraction, _label| {
170    ///         some_progress_bar.set_value(fraction);
171    ///     }
172    /// );
173    /// ```
174    #[track_caller]
175    #[deprecated = "use `yield_progress::Builder` instead"]
176    pub fn new<Y, YFut, P>(yielder: Y, progressor: P) -> Self
177    where
178        Y: Fn() -> YFut + Send + Sync + 'static,
179        YFut: Future<Output = ()> + Send + 'static,
180        P: Fn(f32, &str) + Send + Sync + 'static,
181    {
182        Builder::new()
183            .yield_using(move |_| yielder())
184            .progress_using(move |info| progressor(info.fraction(), info.label_str()))
185            .build()
186    }
187
188    /// Returns a [`YieldProgress`] that does no progress reporting **and no yielding at all**.
189    ///
190    /// This may be used, for example, to call a function that accepts [`YieldProgress`] and
191    /// is not `async` for any other reason.
192    /// It should not be used merely because no progress reporting is desired; in that case
193    /// use [`Builder`] instead so that a yield function can be provided.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// # #[tokio::main(flavor = "current_thread")] async fn main() {
199    /// use yield_progress::YieldProgress;
200    ///
201    /// let mut progress = YieldProgress::noop();
202    /// // These calls will have no effect.
203    /// progress.set_label("a tree falls in a forest");
204    /// progress.progress(0.12345).await;
205    /// # }
206    /// ```
207    pub fn noop() -> Self {
208        Builder::new()
209            .yield_using(|_| core::future::ready(()))
210            .build()
211    }
212
213    /// Add a name for the portion of work this [`YieldProgress`] covers, which will be
214    /// used by all future progress updates.
215    ///
216    /// If there is already a label, it will be overwritten.
217    ///
218    /// This does not immediately report progress; that is, the label will not be visible
219    /// anywhere until the next operation that does. Future versions may report it immediately.
220    pub fn set_label(&mut self, label: impl fmt::Display) {
221        self.set_label_internal(Some(MaRc::from(label.to_string())))
222    }
223
224    fn set_label_internal(&mut self, label: Option<MaRc<str>>) {
225        self.label = label;
226    }
227
228    /// Map a `0..=1` value to `self.start..=self.end`.
229    #[track_caller]
230    fn point_in_range(&self, mut x: f32) -> f32 {
231        x = x.clamp(0.0, 1.0);
232        if !x.is_finite() {
233            if cfg!(debug_assertions) {
234                panic!("NaN progress value");
235            } else {
236                x = 0.5;
237            }
238        }
239        self.start + (x * (self.end - self.start))
240    }
241
242    /// Report the current amount of progress (a number from 0 to 1) and yield.
243    ///
244    /// The value *may* be less than previously given values.
245    #[track_caller] // This is not an `async fn` because `track_caller` is not compatible
246    pub fn progress(&self, progress_fraction: f32) -> maybe_send_impl_future!(()) {
247        let location = Location::caller();
248        let label = self.label.clone();
249
250        self.progress_without_yield(progress_fraction);
251
252        self.yielding.clone().yield_only(location, label)
253    }
254
255    /// Report the current amount of progress (a number from 0 to 1) without yielding.
256    ///
257    /// Caution: Not yielding may mean that the display of progress to the user does not
258    /// update. This should be used only when necessary for non-async code.
259    #[track_caller]
260    pub fn progress_without_yield(&self, progress_fraction: f32) {
261        let location = Location::caller();
262        self.send_progress(progress_fraction, self.label.as_ref(), location);
263    }
264
265    /// Yield only; that is, call the yield function contained within this [`YieldProgress`].
266    #[track_caller] // This is not an `async fn` because `track_caller` is not compatible
267    pub fn yield_without_progress(&self) -> maybe_send_impl_future!(()) {
268        let location = Location::caller();
269        let label = self.label.clone();
270
271        self.yielding.clone().yield_only(location, label)
272    }
273
274    /// Assemble a [`ProgressInfo`] using self's range and send it to the progress function.
275    /// This differs from `progress_without_yield()` by taking an explicit label and location;
276    /// only the range and destination from `self` is used.
277    fn send_progress(
278        &self,
279        progress_fraction: f32,
280        label: Option<&MaRc<str>>,
281        location: &Location<'_>,
282    ) {
283        (self.progressor)(&ProgressInfo {
284            fraction: self.point_in_range(progress_fraction),
285            label,
286            location,
287        });
288    }
289
290    /// Report that 100% of progress has been made.
291    ///
292    /// This is identical to `.progress(1.0)` but consumes the `YieldProgress` object.
293    #[track_caller] // This is not an `async fn` because `track_caller` is not compatible
294    pub fn finish(self) -> maybe_send_impl_future!(()) {
295        self.progress(1.0)
296    }
297
298    /// Report that the given amount of progress has been made, then return
299    /// a [`YieldProgress`] covering the remaining range.
300    #[track_caller] // This is not an `async fn` because `track_caller` is not compatible
301    pub fn finish_and_cut(self, progress_fraction: f32) -> maybe_send_impl_future!(Self) {
302        // Efficiency note: this is structured so that `a` can be dropped immediately
303        // and does not live on in the future.
304        let [a, b] = self.split(progress_fraction);
305        a.progress_without_yield(1.0);
306        async move {
307            b.yield_without_progress().await;
308            b
309        }
310    }
311
312    /// Report the _beginning_ of a unit of work of size `progress_fraction` and described
313    /// by `label`. That fraction is cut off of the beginning range of `self`, and returned
314    /// as a separate [`YieldProgress`].
315    ///
316    /// ```no_run
317    /// # async fn foo() {
318    /// # use yield_progress::YieldProgress;
319    /// # let mut main_progress = YieldProgress::noop();
320    /// let a_progress = main_progress.start_and_cut(0.5, "task A").await;
321    /// // do task A...
322    /// a_progress.finish().await;
323    /// // continue using main_progress...
324    /// # }
325    /// ```
326    #[track_caller]
327    pub fn start_and_cut(
328        &mut self,
329        cut: f32,
330        label: impl fmt::Display,
331    ) -> maybe_send_impl_future!(Self) {
332        let cut_abs = self.point_in_range(cut);
333        let mut portion = self.with_new_range(self.start, cut_abs);
334        self.start = cut_abs;
335
336        portion.set_label(label);
337        async {
338            portion.progress(0.0).await;
339            portion
340        }
341    }
342
343    fn with_new_range(&self, start: f32, end: f32) -> Self {
344        Self {
345            start,
346            end,
347            label: self.label.clone(),
348            yielding: MaRc::clone(&self.yielding),
349            progressor: MaRc::clone(&self.progressor),
350        }
351    }
352
353    /// Construct two new [`YieldProgress`] which divide the progress value into two
354    /// subranges.
355    ///
356    /// The returned instances should be used in sequence, but this is not enforced.
357    /// Using them concurrently will result in the progress bar jumping backwards.
358    pub fn split(self, cut: f32) -> [Self; 2] {
359        let cut_abs = self.point_in_range(cut);
360        [
361            self.with_new_range(self.start, cut_abs),
362            self.with_new_range(cut_abs, self.end),
363        ]
364    }
365
366    /// Construct many new [`YieldProgress`] which together divide the progress value into
367    /// `count` subranges.
368    ///
369    /// The returned instances should be used in sequence, but this is not enforced.
370    /// Using them concurrently will result in the progress bar jumping backwards.
371    pub fn split_evenly(
372        self,
373        count: usize,
374    ) -> impl DoubleEndedIterator<Item = YieldProgress> + ExactSizeIterator + FusedIterator {
375        (0..count).map(move |index| {
376            self.with_new_range(
377                self.point_in_range(index as f32 / count as f32),
378                self.point_in_range((index as f32 + 1.0) / count as f32),
379            )
380        })
381    }
382
383    /// Construct many new [`YieldProgress`] which will collectively advance `self` to completion
384    /// when they have all been advanced to completion, and which may be used concurrently.
385    ///
386    /// This is identical in effect to [`YieldProgress::split_evenly()`], except that it comprehends
387    /// concurrent operations — the progress of `self` is the sum of the progress of the subtasks.
388    /// To support this, it must allocate storage for the state tracking and synchronization, and
389    /// every progress update must calculate the sum from all subtasks. Therefore, for efficiency,
390    /// do not use this except when concurrency is actually present.
391    ///
392    /// The label passed through will be the label from the first subtask that has a progress
393    /// value less than 1.0. This choice may be changed in the future if the label system is
394    /// elaborated.
395    pub fn split_evenly_concurrent(
396        self,
397        count: usize,
398    ) -> impl DoubleEndedIterator<Item = YieldProgress> + ExactSizeIterator + FusedIterator {
399        let yielding = self.yielding.clone();
400        let conc = ConcurrentProgress::new(self, count);
401        (0..count).map(move |index| {
402            let mut builder = Builder::new().yielding_internal(yielding.clone());
403            // The progressor may be `!Sync` if our `sync` feature is disabled.
404            // This is prohibited in our API, but it's safe for us as long as we match the feature.
405            // Therefore, bypass the method and assign the field directly.
406            builder.progressor = MaRc::new(MaRc::clone(&conc).progressor(index));
407            builder.build()
408        })
409    }
410}
411
412impl<F> Yielding<F>
413where
414    F: ?Sized + for<'a> Fn(&'a YieldInfo<'a>) -> BoxFuture<'static, ()> + Send + Sync,
415{
416    fn yield_only(
417        self: MaRc<Self>,
418        location: &'static Location<'static>,
419        mut label: Option<MaRc<str>>,
420    ) -> impl Future<Output = ()> {
421        #[cfg(feature = "log_hiccups")]
422        {
423            #[allow(unused)] // may be redundant depending on other features
424            use alloc::format;
425            use core::time::Duration;
426
427            // Note that we avoid holding the lock while calling yielder().
428            // The worst outcome of an inconsistency is that we will output a meaningless
429            // "between {location} and {location}" message, but none should occur because
430            // [`YieldProgress`] is intended to be used in a sequential manner.
431            let previous_state: YieldState = { self.state.lock().unwrap().clone() };
432
433            let delta = Instant::now().duration_since(previous_state.last_finished_yielding);
434            if delta > Duration::from_millis(100) {
435                let last_label = previous_state.last_yield_label;
436                log::trace!(
437                    "Yielding after {delta} ms between {old_location} and {new_location} {rel}",
438                    delta = delta.as_millis(),
439                    old_location = previous_state.last_yield_location,
440                    new_location = location,
441                    rel = if label == last_label {
442                        format!("during {label:?}")
443                    } else {
444                        format!("between {last_label:?} and {label:?}")
445                    }
446                );
447            }
448        }
449
450        // TODO: Since we're tracking time, we might as well decide whether to not bother
451        // yielding if it has been a short time ... except that different yielders might
452        // want different granularities/policies.
453
454        // Efficiency: This explicit `async` block somehow improves the future data size,
455        // compared to `async fn`, by not allocating both a local and a capture for all of
456        // `self`, `location`, and `label`. Seems odd that this helps...
457        async move {
458            let yield_future = {
459                // Efficiency: This block avoids holding the temp `YieldInfo` across the await.
460                (self.yielder)(&YieldInfo { location })
461            };
462            yield_future.await;
463
464            {
465                let mut state = self.state.lock().unwrap();
466
467                state.last_yield_location = location;
468                // Efficiency: this `Option::take()` avoids generating a drop flag.
469                state.last_yield_label = label.take();
470
471                #[cfg(feature = "log_hiccups")]
472                {
473                    state.last_finished_yielding = Instant::now();
474                }
475            }
476        }
477    }
478}