mtlog_progress/
lib.rs

1//! # mtlog-progress
2//! A progress bar implementation working gracefully with mtlog's logger.
3//!
4//! ## Usage with std threads
5//! ```toml
6//! // Cargo.toml
7//! ...
8//! [dependencies]
9//! mtlog-progress = "0.2.0"
10//! mtlog = "0.2.0"
11//! ```
12//!
13//! ```rust
14//! use mtlog::logger_config;
15//! use mtlog_progress::LogProgressBar;
16//!
17//! let _guard = logger_config()
18//!     .init_global();
19//!
20//! let h = std::thread::spawn(|| {
21//!     let pb = LogProgressBar::new(100, "My Progress Bar");
22//!     for i in 0..100 {
23//!         pb.inc(1);
24//!         if i == 50 {
25//!             log::info!("Halfway there!");
26//!         }
27//!     }
28//!     pb.finish();
29//! });
30//! log::info!("This log goes below the progress bar");
31//! h.join().unwrap(); // the progress bar continue to work at it's line position
32//! // guard ensures logs are flushed when dropped
33//! ```
34//! ## Usage with tokio tasks
35//!
36//! ## Usage
37//! ```toml
38//! // Cargo.toml
39//! ...
40//! [dependencies]
41//! mtlog-progress = "0.1.0"
42//! mtlog-tokio = "0.1.0"
43//! tokio = { version = "1.40.0", features = ["full"] }
44//! ```
45//!
46//! ```rust
47//! use mtlog_tokio::logger_config;
48//! use mtlog_progress::LogProgressBar;
49//!
50//! #[tokio::main]
51//! async fn main() {
52//!     logger_config()
53//!         .scope_global(async move {
54//!             let h = tokio::spawn(async move {
55//!                 logger_config()
56//!                     .scope_local(async move {
57//!                         let pb = LogProgressBar::new(100, "My Progress Bar");
58//!                         for i in 0..100 {
59//!                             pb.inc(1);
60//!                             if i == 50 {
61//!                                 log::info!("Halfway there!");
62//!                             }
63//!                         }
64//!                         pb.finish();
65//!                     }).await;    
66//!             });
67//!             log::info!("This log goes below the progress bar");
68//!             h.await.unwrap(); // the progress bar continue to work at it's line position
69//!         }).await;
70//! }
71//! ```
72
73use colored::Colorize;
74use std::{
75    sync::{Arc, Mutex},
76    time::{Duration, Instant},
77};
78use uuid::Uuid;
79
80#[derive(Clone)]
81pub struct LogProgressBar {
82    n_iter: Arc<usize>,
83    name: Arc<str>,
84    current_iter: Arc<Mutex<usize>>,
85    id: Arc<Uuid>,
86    finished: Arc<Mutex<bool>>,
87    min_duration: Arc<Duration>,
88    last_iter: Arc<Mutex<Instant>>,
89    last_percentage: Arc<Mutex<f64>>,
90    min_percentage_change: Arc<f64>,
91}
92
93impl LogProgressBar {
94    pub fn new(n_iter: usize, name: &str) -> Self {
95        let pb = Self {
96            n_iter: Arc::new(n_iter.max(1)),
97            name: name.into(),
98            current_iter: Arc::new(Mutex::new(0usize)),
99            id: Arc::new(Uuid::new_v4()),
100            finished: Arc::new(Mutex::new(false)),
101            min_duration: Arc::new(Duration::from_millis(100)),
102            last_iter: Arc::new(Mutex::new(Instant::now() - Duration::from_millis(100))),
103            last_percentage: Arc::new(Mutex::new(0.0)),
104            min_percentage_change: Arc::new(0.1),
105        };
106        pb.send();
107        pb
108    }
109
110    pub fn with_min_timestep_ms(mut self, min_duration_ms: f64) -> Self {
111        self.min_duration = Arc::new(Duration::from_micros(
112            (min_duration_ms * 1000.0).round() as u64
113        ));
114        self
115    }
116
117    pub fn with_min_percentage_change(mut self, min_percentage: f64) -> Self {
118        self.min_percentage_change = Arc::new(min_percentage);
119        self
120    }
121
122    pub fn send(&self) {
123        if *self.finished.lock().unwrap() {
124            return;
125        }
126
127        let current_iter = *self.current_iter.lock().unwrap();
128        let current_percentage = (current_iter as f64 / *self.n_iter as f64) * 100.0;
129        let last_percentage = *self.last_percentage.lock().unwrap();
130        let time_elapsed = self.last_iter.lock().unwrap().elapsed() > *self.min_duration;
131        let percentage_changed =
132            (current_percentage - last_percentage).abs() >= *self.min_percentage_change;
133
134        if time_elapsed || percentage_changed {
135            log::info!("___PROGRESS___{}___{}", self.id, self.format());
136            *self.last_iter.lock().unwrap() = Instant::now();
137            *self.last_percentage.lock().unwrap() = current_percentage;
138        }
139    }
140
141    pub fn set_progress(&self, n: usize) {
142        *self.current_iter.lock().unwrap() = n;
143        self.send();
144    }
145
146    pub fn inc(&self, n: usize) {
147        *self.current_iter.lock().unwrap() += n;
148        self.send();
149    }
150
151    fn format(&self) -> String {
152        let current_iter = *self.current_iter.lock().unwrap();
153        let percentage = (current_iter as f64 / *self.n_iter as f64 * 100.0) as usize;
154        let bar_length = 20; // Length of the progress bar
155        let filled_length = (bar_length * current_iter / *self.n_iter).min(bar_length);
156        let bar = "#".repeat(filled_length) + &".".repeat(bar_length - filled_length);
157        let n_iter_str = self.n_iter.to_string();
158        format!(
159            "Progress {name}: [{bar}] {current:>len$}/{n_iter_str} {percentage:>3}%",
160            name = self.name.cyan(),
161            bar = bar.cyan(),
162            current = current_iter,
163            len = n_iter_str.len(),
164        )
165    }
166
167    pub fn finish(&self) {
168        if *self.finished.lock().unwrap() {
169            return;
170        }
171        self.set_progress(*self.n_iter);
172        *self.finished.lock().unwrap() = true;
173        log::info!("___PROGRESS___{}___FINISHED", self.id)
174    }
175}
176
177impl Drop for LogProgressBar {
178    fn drop(&mut self) {
179        if *self.finished.lock().unwrap() {
180            return;
181        }
182        log::info!("___PROGRESS___{}___FINISHED", self.id);
183    }
184}
185
186#[test]
187fn test_progress_bar() {
188    use mtlog::logger_config;
189    let _guard = logger_config().init_global();
190    let n = 5000000;
191    let handle = std::thread::spawn(move || {
192        let pb = LogProgressBar::new(n, "Background Task");
193        for _ in 0..n / 3 {
194            pb.inc(1);
195        }
196        pb.set_progress(0);
197        for _ in 0..n / 3 {
198            pb.inc(1);
199        }
200        pb.finish();
201    });
202    std::thread::sleep(Duration::from_millis(200));
203    let pb = LogProgressBar::new(n, "Main Task");
204    log::info!("Starting main task");
205    for i in 0..n {
206        if i == 10 {
207            log::info!("Main task is at 10 iterations");
208        }
209        pb.inc(1);
210    }
211    pb.finish();
212    handle.join().unwrap();
213    std::thread::sleep(Duration::from_millis(200));
214    let pb_outer = LogProgressBar::new(10, "Outer loop");
215    for _ in 0..10 {
216        let pb_inner = LogProgressBar::new(n / 10, "Inner loop");
217        for _ in 0..n / 10 {
218            pb_inner.inc(1);
219        }
220        pb_inner.finish();
221        pb_outer.inc(1);
222    }
223    pb_outer.finish();
224}