#![no_std]
#![deny(elided_lifetimes_in_paths)]
#![forbid(unsafe_code)]
#![warn(clippy::cast_lossless)]
#![warn(clippy::exhaustive_enums)]
#![warn(clippy::exhaustive_structs)]
#![warn(clippy::missing_panics_doc)]
#![warn(clippy::return_self_not_must_use)]
#![warn(clippy::wrong_self_convention)]
#![warn(missing_docs)]
#![warn(unused_lifetimes)]
#![warn(unused_qualifications)]
extern crate alloc;
#[cfg(any(feature = "sync", feature = "log_hiccups"))]
extern crate std;
use core::fmt;
use core::future::Future;
use core::iter::FusedIterator;
use core::panic::Location;
use core::pin::Pin;
use alloc::boxed::Box;
use alloc::string::ToString as _;
use alloc::sync::Arc;
#[cfg(doc)]
use core::task::Poll;
#[cfg(feature = "log_hiccups")]
use web_time::Instant;
mod basic_yield;
pub use basic_yield::basic_yield_now;
mod builder;
pub use builder::Builder;
#[cfg(feature = "sync")]
mod concurrent;
mod info;
pub use info::{ProgressInfo, YieldInfo};
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
type ProgressFn = dyn for<'a> Fn(&'a ProgressInfo<'a>) + Send + Sync + 'static;
type YieldFn = dyn for<'a> Fn(&'a YieldInfo<'a>) -> BoxFuture<'static, ()> + Send + Sync;
type Label = Arc<str>;
pub struct YieldProgress {
start: f32,
end: f32,
label: Option<Label>,
yielding: BoxYielding,
progressor: Arc<ProgressFn>,
}
struct Yielding<F: ?Sized> {
#[cfg(feature = "log_hiccups")]
state: std::sync::Mutex<YieldState>,
yielder: F,
}
type BoxYielding = Arc<Yielding<YieldFn>>;
#[cfg(feature = "log_hiccups")]
#[derive(Clone)]
struct YieldState {
last_finished_yielding: Instant,
last_yield_location: &'static Location<'static>,
last_yield_label: Option<Label>,
}
impl fmt::Debug for YieldProgress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("YieldProgress")
.field("start", &self.start)
.field("end", &self.end)
.field("label", &self.label)
.finish_non_exhaustive()
}
}
impl YieldProgress {
pub fn noop() -> Self {
Builder::new()
.yield_using(|_| core::future::ready(()))
.build()
}
pub fn set_label(&mut self, label: impl fmt::Display) {
self.set_label_internal(Some(Arc::from(label.to_string())))
}
fn set_label_internal(&mut self, label: Option<Label>) {
self.label = label;
}
#[track_caller]
fn point_in_range(&self, mut x: f32) -> f32 {
x = x.clamp(0.0, 1.0);
if !x.is_finite() {
if cfg!(debug_assertions) {
panic!("NaN progress value");
} else {
x = 0.5;
}
}
self.start + (x * (self.end - self.start))
}
#[track_caller] pub fn progress(&self, progress_fraction: f32) -> impl Future<Output = ()> + use<> {
let location = Location::caller();
self.progress_without_yield(progress_fraction);
self.yielding.clone().yield_only(
location,
#[cfg(feature = "log_hiccups")]
self.label.clone(),
)
}
#[track_caller]
pub fn progress_without_yield(&self, progress_fraction: f32) {
let location = Location::caller();
self.send_progress(progress_fraction, self.label.as_ref(), location);
}
#[track_caller] pub fn yield_without_progress(&self) -> impl Future<Output = ()> + Send + use<> {
let location = Location::caller();
self.yielding.clone().yield_only(
location,
#[cfg(feature = "log_hiccups")]
self.label.clone(),
)
}
fn send_progress(
&self,
progress_fraction: f32,
label: Option<&Label>,
location: &Location<'_>,
) {
(self.progressor)(&ProgressInfo {
fraction: self.point_in_range(progress_fraction),
label,
location,
});
}
#[track_caller] pub fn finish(self) -> impl Future<Output = ()> + Send + use<> {
self.progress(1.0)
}
#[track_caller] pub fn finish_and_cut(
self,
progress_fraction: f32,
) -> impl Future<Output = Self> + Send + use<> {
let [a, b] = self.split(progress_fraction);
a.progress_without_yield(1.0);
async move {
b.yield_without_progress().await;
b
}
}
#[track_caller]
pub fn start_and_cut(
&mut self,
cut: f32,
label: impl fmt::Display,
) -> impl Future<Output = Self> + Send + 'static {
let cut_abs = self.point_in_range(cut);
let mut portion = self.with_new_range(self.start, cut_abs);
self.start = cut_abs;
portion.set_label(label);
async {
portion.progress(0.0).await;
portion
}
}
fn with_new_range(&self, start: f32, end: f32) -> Self {
Self {
start,
end,
label: self.label.clone(),
yielding: Arc::clone(&self.yielding),
progressor: Arc::clone(&self.progressor),
}
}
pub fn split(self, cut: f32) -> [Self; 2] {
let cut_abs = self.point_in_range(cut);
[
self.with_new_range(self.start, cut_abs),
self.with_new_range(cut_abs, self.end),
]
}
pub fn split_evenly(
self,
count: usize,
) -> impl DoubleEndedIterator<Item = YieldProgress> + ExactSizeIterator + FusedIterator + use<>
{
(0..count).map(move |index| {
self.with_new_range(
self.point_in_range(index as f32 / count as f32),
self.point_in_range((index as f32 + 1.0) / count as f32),
)
})
}
#[cfg(feature = "sync")]
pub fn split_evenly_concurrent(
self,
count: usize,
) -> impl DoubleEndedIterator<Item = YieldProgress> + ExactSizeIterator + FusedIterator + use<>
{
let yielding = self.yielding.clone();
let conc = concurrent::ConcurrentProgress::new(self, count);
(0..count).map(move |index| {
Builder::new()
.yielding_internal(yielding.clone())
.progress_using(Arc::clone(&conc).progressor(index))
.build()
})
}
}
impl<F> Yielding<F>
where
F: ?Sized + for<'a> Fn(&'a YieldInfo<'a>) -> BoxFuture<'static, ()> + Send + Sync,
{
#[allow(clippy::manual_async_fn)] fn yield_only(
self: Arc<Self>,
location: &'static Location<'static>,
#[cfg(feature = "log_hiccups")] mut label: Option<Label>,
) -> impl Future<Output = ()> + use<F> {
#[cfg(feature = "log_hiccups")]
{
#[allow(unused)] use alloc::format;
use core::time::Duration;
let previous_state: YieldState = { self.state.lock().unwrap().clone() };
let delta = Instant::now().duration_since(previous_state.last_finished_yielding);
if delta > Duration::from_millis(100) {
let last_label = previous_state.last_yield_label;
log::trace!(
"Yielding after {delta} ms between {old_location} and {new_location} {rel}",
delta = delta.as_millis(),
old_location = previous_state.last_yield_location,
new_location = location,
rel = if label == last_label {
format!("during {label:?}")
} else {
format!("between {last_label:?} and {label:?}")
}
);
}
}
async move {
let yield_future = {
(self.yielder)(&YieldInfo { location })
};
yield_future.await;
#[cfg(feature = "log_hiccups")]
{
let mut state = self.state.lock().unwrap();
state.last_yield_location = location;
state.last_yield_label = label.take();
state.last_finished_yielding = Instant::now();
}
}
}
}