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 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 let _ = self.draw(true, now);
70 }
71
72 pub(crate) fn reset(&mut self, now: Instant, mode: Reset) {
73 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 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 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 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 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 self.draw_target.mark_zombie();
232 }
233}
234
235pub(crate) enum Reset {
236 Eta,
237 Elapsed,
238 All,
239}
240
241#[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 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 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 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 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 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 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#[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 if new_steps <= self.prev_steps || now <= self.prev_time {
445 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 let new_steps_per_second = delta_steps as f64 / delta_t;
459
460 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 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 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 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 self.prev_time = now;
491 self.start_time = now;
492 }
493
494 fn steps_per_second(&self, now: Instant) -> f64 {
496 let delta_t = duration_to_secs(now - self.prev_time);
501 let reweight = estimator_weight(delta_t);
502
503 let delta_t_start = duration_to_secs(now - self.start_time);
523 let total_weight = 1.0 - estimator_weight(delta_t_start);
524
525 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 let prev = self.prev.load(Ordering::Acquire);
559 let elapsed = (now - self.start).as_nanos() as u64;
561 let diff = elapsed.saturating_sub(prev);
563
564 if capacity == 0 && diff < INTERVAL {
568 return false;
569 }
570
571 let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL));
576 capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8;
579
580 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#[derive(Clone, Debug, Default)]
617pub enum ProgressFinish {
618 AndLeave,
622 WithMessage(Cow<'static, str>),
626 #[default]
630 AndClear,
631 Abandon,
635 AbandonWithMessage(Cow<'static, str>),
639}
640
641fn 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 #[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 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 let single_target = 0.09 / 0.99;
755
756 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 now += Duration::from_secs(1);
772 est.record(0, now);
773
774 now += Duration::from_secs(1);
776 est.record(1, now);
777 assert_eq!(est.steps_per_second(now), 1.0);
778
779 let pb = ProgressBar::hidden();
781 pb.set_length(10);
782 pb.set_position(1);
783 pb.tick();
784 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 now += Duration::from_secs(1);
795 est.record(2, now);
796 est.reset(now);
797
798 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 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 assert_eq!(*pos.pos.get_mut(), 0);
882 assert_eq!(*pos.prev.get_mut(), ELAPSE_TIME.as_nanos() as u64);
883 }
884}