progress_logger/
lib.rs

1#[macro_use]
2extern crate log;
3
4use std::time::{Duration, Instant};
5use sysinfo::{System, SystemExt};
6
7/// A tool to report the progress of computations. It can be built and configured
8/// using the `builder` function. If given the expected number of updates,
9/// reports the expected time to completion, based on the current throughtput.
10///
11/// Progress is reported every 10 seconds by default. See the examples about how
12/// to change it.
13///
14/// There are three methods to update the internal counter:
15///
16///  - `update`, for events that don't happen frequently
17///  - `update_light`, which tries to report (by checking the configured frequency
18///     of updates) only once every million updates. To be used in situations where
19///     updates are frequent: it's an order of magnitude faster than `update`.
20///
21/// Reports are issued on the console using the `info!()` macro from the `log` crate.
22/// Therefore, the reports depend on your logging configuration.
23///
24/// Inspired by `ProgressLogger` in the [`dsiutil`](http://dsiutils.di.unimi.it/docs/it/unimi/dsi/logging/ProgressLogger.html) Java library.
25///
26/// # Examples
27///
28/// ## Basic usage
29/// ```
30/// use progress_logger::ProgressLogger;
31///
32/// let mut pl = ProgressLogger::builder().start();
33/// let mut cnt = 0;
34/// for i in 0..10000 {
35///     cnt += 1;
36///     pl.update(1u32);
37/// }
38/// pl.stop();
39/// ```
40///
41/// ## Reporting every 5 seconds
42/// ```
43/// use progress_logger::ProgressLogger;
44/// use std::time::Duration;
45///
46/// let mut pl = ProgressLogger::builder()
47///     .with_frequency(Duration::from_secs(5))
48///     .start();
49/// let mut cnt = 0;
50/// for i in 0..10000 {
51///     cnt += 1;
52///     pl.update(1u32);
53/// }
54/// pl.stop();
55///
56/// ```
57///
58/// ## Changing the names of updates
59/// ```
60/// use progress_logger::ProgressLogger;
61///
62/// let mut pl = ProgressLogger::builder()
63///     .with_items_name("points")
64///     .start();
65/// let mut cnt = 0;
66/// for i in 0..10000 {
67///     cnt += 1;
68///     pl.update(1u32);
69/// }
70/// pl.stop();
71/// ```
72pub struct ProgressLogger {
73    start: Instant,
74    count: u64,
75    expected_updates: Option<u64>,
76    items: String,
77    last_logged: Instant,
78    /// the estimated time to completion, in seconds
79    ettc: Option<f64>,
80    throughput: Option<f64>,
81    frequency: Duration,
82    system: System,
83}
84
85impl ProgressLogger {
86    /// Creates a builder to configure a new progress logger
87    pub fn builder() -> ProgressLoggerBuilder {
88        ProgressLoggerBuilder {
89            expected_updates: None,
90            items: None,
91            frequency: None,
92        }
93    }
94
95    fn log(&mut self) {
96        let elapsed = Instant::now() - self.start;
97        let throughput = self.count as f64 / elapsed.as_secs_f64();
98        self.throughput.replace(throughput);
99        self.system.refresh_memory();
100        let used_kb = PrettyNumber::from(self.system.get_used_memory());
101        let used_swap_kb = PrettyNumber::from(self.system.get_used_swap());
102        if let Some(expected_updates) = self.expected_updates {
103            let prediction = (expected_updates - self.count) as f64 / throughput;
104            self.ettc.replace(prediction);
105            info!(
106                "[mem: {} kB, swap: {} kB] {:.2?} {} {}, {:.2} s left ({:.2} {}/s)",
107                used_kb,
108                used_swap_kb,
109                elapsed,
110                PrettyNumber::from(self.count),
111                self.items,
112                prediction,
113                PrettyNumber::from(throughput),
114                self.items
115            );
116        } else {
117            info!(
118                "[mem: {} kB, swap: {} kB] {:.2?} {} {} ({:.2} {}/s)",
119                used_kb,
120                used_swap_kb,
121                elapsed,
122                PrettyNumber::from(self.count),
123                self.items,
124                PrettyNumber::from(throughput),
125                self.items
126            );
127        }
128    }
129
130    /// Get the estimated time to completion, if such prediction is available
131    pub fn time_to_completion(&self) -> Option<Duration> {
132        self.ettc.map(Duration::from_secs_f64)
133    }
134
135    pub fn throughput(&self) -> Option<f64> {
136        self.throughput
137    }
138
139    /// Try to report progress only once every million updates
140    #[inline]
141    pub fn update_light<N: Into<u64>>(&mut self, cnt: N) {
142        self.count += cnt.into();
143        if self.count % 1_000_000 == 0 {
144            let now = Instant::now();
145            if (now - self.last_logged) > self.frequency {
146                self.log();
147                self.last_logged = now;
148            }
149        }
150    }
151
152    /// Update the internal counter and report progress if the time
153    /// since the last report is greater than the configured duration
154    #[inline]
155    pub fn update<N: Into<u64>>(&mut self, cnt: N) {
156        let cnt: u64 = cnt.into();
157        self.count += cnt;
158        let now = Instant::now();
159        if (now - self.last_logged) > self.frequency {
160            self.log();
161            self.last_logged = now;
162        }
163    }
164
165    /// Stops and drops the progress logger, logging the completion statement
166    pub fn stop(self) {
167        let elapsed = Instant::now() - self.start;
168        let throughput = self.count as f64 / elapsed.as_secs_f64();
169        info!(
170            "Done in {:.2?}. {} {} ({:.2} {}/s)",
171            elapsed,
172            PrettyNumber::from(self.count),
173            self.items,
174            PrettyNumber::from(throughput),
175            self.items
176        );
177    }
178}
179
180/// Builds a new progress logger. All the configurations are optional,
181/// To obtain a builder, use `ProgressLogger::builder()`.
182pub struct ProgressLoggerBuilder {
183    expected_updates: Option<u64>,
184    items: Option<String>,
185    frequency: Option<Duration>,
186}
187
188impl ProgressLoggerBuilder {
189    /// Configure the expected number of updates.
190    pub fn with_expected_updates<N: Into<u64>>(mut self, updates: N) -> Self {
191        self.expected_updates = Some(updates.into());
192        self
193    }
194    /// Set the name of the items being counted.
195    pub fn with_items_name<S: Into<String>>(mut self, name: S) -> Self {
196        self.items = Some(name.into());
197        self
198    }
199    /// Set the frequency of reports on the console.
200    pub fn with_frequency(mut self, freq: Duration) -> Self {
201        self.frequency = Some(freq);
202        self
203    }
204    /// Builds the `ProgressLogger`, starting the internal timer.
205    pub fn start(self) -> ProgressLogger {
206        let now = Instant::now();
207        ProgressLogger {
208            start: now,
209            count: 0,
210            expected_updates: self.expected_updates,
211            items: self.items.unwrap_or_else(|| "updates".to_owned()),
212            last_logged: now,
213            ettc: None,
214            throughput: None,
215            frequency: self.frequency.unwrap_or_else(|| Duration::from_secs(10)),
216            system: System::default(),
217        }
218    }
219}
220
221struct PrettyNumber {
222    rendered: String,
223}
224
225impl std::fmt::Display for PrettyNumber {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        write!(f, "{}", self.rendered)
228    }
229}
230
231impl std::fmt::Debug for PrettyNumber {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        write!(f, "{}", self.rendered)
234    }
235}
236
237impl From<u64> for PrettyNumber {
238    fn from(n: u64) -> PrettyNumber {
239        let s = format!("{}", n);
240        let tmp: Vec<char> = s.chars().rev().collect();
241        let mut chunks: Vec<&[char]> = tmp.chunks(3).collect();
242
243        let mut rendered = String::new();
244        let mut ul = chunks.len() % 2 == 1;
245        while let Some(chunk) = chunks.pop() {
246            let mut chunk = Vec::from(chunk);
247            if ul {
248                rendered.push_str("\x1B[0m");
249            } else {
250                rendered.push_str("\x1B[4m");
251            }
252            ul = !ul;
253            while let Some(c) = chunk.pop() {
254                rendered.push(c);
255            }
256        }
257        if ul {
258            rendered.push_str("\x1B[0m");
259        }
260
261        PrettyNumber { rendered }
262    }
263}
264
265impl From<f64> for PrettyNumber {
266    fn from(x: f64) -> PrettyNumber {
267        assert!(x >= 0.0, "only positive number are supported for now");
268        let s = format!("{:.2}", x);
269        let mut parts = s.split(".");
270        let s = parts.next().expect("missing integer part");
271        let decimal = parts.next();
272        let tmp: Vec<char> = s.chars().rev().collect();
273        let mut chunks: Vec<&[char]> = tmp.chunks(3).collect();
274
275        let mut rendered = String::new();
276        let mut ul = chunks.len() % 2 == 1;
277        while let Some(chunk) = chunks.pop() {
278            let mut chunk = Vec::from(chunk);
279            if ul {
280                rendered.push_str("\x1B[0m");
281            } else {
282                rendered.push_str("\x1B[4m");
283            }
284            ul = !ul;
285            while let Some(c) = chunk.pop() {
286                rendered.push(c);
287            }
288        }
289        if ul {
290            rendered.push_str("\x1B[0m");
291        }
292        if let Some(decimal) = decimal {
293            rendered.push('.');
294            rendered.push_str(decimal);
295        }
296
297        PrettyNumber { rendered }
298    }
299}