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//!
73//! ## Iterator Progress Tracking
74//!
75//! Use the `.progress()` method on iterators for automatic progress tracking:
76//!
77//! ```rust
78//! use mtlog::logger_config;
79//! use mtlog_progress::ProgressIteratorExt;
80//!
81//! let _guard = logger_config().init_global();
82//!
83//! // For ExactSizeIterator, automatically detects length
84//! (0..100)
85//!     .progress("Processing")
86//!     .for_each(|i| {
87//!         // Work with i
88//!     });
89//!
90//! // For any iterator, provide length manually
91//! (0..=99)
92//!     .progress_with(100, "Processing")
93//!     .for_each(|_| {
94//!         // Work
95//!     });
96//! ```
97
98mod iter;
99
100pub use iter::{LogProgressIterator, ProgressIteratorExt};
101
102use colored::Colorize;
103use std::{
104    sync::{Arc, Mutex},
105    time::{Duration, Instant},
106};
107use uuid::Uuid;
108
109#[derive(Clone)]
110pub struct LogProgressBar {
111    n_iter: Arc<usize>,
112    name: Arc<str>,
113    current_iter: Arc<Mutex<usize>>,
114    id: Arc<Uuid>,
115    finished: Arc<Mutex<bool>>,
116    min_duration: Arc<Duration>,
117    last_iter: Arc<Mutex<Instant>>,
118    last_percentage: Arc<Mutex<f64>>,
119    min_percentage_change: Arc<f64>,
120}
121
122impl LogProgressBar {
123    pub fn new(n_iter: usize, name: &str) -> Self {
124        let pb = Self {
125            n_iter: Arc::new(n_iter.max(1)),
126            name: name.into(),
127            current_iter: Arc::new(Mutex::new(0usize)),
128            id: Arc::new(Uuid::new_v4()),
129            finished: Arc::new(Mutex::new(false)),
130            min_duration: Arc::new(Duration::from_millis(100)),
131            last_iter: Arc::new(Mutex::new(Instant::now() - Duration::from_millis(100))),
132            last_percentage: Arc::new(Mutex::new(0.0)),
133            min_percentage_change: Arc::new(0.1),
134        };
135        pb.send();
136        pb
137    }
138
139    pub fn with_min_timestep_ms(mut self, min_duration_ms: f64) -> Self {
140        self.min_duration = Arc::new(Duration::from_micros(
141            (min_duration_ms * 1000.0).round() as u64
142        ));
143        self
144    }
145
146    pub fn with_min_percentage_change(mut self, min_percentage: f64) -> Self {
147        self.min_percentage_change = Arc::new(min_percentage);
148        self
149    }
150
151    pub fn send(&self) {
152        if *self.finished.lock().unwrap() {
153            return;
154        }
155
156        let current_iter = *self.current_iter.lock().unwrap();
157        let current_percentage = (current_iter as f64 / *self.n_iter as f64) * 100.0;
158        let last_percentage = *self.last_percentage.lock().unwrap();
159        let time_elapsed = self.last_iter.lock().unwrap().elapsed() > *self.min_duration;
160        let percentage_changed =
161            (current_percentage - last_percentage).abs() >= *self.min_percentage_change;
162
163        if time_elapsed || percentage_changed {
164            log::info!("___PROGRESS___{}___{}", self.id, self.format());
165            *self.last_iter.lock().unwrap() = Instant::now();
166            *self.last_percentage.lock().unwrap() = current_percentage;
167        }
168    }
169
170    pub fn set_progress(&self, n: usize) {
171        *self.current_iter.lock().unwrap() = n;
172        self.send();
173    }
174
175    pub fn inc(&self, n: usize) {
176        *self.current_iter.lock().unwrap() += n;
177        self.send();
178    }
179
180    fn format(&self) -> String {
181        let current_iter = *self.current_iter.lock().unwrap();
182        let percentage = (current_iter as f64 / *self.n_iter as f64 * 100.0) as usize;
183        let bar_length = 20; // Length of the progress bar
184        let filled_length = (bar_length * current_iter / *self.n_iter).min(bar_length);
185        let bar = "#".repeat(filled_length) + &".".repeat(bar_length - filled_length);
186        let n_iter_str = self.n_iter.to_string();
187        format!(
188            "Progress {name}: [{bar}] {current:>len$}/{n_iter_str} {percentage:>3}%",
189            name = self.name.cyan(),
190            bar = bar.cyan(),
191            current = current_iter,
192            len = n_iter_str.len(),
193        )
194    }
195
196    pub fn finish(&self) {
197        if *self.finished.lock().unwrap() {
198            return;
199        }
200        self.set_progress(*self.n_iter);
201        *self.finished.lock().unwrap() = true;
202        log::info!("___PROGRESS___{}___FINISHED", self.id)
203    }
204}
205
206impl Drop for LogProgressBar {
207    fn drop(&mut self) {
208        if *self.finished.lock().unwrap() {
209            return;
210        }
211        log::info!("___PROGRESS___{}___FINISHED", self.id);
212    }
213}
214
215#[test]
216fn test_progress_bar() {
217    use mtlog::logger_config;
218    let _guard = logger_config().init_global();
219    let n = 5000000;
220    let handle = std::thread::spawn(move || {
221        let pb = LogProgressBar::new(n, "Background Task");
222        for _ in 0..n / 3 {
223            pb.inc(1);
224        }
225        pb.set_progress(0);
226        for _ in 0..n / 3 {
227            pb.inc(1);
228        }
229        pb.finish();
230    });
231    std::thread::sleep(Duration::from_millis(200));
232    let pb = LogProgressBar::new(n, "Main Task");
233    log::info!("Starting main task");
234    for i in 0..n {
235        if i == 10 {
236            log::info!("Main task is at 10 iterations");
237        }
238        pb.inc(1);
239    }
240    pb.finish();
241    handle.join().unwrap();
242    std::thread::sleep(Duration::from_millis(200));
243    let pb_outer = LogProgressBar::new(10, "Outer loop");
244    for _ in 0..10 {
245        let pb_inner = LogProgressBar::new(n / 10, "Inner loop");
246        for _ in 0..n / 10 {
247            pb_inner.inc(1);
248        }
249        pb_inner.finish();
250        pb_outer.inc(1);
251    }
252    pb_outer.finish();
253}