1use std::sync::atomic::{AtomicU64, Ordering};
22use std::sync::Arc;
23
24use parking_lot::Mutex;
25
26pub type ProgressFn = Arc<dyn Fn(u64, u64) + Send + Sync>;
31
32pub const REPORT_INTERVAL: f64 = 0.01;
34
35#[derive(Debug, Clone, Default)]
43pub struct CancelFlag(Arc<std::sync::atomic::AtomicBool>);
44
45impl CancelFlag {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn cancel(&self) {
51 self.0.store(true, Ordering::Relaxed);
52 }
53
54 pub fn is_cancelled(&self) -> bool {
55 self.0.load(Ordering::Relaxed)
56 }
57}
58
59pub struct ProgressTracker {
60 done: AtomicU64,
61 total: u64,
62 callback: Option<ProgressFn>,
63 last_reported: Mutex<f64>,
65}
66
67impl std::fmt::Debug for ProgressTracker {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("ProgressTracker")
70 .field("done", &self.done())
71 .field("total", &self.total)
72 .field("reporting", &self.callback.is_some())
73 .finish()
74 }
75}
76
77impl ProgressTracker {
78 pub fn new(total: u64) -> Self {
80 Self::with_callback(total, None)
81 }
82
83 pub fn with_callback(total: u64, callback: Option<ProgressFn>) -> Self {
84 Self {
85 done: AtomicU64::new(0),
86 total,
87 callback,
88 last_reported: Mutex::new(0.0),
89 }
90 }
91
92 pub fn add(&self, value: u64) {
98 let current = self.done.fetch_add(value, Ordering::Relaxed) + value;
99 let Some(callback) = &self.callback else {
100 return;
101 };
102 let mut last = self.last_reported.lock();
103 let progress = if self.total > 0 {
104 current as f64 / self.total as f64
105 } else {
106 0.0
107 };
108 if progress >= *last + REPORT_INTERVAL {
109 *last = progress;
110 callback(current, self.total);
111 }
112 }
113
114 pub fn done_report(&self) {
119 let Some(callback) = &self.callback else {
120 return;
121 };
122 let mut last = self.last_reported.lock();
123 if *last < 1.0 {
124 *last = 1.0;
125 callback(self.total, self.total);
126 }
127 }
128
129 pub fn done(&self) -> u64 {
130 self.done.load(Ordering::Relaxed)
131 }
132
133 pub fn total(&self) -> u64 {
134 self.total
135 }
136}
137
138pub fn default_progress() -> ProgressFn {
143 Arc::new(|done, total| {
144 use std::io::{IsTerminal, Write};
145 let mut err = std::io::stderr();
146 if !err.is_terminal() {
147 return;
148 }
149 const WIDTH: usize = 40;
150 let fraction = if total == 0 {
151 1.0
152 } else {
153 (done as f64 / total as f64).clamp(0.0, 1.0)
154 };
155 let filled = (fraction * WIDTH as f64).round() as usize;
156 let _ = write!(
157 err,
158 "\r[{}{}] {:5.1}% {done}/{total} bp",
159 "#".repeat(filled),
160 "-".repeat(WIDTH - filled),
161 fraction * 100.0,
162 );
163 if done >= total {
164 let _ = writeln!(err);
165 }
166 let _ = err.flush();
167 })
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 type Recorder = (ProgressFn, Arc<Mutex<Vec<(u64, u64)>>>);
176
177 fn recording() -> Recorder {
178 let seen = Arc::new(Mutex::new(Vec::new()));
179 let sink = seen.clone();
180 (Arc::new(move |d, t| sink.lock().push((d, t))), seen)
181 }
182
183 #[test]
184 fn a_tracker_with_no_callback_just_counts() {
185 let tracker = ProgressTracker::new(100);
186 tracker.add(30);
187 tracker.add(20);
188 assert_eq!(tracker.done(), 50);
189 assert_eq!(tracker.total(), 100);
190 tracker.done_report(); }
192
193 #[test]
194 fn reports_only_once_the_interval_is_crossed() {
195 let (callback, seen) = recording();
196 let tracker = ProgressTracker::with_callback(1000, Some(callback));
197 for _ in 0..9 {
199 tracker.add(1);
200 }
201 assert!(seen.lock().is_empty());
202 tracker.add(1); assert_eq!(&*seen.lock(), &[(10, 1000)]);
204 }
205
206 #[test]
207 fn the_final_report_fires_once_and_says_the_total() {
208 let (callback, seen) = recording();
209 let tracker = ProgressTracker::with_callback(1000, Some(callback));
210 tracker.add(500);
211 tracker.done_report();
212 tracker.done_report();
213 assert_eq!(seen.lock().last(), Some(&(1000, 1000)));
214 assert_eq!(seen.lock().iter().filter(|(d, _)| *d == 1000).count(), 1);
215 }
216
217 #[test]
218 fn a_request_that_finished_exactly_does_not_report_twice() {
219 let (callback, seen) = recording();
220 let tracker = ProgressTracker::with_callback(100, Some(callback));
221 tracker.add(100);
222 assert_eq!(&*seen.lock(), &[(100, 100)]);
223 tracker.done_report();
224 assert_eq!(seen.lock().len(), 1);
225 }
226
227 #[test]
228 fn a_zero_total_never_divides_and_still_completes() {
229 let (callback, seen) = recording();
230 let tracker = ProgressTracker::with_callback(0, Some(callback));
231 tracker.add(5);
232 assert!(seen.lock().is_empty());
233 tracker.done_report();
234 assert_eq!(&*seen.lock(), &[(0, 0)]);
235 }
236
237 #[test]
238 fn concurrent_adds_are_not_lost_and_reports_are_serialised() {
239 let (callback, seen) = recording();
240 let tracker = Arc::new(ProgressTracker::with_callback(8000, Some(callback)));
241 let threads: Vec<_> = (0..8)
242 .map(|_| {
243 let tracker = tracker.clone();
244 std::thread::spawn(move || {
245 for _ in 0..1000 {
246 tracker.add(1);
247 }
248 })
249 })
250 .collect();
251 for t in threads {
252 t.join().unwrap();
253 }
254 assert_eq!(tracker.done(), 8000);
255 let seen = seen.lock();
256 assert!(!seen.is_empty());
257 assert!(seen.len() <= 100, "{} reports", seen.len());
259 assert!(seen.windows(2).all(|w| w[0].0 <= w[1].0));
260 }
261}