Skip to main content

indicatif/
state.rs

1use std::borrow::Cow;
2use std::io;
3use std::sync::{Arc, OnceLock};
4use std::time::Duration;
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Instant;
7
8use portable_atomic::{AtomicU64, AtomicU8, Ordering};
9#[cfg(all(target_arch = "wasm32", feature = "wasmbind"))]
10use web_time::Instant;
11
12use crate::draw_target::{LineType, ProgressDrawTarget};
13use crate::style::ProgressStyle;
14
15pub(crate) struct BarState {
16    pub(crate) draw_target: ProgressDrawTarget,
17    pub(crate) on_finish: ProgressFinish,
18    pub(crate) style: ProgressStyle,
19    pub(crate) state: ProgressState,
20    pub(crate) tab_width: usize,
21}
22
23impl BarState {
24    pub(crate) fn new(
25        len: Option<u64>,
26        draw_target: ProgressDrawTarget,
27        pos: Arc<AtomicPosition>,
28    ) -> Self {
29        Self {
30            draw_target,
31            on_finish: ProgressFinish::default(),
32            style: ProgressStyle::default_bar(),
33            state: ProgressState::new(len, pos),
34            tab_width: DEFAULT_TAB_WIDTH,
35        }
36    }
37
38    /// Finishes the progress bar using the [`ProgressFinish`] behavior stored
39    /// in the [`ProgressStyle`].
40    pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) {
41        let duration = now.duration_since(self.state.started);
42        self.state.status = Status::DoneVisible(duration);
43        match finish {
44            ProgressFinish::AndLeave => {
45                if let Some(len) = self.state.len {
46                    self.state.pos.set(len);
47                }
48            }
49            ProgressFinish::WithMessage(msg) => {
50                if let Some(len) = self.state.len {
51                    self.state.pos.set(len);
52                }
53                self.state.message = TabExpandedString::new(msg, self.tab_width);
54            }
55            ProgressFinish::AndClear => {
56                if let Some(len) = self.state.len {
57                    self.state.pos.set(len);
58                }
59                self.state.status = Status::DoneHidden(duration);
60            }
61            ProgressFinish::Abandon => {}
62            ProgressFinish::AbandonWithMessage(msg) => {
63                self.state.message = TabExpandedString::new(msg, self.tab_width);
64            }
65        }
66
67        // There's no need to update the estimate here; once the `status` is no longer
68        // `InProgress`, we will use the length and elapsed time to estimate.
69        let _ = self.draw(true, now);
70    }
71
72    pub(crate) fn reset(&mut self, now: Instant, mode: Reset) {
73        // Always reset the estimator; this is the only reset that will occur if mode is
74        // `Reset::Eta`.
75        self.state.est.reset(now);
76
77        if let Reset::Elapsed | Reset::All = mode {
78            self.state.started = now;
79        }
80
81        if let Reset::All = mode {
82            self.state.pos.reset(now);
83            self.state.status = Status::InProgress;
84
85            for tracker in self.style.format_map.values_mut() {
86                tracker.reset(&self.state, now);
87            }
88
89            let _ = self.draw(false, now);
90        }
91    }
92
93    pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState), tick: bool) {
94        f(&mut self.state);
95        if tick {
96            self.tick(now);
97        }
98    }
99
100    pub(crate) fn unset_length(&mut self, now: Instant) {
101        self.state.len = None;
102        self.update_estimate_and_draw(now);
103    }
104
105    pub(crate) fn set_length(&mut self, now: Instant, len: u64) {
106        self.state.len = Some(len);
107        self.update_estimate_and_draw(now);
108    }
109
110    pub(crate) fn inc_length(&mut self, now: Instant, delta: u64) {
111        if let Some(len) = self.state.len {
112            self.state.len = Some(len.saturating_add(delta));
113        }
114        self.update_estimate_and_draw(now);
115    }
116
117    pub(crate) fn dec_length(&mut self, now: Instant, delta: u64) {
118        if let Some(len) = self.state.len {
119            self.state.len = Some(len.saturating_sub(delta));
120        }
121        self.update_estimate_and_draw(now);
122    }
123
124    pub(crate) fn set_tab_width(&mut self, tab_width: usize) {
125        self.tab_width = tab_width;
126        self.state.message.set_tab_width(tab_width);
127        self.state.prefix.set_tab_width(tab_width);
128        self.style.set_tab_width(tab_width);
129    }
130
131    pub(crate) fn set_style(&mut self, style: ProgressStyle) {
132        self.style = style;
133        self.style.set_tab_width(self.tab_width);
134    }
135
136    pub(crate) fn tick(&mut self, now: Instant) {
137        self.state.tick = self.state.tick.saturating_add(1);
138        self.update_estimate_and_draw(now);
139    }
140
141    pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) {
142        let pos = self.state.pos.pos.load(Ordering::Relaxed);
143        self.state.est.record(pos, now);
144
145        for tracker in self.style.format_map.values_mut() {
146            tracker.tick(&self.state, now);
147        }
148
149        let _ = self.draw(false, now);
150    }
151
152    pub(crate) fn println(&mut self, now: Instant, msg: &str) {
153        let width = self.draw_target.width();
154        let mut drawable = match self.draw_target.drawable(true, now) {
155            Some(drawable) => drawable,
156            None => return,
157        };
158
159        let mut draw_state = drawable.state();
160        let lines: Vec<LineType> = msg.lines().map(|l| LineType::Text(Into::into(l))).collect();
161        // Empty msg should trigger newline as we are in println
162        if lines.is_empty() {
163            draw_state.lines.push(LineType::Empty);
164        } else {
165            draw_state.lines.extend(lines);
166        }
167
168        if let Some(width) = width {
169            if !matches!(self.state.status, Status::DoneHidden(_)) {
170                self.style
171                    .format_state(&self.state, &mut draw_state.lines, width);
172            }
173        }
174
175        drop(draw_state);
176        let _ = drawable.draw();
177    }
178
179    pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, now: Instant, f: F) -> R {
180        if let Some((state, _)) = self.draw_target.remote() {
181            return state.write().unwrap().suspend(f, now);
182        }
183
184        if let Some(drawable) = self.draw_target.drawable(true, now) {
185            let _ = drawable.clear();
186        }
187
188        let ret = f();
189        let _ = self.draw(true, Instant::now());
190        ret
191    }
192
193    pub(crate) fn draw(&mut self, mut force_draw: bool, now: Instant) -> io::Result<()> {
194        // `|= self.is_finished()` should not be needed here, but we used to always draw for
195        // finished progress bars, so it's kept as to not cause compatibility issues in weird cases.
196        force_draw |= self.state.is_finished();
197        let mut drawable = match self.draw_target.drawable(force_draw, now) {
198            Some(drawable) => drawable,
199            None => return Ok(()),
200        };
201
202        // Getting the width can be expensive; thus this should happen after checking drawable.
203        let width = drawable.width();
204
205        let mut draw_state = drawable.state();
206
207        if let Some(width) = width {
208            if !matches!(self.state.status, Status::DoneHidden(_)) {
209                self.style
210                    .format_state(&self.state, &mut draw_state.lines, width);
211            }
212        }
213
214        drop(draw_state);
215        drawable.draw()
216    }
217}
218
219impl Drop for BarState {
220    fn drop(&mut self) {
221        // Progress bar is already finished.  Do not need to do anything other than notify
222        // the `MultiProgress` that we're now a zombie.
223        if self.state.is_finished() {
224            self.draw_target.mark_zombie();
225            return;
226        }
227
228        self.finish_using_style(Instant::now(), self.on_finish.clone());
229
230        // Notify the `MultiProgress` that we're now a zombie.
231        self.draw_target.mark_zombie();
232    }
233}
234
235pub(crate) enum Reset {
236    Eta,
237    Elapsed,
238    All,
239}
240
241/// The state of a progress bar at a moment in time.
242#[non_exhaustive]
243pub struct ProgressState {
244    pos: Arc<AtomicPosition>,
245    len: Option<u64>,
246    pub(crate) tick: u64,
247    pub(crate) started: Instant,
248    status: Status,
249    est: Estimator,
250    pub(crate) message: TabExpandedString,
251    pub(crate) prefix: TabExpandedString,
252}
253
254impl ProgressState {
255    pub(crate) fn new(len: Option<u64>, pos: Arc<AtomicPosition>) -> Self {
256        let now = Instant::now();
257        Self {
258            pos,
259            len,
260            tick: 0,
261            status: Status::InProgress,
262            started: now,
263            est: Estimator::new(now),
264            message: TabExpandedString::NoTabs("".into()),
265            prefix: TabExpandedString::NoTabs("".into()),
266        }
267    }
268
269    /// Indicates that the progress bar finished.
270    pub fn is_finished(&self) -> bool {
271        match self.status {
272            Status::InProgress => false,
273            Status::DoneVisible(_) => true,
274            Status::DoneHidden(_) => true,
275        }
276    }
277
278    /// Returns the completion as a floating-point number between 0 and 1
279    pub fn fraction(&self) -> f32 {
280        let pos = self.pos.pos.load(Ordering::Relaxed);
281        let pct = match (pos, self.len) {
282            (_, None) => 0.0,
283            (_, Some(0)) => 1.0,
284            (0, _) => 0.0,
285            (pos, Some(len)) => pos as f32 / len as f32,
286        };
287        pct.clamp(0.0, 1.0)
288    }
289
290    /// The expected ETA
291    pub fn eta(&self) -> Duration {
292        if self.is_finished() {
293            return Duration::new(0, 0);
294        }
295
296        let len = match self.len {
297            Some(len) => len,
298            None => return Duration::new(0, 0),
299        };
300
301        let pos = self.pos.pos.load(Ordering::Relaxed);
302
303        let sps = self.est.steps_per_second(Instant::now());
304
305        // Infinite duration should only ever happen at the beginning, so in this case it's okay to
306        // just show an ETA of 0 until progress starts to occur.
307        if sps == 0.0 {
308            return Duration::new(0, 0);
309        }
310
311        secs_to_duration(len.saturating_sub(pos) as f64 / sps)
312    }
313
314    /// The expected total duration (that is, elapsed time + expected ETA)
315    pub fn duration(&self) -> Duration {
316        match (self.status, self.len) {
317            (Status::DoneVisible(duration) | Status::DoneHidden(duration), _) => duration,
318            (Status::InProgress, Some(_)) => self.started.elapsed().saturating_add(self.eta()),
319            (Status::InProgress, None) => Duration::ZERO,
320        }
321    }
322
323    /// The number of steps per second
324    pub fn per_sec(&self) -> f64 {
325        if let Status::InProgress = self.status {
326            self.est.steps_per_second(Instant::now())
327        } else {
328            self.pos() as f64 / self.started.elapsed().as_secs_f64()
329        }
330    }
331
332    pub fn elapsed(&self) -> Duration {
333        self.started.elapsed()
334    }
335
336    pub fn pos(&self) -> u64 {
337        self.pos.pos.load(Ordering::Relaxed)
338    }
339
340    pub fn set_pos(&mut self, pos: u64) {
341        self.pos.set(pos);
342    }
343
344    #[allow(clippy::len_without_is_empty)]
345    pub fn len(&self) -> Option<u64> {
346        self.len
347    }
348
349    pub fn set_len(&mut self, len: u64) {
350        self.len = Some(len);
351    }
352}
353
354#[derive(Debug, PartialEq, Eq, Clone)]
355pub(crate) enum TabExpandedString {
356    NoTabs(Cow<'static, str>),
357    WithTabs {
358        original: Cow<'static, str>,
359        expanded: OnceLock<String>,
360        tab_width: usize,
361    },
362}
363
364impl TabExpandedString {
365    pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self {
366        if !s.contains('\t') {
367            Self::NoTabs(s)
368        } else {
369            Self::WithTabs {
370                original: s,
371                tab_width,
372                expanded: OnceLock::new(),
373            }
374        }
375    }
376
377    pub(crate) fn expanded(&self) -> &str {
378        match &self {
379            Self::NoTabs(s) => {
380                debug_assert!(!s.contains('\t'));
381                s
382            }
383            Self::WithTabs {
384                original,
385                tab_width,
386                expanded,
387            } => expanded.get_or_init(|| original.replace('\t', &" ".repeat(*tab_width))),
388        }
389    }
390
391    pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
392        if let Self::WithTabs {
393            expanded,
394            tab_width,
395            ..
396        } = self
397        {
398            if *tab_width != new_tab_width {
399                *tab_width = new_tab_width;
400                expanded.take();
401            }
402        }
403    }
404}
405
406/// Double-smoothed exponentially weighted estimator
407///
408/// This uses an exponentially weighted *time-based* estimator, meaning that it exponentially
409/// downweights old data based on its age. The rate at which this occurs is currently a constant
410/// value of 15 seconds for 90% weighting. This means that all data older than 15 seconds has a
411/// collective weight of 0.1 in the estimate, and all data older than 30 seconds has a collective
412/// weight of 0.01, and so on.
413///
414/// The primary value exposed by `Estimator` is `steps_per_second`. This value is doubly-smoothed,
415/// meaning that is the result of using an exponentially weighted estimator (as described above) to
416/// estimate the value of another exponentially weighted estimator, which estimates the value of
417/// the raw data.
418///
419/// The purpose of this extra smoothing step is to reduce instantaneous fluctations in the estimate
420/// when large updates are received. Without this, estimates might have a large spike followed by a
421/// slow asymptotic approach to zero (until the next spike).
422#[derive(Debug)]
423pub(crate) struct Estimator {
424    smoothed_steps_per_sec: f64,
425    double_smoothed_steps_per_sec: f64,
426    prev_steps: u64,
427    prev_time: Instant,
428    start_time: Instant,
429}
430
431impl Estimator {
432    fn new(now: Instant) -> Self {
433        Self {
434            smoothed_steps_per_sec: 0.0,
435            double_smoothed_steps_per_sec: 0.0,
436            prev_steps: 0,
437            prev_time: now,
438            start_time: now,
439        }
440    }
441
442    fn record(&mut self, new_steps: u64, now: Instant) {
443        // sanity check: don't record data if time or steps have not advanced
444        if new_steps <= self.prev_steps || now <= self.prev_time {
445            // Reset on backwards seek to prevent breakage from seeking to the end for length determination
446            // See https://github.com/console-rs/indicatif/issues/480
447            if new_steps < self.prev_steps {
448                self.prev_steps = new_steps;
449                self.reset(now);
450            }
451            return;
452        }
453
454        let delta_steps = new_steps - self.prev_steps;
455        let delta_t = duration_to_secs(now - self.prev_time);
456
457        // the rate of steps we saw in this update
458        let new_steps_per_second = delta_steps as f64 / delta_t;
459
460        // update the estimate: a weighted average of the old estimate and new data
461        let weight = estimator_weight(delta_t);
462        self.smoothed_steps_per_sec =
463            self.smoothed_steps_per_sec * weight + new_steps_per_second * (1.0 - weight);
464
465        // An iterative estimate like `smoothed_steps_per_sec` is supposed to be an exponentially
466        // weighted average from t=0 back to t=-inf; Since we initialize it to 0, we neglect the
467        // (non-existent) samples in the weighted average prior to the first one, so the resulting
468        // average must be normalized. We normalize the single estimate here in order to use it as
469        // a source for the double smoothed estimate. See comment on normalization in
470        // `steps_per_second` for details.
471        let delta_t_start = duration_to_secs(now - self.start_time);
472        let total_weight = 1.0 - estimator_weight(delta_t_start);
473        let normalized_smoothed_steps_per_sec = self.smoothed_steps_per_sec / total_weight;
474
475        // determine the double smoothed value (EWA smoothing of the single EWA)
476        self.double_smoothed_steps_per_sec = self.double_smoothed_steps_per_sec * weight
477            + normalized_smoothed_steps_per_sec * (1.0 - weight);
478
479        self.prev_steps = new_steps;
480        self.prev_time = now;
481    }
482
483    /// Reset the state of the estimator. Once reset, estimates will not depend on any data prior
484    /// to `now`. This does not reset the stored position of the progress bar.
485    pub(crate) fn reset(&mut self, now: Instant) {
486        self.smoothed_steps_per_sec = 0.0;
487        self.double_smoothed_steps_per_sec = 0.0;
488
489        // only reset prev_time, not prev_steps
490        self.prev_time = now;
491        self.start_time = now;
492    }
493
494    /// Average time per step in seconds, using double exponential smoothing
495    fn steps_per_second(&self, now: Instant) -> f64 {
496        // Because the value stored in the Estimator is only updated when the Estimator receives an
497        // update, this value will become stuck if progress stalls. To return an accurate estimate,
498        // we determine how much time has passed since the last update, and treat this as a
499        // pseudo-update with 0 steps.
500        let delta_t = duration_to_secs(now - self.prev_time);
501        let reweight = estimator_weight(delta_t);
502
503        // Normalization of estimates:
504        //
505        // The raw estimate is a single value (smoothed_steps_per_second) that is iteratively
506        // updated. At each update, the previous value of the estimate is downweighted according to
507        // its age, receiving the iterative weight W(t) = 0.1 ^ (t/15).
508        //
509        // Since W(Sum(t_n)) = Prod(W(t_n)), the total weight of a sample after a series of
510        // iterative steps is simply W(t_e) - W(t_b), where t_e is the time since the end of the
511        // sample, and t_b is the time since the beginning. The resulting estimate is therefore a
512        // weighted average with sample weights W(t_e) - W(t_b).
513        //
514        // Notice that the weighting function generates sample weights that sum to 1 only when the
515        // sample times span from t=0 to t=inf; but this is not the case. We have a first sample
516        // with finite, positive t_b = t_f. In the raw estimate, we handle times prior to t_f by
517        // setting an initial value of 0, meaning that these (non-existent) samples have no weight.
518        //
519        // Therefore, the raw estimate must be normalized by dividing it by the sum of the weights
520        // in the weighted average. This sum is just W(0) - W(t_f), where t_f is the time since the
521        // first sample, and W(0) = 1.
522        let delta_t_start = duration_to_secs(now - self.start_time);
523        let total_weight = 1.0 - estimator_weight(delta_t_start);
524
525        // Generate updated values for `smoothed_steps_per_sec` and `double_smoothed_steps_per_sec`
526        // (sps and dsps) without storing them. Note that we normalize sps when using it as a
527        // source to update dsps, and then normalize dsps itself before returning it.
528        let sps = self.smoothed_steps_per_sec * reweight / total_weight;
529        let dsps = self.double_smoothed_steps_per_sec * reweight + sps * (1.0 - reweight);
530        dsps / total_weight
531    }
532}
533
534pub(crate) struct AtomicPosition {
535    pub(crate) pos: AtomicU64,
536    capacity: AtomicU8,
537    prev: AtomicU64,
538    start: Instant,
539}
540
541impl AtomicPosition {
542    pub(crate) fn new() -> Self {
543        Self {
544            pos: AtomicU64::new(0),
545            capacity: AtomicU8::new(MAX_BURST),
546            prev: AtomicU64::new(0),
547            start: Instant::now(),
548        }
549    }
550
551    pub(crate) fn allow(&self, now: Instant) -> bool {
552        if now < self.start {
553            return false;
554        }
555
556        let mut capacity = self.capacity.load(Ordering::Acquire);
557        // `prev` is the number of ns after `self.started` we last returned `true`
558        let prev = self.prev.load(Ordering::Acquire);
559        // `elapsed` is the number of ns since `self.started`
560        let elapsed = (now - self.start).as_nanos() as u64;
561        // `diff` is the number of ns since we last returned `true`
562        let diff = elapsed.saturating_sub(prev);
563
564        // If `capacity` is 0 and not enough time (1ms) has passed since `prev`
565        // to add new capacity, return `false`. The goal of this method is to
566        // make this decision as efficient as possible.
567        if capacity == 0 && diff < INTERVAL {
568            return false;
569        }
570
571        // We now calculate `new`, the number of INTERVALs since we last returned `true`,
572        // and `remainder`, which represents a number of ns less than INTERVAL which we cannot
573        // convert into capacity now, so we're saving it for later. We do this by
574        // subtracting this from `elapsed` before storing it into `self.prev`.
575        let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL));
576        // We add `new` to `capacity`, subtract one for returning `true` from here,
577        // then make sure it does not exceed a maximum of `MAX_BURST`.
578        capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8;
579
580        // Then, we just store `capacity` and `prev` atomically for the next iteration
581        self.capacity.store(capacity, Ordering::Release);
582        self.prev.store(elapsed - remainder, Ordering::Release);
583        true
584    }
585
586    fn reset(&self, now: Instant) {
587        self.set(0);
588        let elapsed = (now.saturating_duration_since(self.start)).as_nanos() as u64;
589        self.prev.store(elapsed, Ordering::Release);
590    }
591
592    pub(crate) fn inc(&self, delta: u64) {
593        self.pos.fetch_add(delta, Ordering::SeqCst);
594    }
595
596    pub(crate) fn dec(&self, delta: u64) {
597        self.pos.fetch_sub(delta, Ordering::SeqCst);
598    }
599
600    pub(crate) fn set(&self, pos: u64) {
601        self.pos.store(pos, Ordering::Release);
602    }
603}
604
605const INTERVAL: u64 = 1_000_000;
606const MAX_BURST: u8 = 10;
607
608/// Behavior of a progress bar when it is finished
609///
610/// This is invoked when a [`ProgressBar`] or [`ProgressBarIter`] completes and
611/// [`ProgressBar::is_finished`] is false.
612///
613/// [`ProgressBar`]: crate::ProgressBar
614/// [`ProgressBarIter`]: crate::ProgressBarIter
615/// [`ProgressBar::is_finished`]: crate::ProgressBar::is_finished
616#[derive(Clone, Debug, Default)]
617pub enum ProgressFinish {
618    /// Finishes the progress bar and leaves the current message
619    ///
620    /// Same behavior as calling [`ProgressBar::finish()`](crate::ProgressBar::finish).
621    AndLeave,
622    /// Finishes the progress bar and sets a message
623    ///
624    /// Same behavior as calling [`ProgressBar::finish_with_message()`](crate::ProgressBar::finish_with_message).
625    WithMessage(Cow<'static, str>),
626    /// Finishes the progress bar and completely clears it (this is the default)
627    ///
628    /// Same behavior as calling [`ProgressBar::finish_and_clear()`](crate::ProgressBar::finish_and_clear).
629    #[default]
630    AndClear,
631    /// Finishes the progress bar and leaves the current message and progress
632    ///
633    /// Same behavior as calling [`ProgressBar::abandon()`](crate::ProgressBar::abandon).
634    Abandon,
635    /// Finishes the progress bar and sets a message, and leaves the current progress
636    ///
637    /// Same behavior as calling [`ProgressBar::abandon_with_message()`](crate::ProgressBar::abandon_with_message).
638    AbandonWithMessage(Cow<'static, str>),
639}
640
641/// Get the appropriate dilution weight for Estimator data given the data's age (in seconds)
642///
643/// Whenever an update occurs, we will create a new estimate using a weight `w_i` like so:
644///
645/// ```math
646/// <new estimate> = <previous estimate> * w_i + <new data> * (1 - w_i)
647/// ```
648///
649/// In other words, the new estimate is a weighted average of the previous estimate and the new
650/// data. We want to choose weights such that for any set of samples where `t_0, t_1, ...` are
651/// the durations of the samples:
652///
653/// ```math
654/// Sum(t_i) = ews ==> Prod(w_i) = 0.1
655/// ```
656///
657/// With this constraint it is easy to show that
658///
659/// ```math
660/// w_i = 0.1 ^ (t_i / ews)
661/// ```
662///
663/// Notice that the constraint implies that estimates are independent of the durations of the
664/// samples, a very useful feature.
665fn estimator_weight(age: f64) -> f64 {
666    const EXPONENTIAL_WEIGHTING_SECONDS: f64 = 15.0;
667    0.1_f64.powf(age / EXPONENTIAL_WEIGHTING_SECONDS)
668}
669
670fn duration_to_secs(d: Duration) -> f64 {
671    d.as_secs() as f64 + f64::from(d.subsec_nanos()) / 1_000_000_000f64
672}
673
674fn secs_to_duration(s: f64) -> Duration {
675    let secs = s.trunc() as u64;
676    let nanos = (s.fract() * 1_000_000_000f64) as u32;
677    Duration::new(secs, nanos)
678}
679
680#[derive(Debug, Clone, Copy)]
681pub(crate) enum Status {
682    InProgress,
683    DoneVisible(Duration),
684    DoneHidden(Duration),
685}
686
687pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use crate::ProgressBar;
693
694    // https://github.com/rust-lang/rust-clippy/issues/10281
695    #[allow(clippy::uninlined_format_args)]
696    #[test]
697    fn test_steps_per_second() {
698        let test_rate = |items_per_second| {
699            let mut now = Instant::now();
700            let mut est = Estimator::new(now);
701            let mut pos = 0;
702
703            for _ in 0..20 {
704                pos += items_per_second;
705                now += Duration::from_secs(1);
706                est.record(pos, now);
707            }
708            let avg_steps_per_second = est.steps_per_second(now);
709
710            assert!(avg_steps_per_second > 0.0);
711            assert!(avg_steps_per_second.is_finite());
712
713            let absolute_error = (avg_steps_per_second - items_per_second as f64).abs();
714            let relative_error = absolute_error / items_per_second as f64;
715            assert!(
716                relative_error < 1.0 / 1e9,
717                "Expected rate: {}, actual: {}, relative error: {}",
718                items_per_second,
719                avg_steps_per_second,
720                relative_error
721            );
722        };
723
724        test_rate(1);
725        test_rate(1_000);
726        test_rate(1_000_000);
727        test_rate(1_000_000_000);
728        test_rate(1_000_000_001);
729        test_rate(100_000_000_000);
730        test_rate(1_000_000_000_000);
731        test_rate(100_000_000_000_000);
732        test_rate(1_000_000_000_000_000);
733    }
734
735    #[test]
736    fn test_double_exponential_ave() {
737        let mut now = Instant::now();
738        let mut est = Estimator::new(now);
739        let mut pos = 0;
740
741        // note: this is the default weight set in the Estimator
742        let weight = 15;
743
744        for _ in 0..weight {
745            pos += 1;
746            now += Duration::from_secs(1);
747            est.record(pos, now);
748        }
749        now += Duration::from_secs(weight);
750
751        // The first level EWA:
752        //   -> 90% weight @ 0 eps, 9% weight @ 1 eps, 1% weight @ 0 eps
753        //   -> then normalized by deweighting the 1% weight (before -30 seconds)
754        let single_target = 0.09 / 0.99;
755
756        // The second level EWA:
757        //   -> same logic as above, but using the first level EWA as the source
758        let double_target = (0.9 * single_target + 0.09) / 0.99;
759        assert_eq!(est.steps_per_second(now), double_target);
760    }
761
762    #[test]
763    fn test_estimator_rewind_position() {
764        let mut now = Instant::now();
765        let mut est = Estimator::new(now);
766
767        now += Duration::from_secs(1);
768        est.record(1, now);
769
770        // should not panic
771        now += Duration::from_secs(1);
772        est.record(0, now);
773
774        // check that reset occurred (estimator at 1 event per sec)
775        now += Duration::from_secs(1);
776        est.record(1, now);
777        assert_eq!(est.steps_per_second(now), 1.0);
778
779        // check that progress bar handles manual seeking
780        let pb = ProgressBar::hidden();
781        pb.set_length(10);
782        pb.set_position(1);
783        pb.tick();
784        // Should not panic.
785        pb.set_position(0);
786    }
787
788    #[test]
789    fn test_reset_eta() {
790        let mut now = Instant::now();
791        let mut est = Estimator::new(now);
792
793        // two per second, then reset
794        now += Duration::from_secs(1);
795        est.record(2, now);
796        est.reset(now);
797
798        // now one per second, and verify
799        now += Duration::from_secs(1);
800        est.record(3, now);
801        assert_eq!(est.steps_per_second(now), 1.0);
802    }
803
804    #[test]
805    fn test_duration_stuff() {
806        let duration = Duration::new(42, 100_000_000);
807        let secs = duration_to_secs(duration);
808        assert_eq!(secs_to_duration(secs), duration);
809    }
810
811    #[test]
812    fn test_duration_after_finish_and_leave() {
813        let duration = Duration::from_secs(42);
814        let now = Instant::now();
815
816        let mut state = state_started_at(now - duration);
817        state.finish_using_style(now, ProgressFinish::AndLeave);
818
819        assert_eq!(
820            state.state.duration(),
821            duration,
822            "Expected duration: {}, actual: {}",
823            duration_to_secs(duration),
824            duration_to_secs(state.state.duration())
825        );
826    }
827
828    #[test]
829    fn test_duration_after_finish_and_clear() {
830        let duration = Duration::from_secs(42);
831        let now = Instant::now();
832
833        let mut state = state_started_at(now - duration);
834        state.finish_using_style(now, ProgressFinish::AndClear);
835
836        assert_eq!(
837            state.state.duration(),
838            duration,
839            "Expected duration: {}, actual: {}",
840            duration_to_secs(duration),
841            duration_to_secs(state.state.duration())
842        );
843    }
844
845    fn state_started_at(started: Instant) -> BarState {
846        BarState {
847            draw_target: ProgressDrawTarget::hidden(),
848            on_finish: ProgressFinish::default(),
849            style: ProgressStyle::default_bar(),
850            state: {
851                ProgressState {
852                    pos: Arc::new(AtomicPosition::new()),
853                    len: None,
854                    tick: 0,
855                    status: Status::InProgress,
856                    started,
857                    est: Estimator::new(started),
858                    message: TabExpandedString::NoTabs("".into()),
859                    prefix: TabExpandedString::NoTabs("".into()),
860                }
861            },
862            tab_width: DEFAULT_TAB_WIDTH,
863        }
864    }
865
866    #[test]
867    fn test_atomic_position_large_time_difference() {
868        let atomic_position = AtomicPosition::new();
869        let later = atomic_position.start + Duration::from_nanos(INTERVAL * u64::from(u8::MAX));
870        // Should not panic.
871        atomic_position.allow(later);
872    }
873
874    #[test]
875    fn test_atomic_position_reset() {
876        const ELAPSE_TIME: Duration = Duration::from_millis(20);
877        let mut pos = AtomicPosition::new();
878        pos.reset(pos.start + ELAPSE_TIME);
879
880        // prev should be exactly ELAPSE_TIME after reset
881        assert_eq!(*pos.pos.get_mut(), 0);
882        assert_eq!(*pos.prev.get_mut(), ELAPSE_TIME.as_nanos() as u64);
883    }
884}