1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//! Fast, concurrent, lockless progress bars.

use atomic_counter::{AtomicCounter, RelaxedCounter};
use console::style;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

/// Common interface for components that can show progress of a task. E.g. progress bars.
pub trait ProgressTracker: Sync + Send {
    fn inc(&self, delta: u64);
}

/// A progress bar that doesn't display itself and does nothing.
/// This exists purely because sometimes there is an operation that needs to report progress,
/// but we don't want to show it to the user.
pub struct NoProgressBar;

impl ProgressTracker for NoProgressBar {
    fn inc(&self, _delta: u64) {}
}

/// Console-based progress bar that renders to standard error.
///
/// Implemented as a wrapper over `indicatif::ProgressBar` that makes updating its progress
/// lockless.
/// Unfortunately `indicatif::ProgressBar` wraps state in a `Mutex`, so updates are slow
/// and can become a bottleneck in multithreaded context.
/// This wrapper uses `atomic_counter::RelaxedCounter` to keep shared state without ever blocking
/// writers. That state is copied repeatedly by a background thread to an underlying
/// `ProgressBar` at a low rate.
pub struct FastProgressBar {
    counter: Arc<RelaxedCounter>,
    progress_bar: Arc<ProgressBar>,
}

impl FastProgressBar {
    /// Width of the progress bar in characters
    const WIDTH: usize = 50;
    /// Spinner animation looks like this (moves right and left):
    const SPACESHIP: &'static str = "<===>";
    /// Progress bar looks like this:
    const PROGRESS_CHARS: &'static str = "=> ";
    /// How much time to wait between refreshes, in milliseconds
    const REFRESH_PERIOD_MS: u64 = 50;

    /// Wrap an existing `ProgressBar` and start the background updater-thread.
    /// The thread periodically copies the `FastProgressBar` position into the wrapped
    /// `ProgressBar` instance.
    pub fn wrap(progress_bar: ProgressBar) -> FastProgressBar {
        let pb = Arc::new(progress_bar);
        let pb2 = pb.clone();
        let counter = Arc::new(RelaxedCounter::new(0));
        let counter2 = counter.clone();
        thread::spawn(move || {
            while Arc::strong_count(&counter2) > 1 && !pb2.is_finished() {
                pb2.set_position(counter2.get() as u64);
                thread::sleep(Duration::from_millis(Self::REFRESH_PERIOD_MS));
            }
        });
        FastProgressBar {
            counter,
            progress_bar: pb,
        }
    }

    /// Generate spinner animation strings.
    /// The spinner moves to the next string from the returned vector with every tick.
    /// The spinner is rendered as a SPACESHIP that bounces right and left from the
    /// ends of the spinner bar.
    fn gen_tick_strings() -> Vec<String> {
        let mut tick_strings = vec![];
        for i in 0..(Self::WIDTH - Self::SPACESHIP.len()) {
            let prefix_len = i;
            let suffix_len = Self::WIDTH - i - Self::SPACESHIP.len();
            let tick_str = " ".repeat(prefix_len) + Self::SPACESHIP + &" ".repeat(suffix_len);
            tick_strings.push(tick_str);
        }
        let mut tick_strings_2 = tick_strings.clone();
        tick_strings_2.reverse();
        tick_strings.extend(tick_strings_2);
        tick_strings
    }

    /// Create a new preconfigured animated spinner with given message.
    pub fn new_spinner(msg: &str) -> FastProgressBar {
        let inner = ProgressBar::new_spinner();
        let template =
            style("{msg:32}").cyan().bold().for_stderr().to_string() + "[{spinner}] {pos:>10}";
        let tick_strings = Self::gen_tick_strings();
        let tick_strings: Vec<&str> = tick_strings.iter().map(|s| s as &str).collect();
        inner.set_style(
            ProgressStyle::default_spinner()
                .template(template.as_str())
                .unwrap()
                .tick_strings(tick_strings.as_slice()),
        );
        inner.set_message(msg.to_string());
        Self::wrap(inner)
    }

    /// Create a new preconfigured progress bar with given message.
    pub fn new_progress_bar(msg: &str, len: u64) -> FastProgressBar {
        let inner = ProgressBar::new(len);
        let template = style("{msg:32}").cyan().bold().for_stderr().to_string()
            + &"[{bar:WIDTH}] {pos:>10}/{len}".replace("WIDTH", Self::WIDTH.to_string().as_str());

        inner.set_style(
            ProgressStyle::default_bar()
                .template(template.as_str())
                .unwrap()
                .progress_chars(Self::PROGRESS_CHARS),
        );
        inner.set_message(msg.to_string());
        FastProgressBar::wrap(inner)
    }

    /// Create a new preconfigured progress bar with given message.
    /// Displays progress in bytes.
    pub fn new_bytes_progress_bar(msg: &str, len: u64) -> FastProgressBar {
        let inner = ProgressBar::new(len);
        let template = style("{msg:32}").cyan().bold().for_stderr().to_string()
            + &"[{bar:WIDTH}] {bytes:>10}/{total_bytes}"
                .replace("WIDTH", Self::WIDTH.to_string().as_str());

        inner.set_style(
            ProgressStyle::default_bar()
                .template(template.as_str())
                .unwrap()
                .progress_chars(Self::PROGRESS_CHARS),
        );
        inner.set_message(msg.to_string());

        FastProgressBar::wrap(inner)
    }

    /// Creates a new invisible progress bar.
    /// This is useful when you need to disable progress bar, but you need to pass an instance
    /// of a `ProgressBar` to something that expects it.
    pub fn new_hidden() -> FastProgressBar {
        let inner = ProgressBar::new(u64::MAX);
        inner.set_draw_target(ProgressDrawTarget::hidden());
        FastProgressBar::wrap(inner)
    }

    fn update_progress(&self) {
        let value = self.counter.get() as u64;
        self.progress_bar.set_position(value);
    }

    pub fn set_draw_target(&self, target: ProgressDrawTarget) {
        self.progress_bar.set_draw_target(target)
    }

    pub fn is_visible(&self) -> bool {
        !self.progress_bar.is_hidden()
    }

    pub fn println<I: AsRef<str>>(&self, msg: I) {
        self.progress_bar.println(msg);
    }

    pub fn tick(&self) {
        self.counter.inc();
    }

    pub fn position(&self) -> usize {
        self.counter.get()
    }

    pub fn last_displayed_position(&self) -> u64 {
        self.progress_bar.position()
    }

    pub fn finish(&self) {
        self.update_progress();
        self.progress_bar.finish();
    }

    pub fn finish_and_clear(&self) {
        self.update_progress();
        self.progress_bar.finish_and_clear();
    }

    pub fn abandon(&self) {
        self.update_progress();
        self.progress_bar.abandon();
    }

    pub fn is_finished(&self) -> bool {
        self.progress_bar.is_finished()
    }
}

impl ProgressTracker for FastProgressBar {
    fn inc(&self, delta: u64) {
        self.counter.add(delta as usize);
    }
}

impl Drop for FastProgressBar {
    fn drop(&mut self) {
        if !self.is_finished() {
            self.finish_and_clear();
        }
    }
}

#[cfg(test)]
mod test {

    use super::*;
    use rayon::prelude::*;

    #[test]
    fn all_ticks_should_be_counted() {
        let collection = vec![0; 100000];
        let pb = ProgressBar::new(collection.len() as u64);
        pb.set_draw_target(ProgressDrawTarget::hidden());
        let pb = FastProgressBar::wrap(pb);
        collection
            .par_iter()
            .inspect(|_| pb.tick())
            .for_each(|_| ());
        pb.abandon();
        assert_eq!(pb.position(), 100000);
        assert_eq!(pb.last_displayed_position(), 100000);
    }
}