1use std::sync::atomic::{AtomicUsize, Ordering};
4use std::time::{Duration, Instant};
5
6#[derive(Debug, Clone)]
8pub struct ProgressInfo {
9 pub total_jobs: usize,
11 pub completed_jobs: usize,
13 pub failed_jobs: usize,
15 pub running_jobs: usize,
17 pub start_time: Instant,
19 pub estimated_remaining: Option<Duration>,
21 pub throughput: f64,
23}
24
25impl ProgressInfo {
26 pub fn percentage(&self) -> f64 {
28 if self.total_jobs == 0 {
29 100.0
30 } else {
31 (self.completed_jobs as f64 / self.total_jobs as f64) * 100.0
32 }
33 }
34
35 pub fn is_complete(&self) -> bool {
37 self.completed_jobs + self.failed_jobs >= self.total_jobs
38 }
39
40 pub fn elapsed(&self) -> Duration {
42 self.start_time.elapsed()
43 }
44
45 pub fn calculate_eta(&self) -> Option<Duration> {
47 let processed = self.completed_jobs + self.failed_jobs;
48 if processed == 0 || self.throughput <= 0.0 {
49 return None;
50 }
51
52 let remaining = self.total_jobs.saturating_sub(processed);
53 let seconds_remaining = remaining as f64 / self.throughput;
54 Some(Duration::from_secs_f64(seconds_remaining))
55 }
56
57 pub fn format_progress(&self) -> String {
59 format!(
60 "{}/{} ({:.1}%) - {} running, {} failed",
61 self.completed_jobs,
62 self.total_jobs,
63 self.percentage(),
64 self.running_jobs,
65 self.failed_jobs
66 )
67 }
68
69 pub fn format_eta(&self) -> String {
71 match self.estimated_remaining {
72 Some(duration) => {
73 let secs = duration.as_secs();
74 if secs < 60 {
75 format!("{secs}s")
76 } else if secs < 3600 {
77 format!("{}m {}s", secs / 60, secs % 60)
78 } else {
79 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
80 }
81 }
82 None => "calculating...".to_string(),
83 }
84 }
85}
86
87pub struct BatchProgress {
89 total_jobs: AtomicUsize,
90 completed_jobs: AtomicUsize,
91 failed_jobs: AtomicUsize,
92 running_jobs: AtomicUsize,
93 start_time: Instant,
94}
95
96impl Default for BatchProgress {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl BatchProgress {
103 pub fn new() -> Self {
105 Self {
106 total_jobs: AtomicUsize::new(0),
107 completed_jobs: AtomicUsize::new(0),
108 failed_jobs: AtomicUsize::new(0),
109 running_jobs: AtomicUsize::new(0),
110 start_time: Instant::now(),
111 }
112 }
113
114 pub fn add_job(&self) {
116 self.total_jobs.fetch_add(1, Ordering::SeqCst);
117 }
118
119 pub fn start_job(&self) {
121 self.running_jobs.fetch_add(1, Ordering::SeqCst);
122 }
123
124 pub fn complete_job(&self) {
126 self.running_jobs.fetch_sub(1, Ordering::SeqCst);
127 self.completed_jobs.fetch_add(1, Ordering::SeqCst);
128 }
129
130 pub fn fail_job(&self) {
132 self.running_jobs.fetch_sub(1, Ordering::SeqCst);
133 self.failed_jobs.fetch_add(1, Ordering::SeqCst);
134 }
135
136 pub fn get_info(&self) -> ProgressInfo {
138 let total = self.total_jobs.load(Ordering::SeqCst);
139 let completed = self.completed_jobs.load(Ordering::SeqCst);
140 let failed = self.failed_jobs.load(Ordering::SeqCst);
141 let running = self.running_jobs.load(Ordering::SeqCst);
142
143 let elapsed = self.start_time.elapsed();
144 let processed = completed + failed;
145 let throughput = if elapsed.as_secs_f64() > 0.0 {
146 processed as f64 / elapsed.as_secs_f64()
147 } else {
148 0.0
149 };
150
151 let mut info = ProgressInfo {
152 total_jobs: total,
153 completed_jobs: completed,
154 failed_jobs: failed,
155 running_jobs: running,
156 start_time: self.start_time,
157 estimated_remaining: None,
158 throughput,
159 };
160
161 info.estimated_remaining = info.calculate_eta();
162 info
163 }
164
165 pub fn reset(&self) {
167 self.total_jobs.store(0, Ordering::SeqCst);
168 self.completed_jobs.store(0, Ordering::SeqCst);
169 self.failed_jobs.store(0, Ordering::SeqCst);
170 self.running_jobs.store(0, Ordering::SeqCst);
171 }
172}
173
174pub trait ProgressCallback: Send + Sync {
176 fn on_progress(&self, info: &ProgressInfo);
178}
179
180impl<F> ProgressCallback for F
182where
183 F: Fn(&ProgressInfo) + Send + Sync,
184{
185 fn on_progress(&self, info: &ProgressInfo) {
186 self(info)
187 }
188}
189
190pub struct ProgressBar {
192 width: usize,
193 show_eta: bool,
194 show_throughput: bool,
195}
196
197impl Default for ProgressBar {
198 fn default() -> Self {
199 Self {
200 width: 50,
201 show_eta: true,
202 show_throughput: true,
203 }
204 }
205}
206
207impl ProgressBar {
208 pub fn new(width: usize) -> Self {
210 Self {
211 width,
212 ..Default::default()
213 }
214 }
215
216 pub fn render(&self, info: &ProgressInfo) -> String {
218 let percentage = info.percentage();
219 let filled = (percentage / 100.0 * self.width as f64) as usize;
220 let empty = self.width.saturating_sub(filled);
221
222 let bar = format!(
223 "[{}{}] {:.1}%",
224 "=".repeat(filled),
225 " ".repeat(empty),
226 percentage
227 );
228
229 let mut parts = vec![bar];
230
231 parts.push(format!("{}/{}", info.completed_jobs, info.total_jobs));
232
233 if self.show_throughput && info.throughput > 0.0 {
234 parts.push(format!("{:.1} jobs/s", info.throughput));
235 }
236
237 if self.show_eta {
238 parts.push(format!("ETA: {}", info.format_eta()));
239 }
240
241 parts.join(" | ")
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_progress_info() {
251 let info = ProgressInfo {
252 total_jobs: 100,
253 completed_jobs: 25,
254 failed_jobs: 5,
255 running_jobs: 2,
256 start_time: Instant::now(),
257 estimated_remaining: Some(Duration::from_secs(60)),
258 throughput: 2.5,
259 };
260
261 assert_eq!(info.percentage(), 25.0);
262 assert!(!info.is_complete());
263 assert!(info.elapsed().as_millis() < u128::MAX); }
265
266 #[test]
267 fn test_progress_info_formatting() {
268 let info = ProgressInfo {
269 total_jobs: 100,
270 completed_jobs: 50,
271 failed_jobs: 10,
272 running_jobs: 5,
273 start_time: Instant::now(),
274 estimated_remaining: Some(Duration::from_secs(125)),
275 throughput: 1.0,
276 };
277
278 let progress_str = info.format_progress();
279 assert!(progress_str.contains("50/100"));
280 assert!(progress_str.contains("50.0%"));
281 assert!(progress_str.contains("5 running"));
282 assert!(progress_str.contains("10 failed"));
283
284 let eta_str = info.format_eta();
285 assert!(eta_str.contains("2m"));
286 }
287
288 #[test]
289 fn test_batch_progress() {
290 let progress = BatchProgress::new();
291
292 progress.add_job();
294 progress.add_job();
295 progress.add_job();
296
297 let info = progress.get_info();
298 assert_eq!(info.total_jobs, 3);
299 assert_eq!(info.completed_jobs, 0);
300
301 progress.start_job();
303 progress.complete_job();
304
305 let info = progress.get_info();
306 assert_eq!(info.completed_jobs, 1);
307 assert_eq!(info.running_jobs, 0);
308
309 progress.start_job();
311 progress.fail_job();
312
313 let info = progress.get_info();
314 assert_eq!(info.failed_jobs, 1);
315 }
316
317 #[test]
318 fn test_progress_bar() {
319 let bar = ProgressBar::new(20);
320
321 let info = ProgressInfo {
322 total_jobs: 100,
323 completed_jobs: 50,
324 failed_jobs: 0,
325 running_jobs: 0,
326 start_time: Instant::now(),
327 estimated_remaining: Some(Duration::from_secs(60)),
328 throughput: 2.0,
329 };
330
331 let rendered = bar.render(&info);
332 assert!(rendered.contains("[=========="));
333 assert!(rendered.contains("50.0%"));
334 assert!(rendered.contains("50/100"));
335 assert!(rendered.contains("2.0 jobs/s"));
336 assert!(rendered.contains("ETA:"));
337 }
338
339 #[test]
340 fn test_progress_callback() {
341 use std::sync::atomic::AtomicBool;
342 use std::sync::Arc;
343
344 let called = Arc::new(AtomicBool::new(false));
345 let called_clone = Arc::clone(&called);
346
347 let callback = move |_info: &ProgressInfo| {
348 called_clone.store(true, Ordering::SeqCst);
349 };
350
351 let info = ProgressInfo {
352 total_jobs: 1,
353 completed_jobs: 1,
354 failed_jobs: 0,
355 running_jobs: 0,
356 start_time: Instant::now(),
357 estimated_remaining: None,
358 throughput: 1.0,
359 };
360
361 callback.on_progress(&info);
362 assert!(called.load(Ordering::SeqCst));
363 }
364
365 #[test]
366 fn test_eta_calculation() {
367 let info = ProgressInfo {
368 total_jobs: 100,
369 completed_jobs: 25,
370 failed_jobs: 0,
371 running_jobs: 0,
372 start_time: Instant::now(),
373 estimated_remaining: None,
374 throughput: 5.0, };
376
377 let eta = info.calculate_eta();
378 assert!(eta.is_some());
379
380 assert_eq!(eta.unwrap().as_secs(), 15);
382 }
383}