diff --git a/examples/fastbar.rs b/examples/fastbar.rs index dbad0e6c..1b71f0ca 100644 --- a/examples/fastbar.rs +++ b/examples/fastbar.rs @@ -1,26 +1,17 @@ -use std::time::Instant; - -use indicatif::{HumanDuration, ProgressBar}; +use indicatif::ProgressBar; fn many_units_of_easy_work(n: u64, label: &str) { let pb = ProgressBar::new(n); let mut sum = 0; - let started = Instant::now(); for i in 0..n { // Any quick computation, followed by an update to the progress bar. sum += 2 * i + 3; pb.inc(1); } pb.finish(); - let finished = started.elapsed(); - println!( - "[{}] Sum ({}) calculated in {}", - label, - sum, - HumanDuration(finished) - ); + println!("[{}] Sum ({}) calculated in {:?}", label, sum, pb.elapsed()); } fn main() { diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 6b083d78..8b2f2839 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex, Weak}; use std::time::{Duration, Instant}; use crate::draw_target::ProgressDrawTarget; -use crate::state::{BarState, ProgressFinish, Reset, Ticker}; +use crate::state::{AtomicPosition, BarState, ProgressFinish, Reset, Ticker}; use crate::style::ProgressStyle; use crate::ProgressState; use crate::{ProgressBarIter, ProgressIterator}; @@ -18,6 +18,7 @@ use crate::{ProgressBarIter, ProgressIterator}; #[derive(Clone)] pub struct ProgressBar { state: Arc>, + pos: Arc, } impl fmt::Debug for ProgressBar { @@ -46,14 +47,16 @@ impl ProgressBar { /// Creates a new progress bar with a given length and draw target pub fn with_draw_target(len: u64, draw_target: ProgressDrawTarget) -> ProgressBar { + let pos = Arc::new(AtomicPosition::default()); ProgressBar { - state: Arc::new(Mutex::new(BarState::new(len, draw_target))), + state: Arc::new(Mutex::new(BarState::new(len, draw_target, pos.clone()))), + pos, } } /// A convenience builder-like function for a progress bar with a given style pub fn with_style(self, style: ProgressStyle) -> ProgressBar { - self.state.lock().unwrap().style = style; + self.state().style = style; self } @@ -71,13 +74,13 @@ impl ProgressBar { /// A convenience builder-like function for a progress bar with a given position pub fn with_position(self, pos: u64) -> ProgressBar { - self.state.lock().unwrap().state.set_pos(pos); + self.state().state.set_pos(pos); self } /// A convenience builder-like function for a progress bar with a given elapsed time pub fn with_elapsed(self, elapsed: Duration) -> ProgressBar { - self.state.lock().unwrap().state.started = Instant::now() - elapsed; + self.state().state.started = Instant::now() - elapsed; self } @@ -110,7 +113,7 @@ impl ProgressBar { /// /// This does not redraw the bar. Call [`ProgressBar::tick()`] to force it. pub fn set_style(&self, style: ProgressStyle) { - self.state.lock().unwrap().style = style; + self.state().style = style; } /// Spawns a background thread to tick the progress bar @@ -138,17 +141,21 @@ impl ProgressBar { /// Advances the position of the progress bar by `delta` pub fn inc(&self, delta: u64) { - self.state().inc(Instant::now(), delta) + self.pos.inc(delta); + let now = Instant::now(); + if self.pos.allow(now) { + self.state().tick(now); + } } /// A quick convenience check if the progress bar is hidden pub fn is_hidden(&self) -> bool { - self.state.lock().unwrap().draw_target.is_hidden() + self.state().draw_target.is_hidden() } /// Indicates that the progress bar finished pub fn is_finished(&self) -> bool { - self.state.lock().unwrap().state.is_finished() + self.state().state.is_finished() } /// Print a log line above the progress bar @@ -173,7 +180,11 @@ impl ProgressBar { /// Sets the position of the progress bar pub fn set_position(&self, pos: u64) { - self.state().set_position(Instant::now(), pos) + self.pos.set(pos); + let now = Instant::now(); + if self.pos.allow(now) { + self.state().tick(now); + } } /// Sets the length of the progress bar @@ -206,6 +217,7 @@ impl ProgressBar { pub fn downgrade(&self) -> WeakProgressBar { WeakProgressBar { state: Arc::downgrade(&self.state), + pos: Arc::downgrade(&self.pos), } } @@ -292,7 +304,7 @@ impl ProgressBar { /// [`MultiProgress::add`]: crate::MultiProgress::add /// [`MultiProgress::set_draw_target`]: crate::MultiProgress::set_draw_target pub fn set_draw_target(&self, target: ProgressDrawTarget) { - let mut state = self.state.lock().unwrap(); + let mut state = self.state(); state.draw_target.disconnect(Instant::now()); state.draw_target = target; } @@ -420,27 +432,27 @@ impl ProgressBar { /// Returns the current position pub fn position(&self) -> u64 { - self.state.lock().unwrap().state.pos() + self.state().state.pos() } /// Returns the current length pub fn length(&self) -> u64 { - self.state.lock().unwrap().state.len() + self.state().state.len() } /// Returns the current ETA pub fn eta(&self) -> Duration { - self.state.lock().unwrap().state.eta() + self.state().state.eta() } /// Returns the current rate of progress pub fn per_sec(&self) -> f64 { - self.state.lock().unwrap().state.per_sec() + self.state().state.per_sec() } /// Returns the current expected duration pub fn duration(&self) -> Duration { - self.state.lock().unwrap().state.duration() + self.state().state.duration() } /// Returns the current elapsed time @@ -453,6 +465,7 @@ impl ProgressBar { self.state().draw_target.remote().map(|(_, idx)| idx) } + #[inline] pub(crate) fn state(&self) -> MutexGuard<'_, BarState> { self.state.lock().unwrap() } @@ -464,6 +477,7 @@ impl ProgressBar { #[derive(Clone, Default)] pub struct WeakProgressBar { state: Weak>, + pos: Weak, } impl WeakProgressBar { @@ -479,7 +493,9 @@ impl WeakProgressBar { /// /// [`ProgressBar`]: struct.ProgressBar.html pub fn upgrade(&self) -> Option { - self.state.upgrade().map(|state| ProgressBar { state }) + let state = self.state.upgrade()?; + let pos = self.pos.upgrade()?; + Some(ProgressBar { state, pos }) } } @@ -491,14 +507,14 @@ mod tests { #[test] fn test_pbar_zero() { let pb = ProgressBar::new(0); - assert_eq!(pb.state.lock().unwrap().state.fraction(), 1.0); + assert_eq!(pb.state().state.fraction(), 1.0); } #[allow(clippy::float_cmp)] #[test] fn test_pbar_maxu64() { let pb = ProgressBar::new(!0); - assert_eq!(pb.state.lock().unwrap().state.fraction(), 0.0); + assert_eq!(pb.state().state.fraction(), 0.0); } #[test] diff --git a/src/state.rs b/src/state.rs index 245bb43a..7e8a30f2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::fmt; use std::io; +use std::sync::atomic::{AtomicU64, AtomicU8, Ordering}; use std::sync::{Arc, Mutex, Weak}; use std::thread; use std::time::{Duration, Instant}; @@ -17,12 +18,12 @@ pub(crate) struct BarState { } impl BarState { - pub(crate) fn new(len: u64, draw_target: ProgressDrawTarget) -> Self { + pub(crate) fn new(len: u64, draw_target: ProgressDrawTarget, pos: Arc) -> Self { Self { draw_target, on_finish: ProgressFinish::default(), style: ProgressStyle::default_bar(), - state: ProgressState::new(len), + state: ProgressState::new(len, pos), ticker: None, } } @@ -32,13 +33,13 @@ impl BarState { pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) { self.state.status = Status::DoneVisible; match finish { - ProgressFinish::AndLeave => self.state.pos = self.state.len, + ProgressFinish::AndLeave => self.state.pos.set(self.state.len), ProgressFinish::WithMessage(msg) => { - self.state.pos = self.state.len; + self.state.pos.set(self.state.len); self.style.message = msg; } ProgressFinish::AndClear => { - self.state.pos = self.state.len; + self.state.pos.set(self.state.len); self.state.status = Status::DoneHidden; } ProgressFinish::Abandon => {} @@ -60,30 +61,14 @@ impl BarState { } if let Reset::All = mode { - self.state.pos = 0; + self.state.pos.reset(now); self.state.status = Status::InProgress; let _ = self.draw(false, now); } } pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState)) { - let old = self.state.pos; f(&mut self.state); - let delta = self.state.pos.saturating_sub(old); - self.state.est.record(delta, now); - self.tick(now); - } - - pub(crate) fn set_position(&mut self, now: Instant, new: u64) { - let old = self.state.pos; - self.state.pos = new; - self.state.est.record(new.saturating_sub(old), now); - self.tick(now); - } - - pub(crate) fn inc(&mut self, now: Instant, delta: u64) { - self.state.pos = self.state.pos.saturating_add(delta); - self.state.est.record(delta, now); self.tick(now); } @@ -112,6 +97,8 @@ impl BarState { self.state.tick = self.state.tick.saturating_add(1); } + let pos = self.state.pos.pos.load(Ordering::Relaxed); + self.state.est.record(pos, now); let _ = self.draw(false, now); } @@ -186,7 +173,7 @@ pub(crate) enum Reset { /// The state of a progress bar at a moment in time. #[non_exhaustive] pub struct ProgressState { - pos: u64, + pos: Arc, len: u64, pub(crate) tick: u64, pub(crate) started: Instant, @@ -195,9 +182,9 @@ pub struct ProgressState { } impl ProgressState { - pub(crate) fn new(len: u64) -> Self { + pub(crate) fn new(len: u64, pos: Arc) -> Self { Self { - pos: 0, + pos, len, tick: 0, status: Status::InProgress, @@ -217,7 +204,8 @@ impl ProgressState { /// Returns the completion as a floating-point number between 0 and 1 pub fn fraction(&self) -> f32 { - let pct = match (self.pos, self.len) { + let pos = self.pos.pos.load(Ordering::Relaxed); + let pct = match (pos, self.len) { (_, 0) => 1.0, (0, _) => 0.0, (pos, len) => pos as f32 / len as f32, @@ -230,8 +218,10 @@ impl ProgressState { if self.len == !0 || self.is_finished() { return Duration::new(0, 0); } + + let pos = self.pos.pos.load(Ordering::Relaxed); let t = self.est.seconds_per_step(); - secs_to_duration(t * self.len.saturating_sub(self.pos) as f64) + secs_to_duration(t * self.len.saturating_sub(pos) as f64) } /// The expected total duration (that is, elapsed time + expected ETA) @@ -258,11 +248,11 @@ impl ProgressState { } pub fn pos(&self) -> u64 { - self.pos + self.pos.pos.load(Ordering::Relaxed) } pub fn set_pos(&mut self, pos: u64) { - self.pos = pos; + self.pos.set(pos); } #[allow(clippy::len_without_is_empty)] @@ -332,7 +322,7 @@ pub(crate) struct Estimator { steps: [f64; 16], pos: u8, full: bool, - prev: Instant, + prev: (u64, Instant), } impl Estimator { @@ -341,16 +331,17 @@ impl Estimator { steps: [0.0; 16], pos: 0, full: false, - prev: now, + prev: (0, now), } } - fn record(&mut self, delta: u64, now: Instant) { - if delta == 0 || now < self.prev { + fn record(&mut self, new: u64, now: Instant) { + let delta = new - self.prev.0; + if delta == 0 || now < self.prev.1 { return; } - let elapsed = now - self.prev; + let elapsed = now - self.prev.1; let divisor = delta as f64; let mut batch = 0.0; if divisor != 0.0 { @@ -363,13 +354,13 @@ impl Estimator { self.full = true; } - self.prev = now; + self.prev = (new, now); } pub(crate) fn reset(&mut self, now: Instant) { self.pos = 0; self.full = false; - self.prev = now; + self.prev = (0, now); } /// Average time per step in seconds, using rolling buffer of last 15 steps @@ -395,6 +386,74 @@ impl fmt::Debug for Estimator { } } +pub(crate) struct AtomicPosition { + pub(crate) pos: AtomicU64, + capacity: AtomicU8, + prev: AtomicU64, + start: Instant, +} + +impl AtomicPosition { + pub(crate) fn allow(&self, now: Instant) -> bool { + let mut capacity = self.capacity.load(Ordering::Acquire); + // `prev` is the number of ms after `self.started` we last returned `true`, in ns + let prev = self.prev.load(Ordering::Acquire); + // `elapsed` is the number of ns since `self.started` + let elapsed = (now - self.start).as_nanos() as u64; + // `diff` is the number of ns since we last returned `true` + let diff = elapsed - prev; + + // If `capacity` is 0 and not enough time (1ms) has passed since `prev` + // to add new capacity, return `false`. The goal of this method is to + // make this decision as efficient as possible. + if capacity == 0 && diff < INTERVAL { + return false; + } + + // We now calculate `new`, the number of ms, in ns, since we last returned `true`, + // and `remainder`, which represents a number of ns less than 1ms which we cannot + // convert into capacity now, so we're saving it for later. We do this by + // substracting this from `elapsed` before storing it into `self.prev`. + let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL)); + // We add `new` to `capacity`, subtract one for returning `true` from here, + // then make sure it does not exceed a maximum of `MAX_BURST`. + capacity = Ord::min(MAX_BURST, capacity + new as u8 - 1); + + // Then, we just store `capacity` and `prev` atomically for the next iteration + self.capacity.store(capacity, Ordering::Release); + self.prev.store(elapsed - remainder, Ordering::Release); + true + } + + fn reset(&self, now: Instant) { + self.set(0); + let elapsed = (now - self.start).as_millis() as u64; + self.prev.store(elapsed, Ordering::Release); + } + + pub(crate) fn inc(&self, delta: u64) { + self.pos.fetch_add(delta, Ordering::SeqCst); + } + + pub(crate) fn set(&self, pos: u64) { + self.pos.store(pos, Ordering::Release); + } +} + +impl Default for AtomicPosition { + fn default() -> Self { + Self { + pos: AtomicU64::new(0), + capacity: AtomicU8::new(MAX_BURST), + prev: AtomicU64::new(0), + start: Instant::now(), + } + } +} + +const INTERVAL: u64 = 1_000_000; +const MAX_BURST: u8 = 10; + /// Behavior of a progress bar when it is finished /// /// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and @@ -457,11 +516,14 @@ mod tests { #[test] fn test_time_per_step() { let test_rate = |items_per_second| { - let mut est = Estimator::new(Instant::now()); - let mut current_time = est.prev; + let mut now = Instant::now(); + let mut est = Estimator::new(now); + let mut pos = 0; + for _ in 0..est.steps.len() { - current_time += Duration::from_secs(1); - est.record(items_per_second, current_time); + pos += items_per_second; + now += Duration::from_secs(1); + est.record(pos, now); } let avg_seconds_per_step = est.seconds_per_step(); diff --git a/src/style.rs b/src/style.rs index b381b8d1..8076a7bb 100644 --- a/src/style.rs +++ b/src/style.rs @@ -660,12 +660,14 @@ enum Alignment { #[cfg(test)] mod tests { use super::*; - use crate::state::ProgressState; + use crate::state::{AtomicPosition, ProgressState}; + use std::sync::Arc; #[test] fn test_expand_template() { const WIDTH: u16 = 80; - let state = ProgressState::new(10); + let pos = Arc::new(AtomicPosition::default()); + let state = ProgressState::new(10, pos); let mut buf = Vec::new(); let mut style = ProgressStyle::default_bar(); @@ -688,7 +690,8 @@ mod tests { set_colors_enabled(true); const WIDTH: u16 = 80; - let state = ProgressState::new(10); + let pos = Arc::new(AtomicPosition::default()); + let state = ProgressState::new(10, pos); let mut buf = Vec::new(); let mut style = ProgressStyle::default_bar();