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}