pit_wall/
lib.rs

1//! Measure the progress of execution and report on it.
2//!
3//! Not a profiling library. You define, and report the work done yourself.
4//!
5//! ## Usage
6//! ```rs
7//! use pit_wall::Progress;
8//!
9//! let mut progress = Progress::new("job name", 100);
10//! progress.inc_work_done();
11//! println!("{}", progress.get_progress_string()); // job name 2/100 - 2.0% started 2s ago, eta: 98s
12//! ```
13
14use std::time::{Duration, Instant};
15use tracing::warn;
16
17/// The struct holding the state and functions related to our progress.
18pub struct Progress {
19    name: String,
20    pub work_done: u64,
21    started_at: Instant,
22    work_total: u64,
23}
24
25impl Progress {
26    /// Makes a progress object to track the work done.
27    ///
28    /// # Arguments
29    ///
30    /// * `name` - A string slice to identify the job we're tracking progress for.
31    /// * `work_todo` - The units of work that need to be done to reach 100% progress.
32    ///
33    /// # Examples
34    ///
35    /// ```
36    /// use pit_wall::Progress;
37    /// let mut progress = Progress::new("my job", 100);
38    /// ```
39    pub fn new(name: &str, work_todo: u64) -> Self {
40        Self {
41            name: name.to_owned(),
42            started_at: Instant::now(),
43            work_done: 0,
44            work_total: work_todo,
45        }
46    }
47
48    /// Increment work done by one unit.
49    ///
50    /// ```
51    /// use pit_wall::Progress;
52    /// let mut progress = Progress::new("my job", 100);
53    /// progress.inc_work_done();
54    /// assert_eq!(progress.work_done, 1);
55    /// ```
56    pub fn inc_work_done(&mut self) {
57        self.work_done = self.work_done + 1;
58    }
59
60    /// Increment work done by a given amonut.
61    pub fn inc_work_done_by(&mut self, units: u64) {
62        self.work_done = self.work_done + units;
63    }
64
65    /// Set work done.
66    pub fn set_work_done(&mut self, units: u64) {
67        self.work_done = units;
68    }
69
70    /// Get an estimate in seconds of the estimated seconds remaining.
71    /// Uses basic linear interpolation to come up with an estimate.
72    fn estimate_time_left(&self) -> Duration {
73        if self.work_done > self.work_total {
74            warn!(self.work_done, self.work_total, "work done is larger than work total, using work done == work total to calculate time left");
75        }
76        let work_not_done = self
77            .work_total
78            .checked_sub(self.work_done)
79            .unwrap_or(self.work_total);
80        let not_done_to_done_ratio = work_not_done as f64 / self.work_done as f64;
81        let seconds_since_start = Instant::now() - self.started_at;
82        let eta_seconds = not_done_to_done_ratio * seconds_since_start.as_secs() as f64;
83
84        Duration::from_secs(eta_seconds as u64)
85    }
86
87    /// Returns a formatted string giving a bunch of information on the current progress.
88    /// You may want to log this periodically with whatever logging you have set up.
89    pub fn get_progress_string(&self) -> String {
90        let time_elapsed = format!("{:.0?}", Instant::now().duration_since(self.started_at));
91
92        let eta = if self.work_done == self.work_total {
93            "done!".to_string()
94        } else {
95            humantime::format_duration(self.estimate_time_left()).to_string()
96        };
97
98        format!(
99            "{} {}/{} - {:.1}% started {} ago, eta: {}",
100            self.name,
101            self.work_done,
102            self.work_total,
103            self.work_done as f64 / self.work_total as f64 * 100f64,
104            time_elapsed,
105            eta
106        )
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use std::thread;
113
114    use super::*;
115
116    #[test]
117    fn inc_work_done_test() {
118        let mut progress = Progress::new("test progress", 100);
119        progress.inc_work_done();
120        assert_eq!(progress.work_done, 1);
121    }
122
123    #[test]
124    fn inc_work_done_by_test() {
125        let mut progress = Progress::new("test progress", 100);
126        progress.inc_work_done_by(10);
127        assert_eq!(progress.work_done, 10);
128    }
129
130    #[test]
131    fn set_work_done_test() {
132        let mut progress = Progress::new("test progress", 100);
133        progress.set_work_done(50);
134        assert_eq!(progress.work_done, 50);
135    }
136
137    #[test]
138    fn estimate_eta_test() {
139        let mut progress = Progress::new("test progress", 100);
140        progress.set_work_done(50);
141        thread::sleep(Duration::from_secs(1));
142        let eta = progress.estimate_time_left();
143        assert_eq!(eta, Duration::from_secs(1));
144    }
145
146    #[test]
147    fn get_progress_string_test() {
148        let mut progress = Progress::new("test progress", 100);
149        progress.set_work_done(50);
150
151        // something like `test progress 50/100 - 50.0% started 41ns ago, eta: 0ns`
152        // time elapsed will differ from test to test so we skip testing.
153        let progress_string = progress.get_progress_string();
154
155        assert!(progress_string.starts_with("test progress 50/100 - 50.0% started"));
156        assert!(progress_string.ends_with("ago, eta: 0s"));
157    }
158
159    #[test]
160    fn work_complete_string_test() {
161        let mut progress = Progress::new("test progress", 100);
162        progress.set_work_done(100);
163
164        // something like `test progress 50/100 - 50.0% started 41ns ago, eta: 0ns`
165        // time elapsed will differ from test to test so we skip testing.
166        let progress_string = progress.get_progress_string();
167
168        assert!(progress_string.starts_with("test progress 100/100 - 100.0% started"));
169        assert!(progress_string.ends_with("ago, eta: done!"));
170    }
171
172    #[test]
173    fn guard_against_work_done_overflow_test() {
174        let mut progress = Progress::new("test progress", 2);
175        progress.inc_work_done();
176        progress.inc_work_done();
177        progress.inc_work_done();
178        progress.estimate_time_left();
179    }
180}