progress_observer/lib.rs
1//! Simple utility for scheduling efficient regular progress updates synchronously on long running, singlethreaded tasks.
2//!
3//! Adjusts the interval at which updates are provided automatically based on the length of time taken since the last printout.
4//!
5//! As opposed to a naive implementation that checks the system clock at regular, predetermined intervals, this only checks
6//! the system clock exactly once per progress readout. It then observes the time elapsed since the last readout, and uses
7//! that to estimate how many more ticks to wait until it should observe the clock again for the next one. As a result, this
8//! implementation is extremely efficient, while still being able to give regular updates at a desired time interval.
9//!
10//! If the execution time of individual steps is too chaotic, then the progress updates may become unpredictable and irregular.
11//! However, the observer's operation is largely resilient to even a moderate amount of irregularity in execution time.
12//!
13//! ```
14//! use std::time::Duration;
15//! use progress_observer::prelude::*;
16//! use rand::prelude::*;
17//!
18//! // compute pi by generating random points within a square, and checking if they fall within a circle
19//!
20//! fn pi(total: u64, in_circle: u64) -> f64 {
21//! in_circle as f64 / total as f64 * 4.0
22//! }
23//!
24//! let mut rng = thread_rng();
25//! let mut in_circle: u64 = 0;
26//! let mut observer = Observer::new(Duration::from_secs_f64(0.5));
27//! let n: u64 = 10_000_000;
28//! for i in 1..n {
29//! let (x, y): (f64, f64) = rng.gen();
30//! if x * x + y * y <= 1.0 {
31//! in_circle += 1;
32//! }
33//! if observer.tick() {
34//! reprint!("pi = {}", pi(i, in_circle));
35//! }
36//! }
37//! println!("pi = {}", pi(n, in_circle))
38//! ```
39//!
40//! ```
41//! use std::time::Duration;
42//! use std::io::{stdout, Write};
43//! use progress_observer::prelude::*;
44//! use rand::prelude::*;
45//!
46//! // use the observer as an iterator
47//!
48//! fn pi(total: usize, in_circle: u64) -> f64 {
49//! in_circle as f64 / total as f64 * 4.0
50//! }
51//!
52//! let mut rng = thread_rng();
53//! let mut in_circle: u64 = 0;
54//! let n = 10_000_000;
55//! for (i, should_print) in
56//! Observer::new(Duration::from_secs_f64(0.5))
57//! .take(n)
58//! .enumerate()
59//! {
60//! let (x, y): (f64, f64) = rng.gen();
61//! if x * x + y * y <= 1.0 {
62//! in_circle += 1;
63//! }
64//! if should_print {
65//! reprint!("pi = {}", pi(i, in_circle));
66//! }
67//! }
68//! println!("pi = {}", pi(n, in_circle))
69//! ```
70#![feature(div_duration)]
71use std::time::{Duration, Instant};
72
73pub mod prelude {
74 pub use super::{reprint, Observer, Options};
75}
76
77/// Utility macro for re-printing over the same terminal line.
78/// Useful when used in tandem with a progress observer.
79///
80/// ```
81/// use progress_observer::prelude::*;
82/// use std::time::Duration;
83///
84/// // benchmark how many integers you can add per second
85///
86/// let mut count: u128 = 0;
87///
88/// for should_print in Observer::new(Duration::from_secs(1)).take(100_000_000) {
89/// count += 1;
90/// if should_print {
91/// reprint!("{count}");
92/// count = 0;
93/// }
94/// }
95/// ```
96#[macro_export]
97macro_rules! reprint {
98 ($($tk:tt)*) => {
99 {
100 print!("\r{}", format!($($tk)*));
101 ::std::io::Write::flush(&mut ::std::io::stdout()).unwrap();
102 }
103 };
104}
105
106/// Regular progress update observer.
107pub struct Observer {
108 frequency_target: Duration,
109
110 checkpoint_size: u64,
111 max_checkpoint_size: Option<u64>,
112 delay: u64,
113 max_scale_factor: f64,
114 run_for: Option<Duration>,
115
116 next_checkpoint: u64,
117 last_observation: Instant,
118 first_observation: Instant,
119 ticks: u64,
120 finished: bool,
121}
122
123/// Optional parameters for creating a new progress observer.
124pub struct Options {
125 /// Number of ticks before sending the first report.
126 ///
127 /// The default value of 1 is sometimes quite small, and in combination with the default max_scale_factor of 2,
128 /// it can take several dozen iterations before the typical checkpoint size settles to an appropriate value.
129 /// These unnecessary extra rapid prints can cause the beginning of your observed timeframe to be crowded with
130 /// the expensive syscalls and calculations that might be associated with your operation.
131 /// Setting this value to an approximate estimate of the number of iterations you expect to pass within
132 /// the time frame specified by your frequency target will prevent this frontloading of printouts.
133 pub first_checkpoint: u64,
134
135 /// Specify a maximum number of ticks to wait for in between observations.
136 ///
137 /// In some instances, such as during particularly chaotic computations, the observer
138 /// could erroneously derive an exceedingly large size for the next potential checkpoint. In those situations,
139 /// you might want to specify a maximum number of ticks between progress reports, so that
140 /// the observer doesn't get stuck waiting indefinitely after a bad next checkpoint estimate.
141 pub max_checkpoint_size: Option<u64>,
142
143 /// Delay observations for this many initial ticks.
144 ///
145 /// Sometimes your computation needs time to "warm up", where the first 1 or 2 iterations may take significantly
146 /// longer to process than all subsequent ones. This may throw off the checkpoint estimation. Specify this
147 /// argument to ignore the first n ticks processed, only beginning to record progress after they have elapsed.
148 pub delay: u64,
149
150 /// Maximum factor that subsequent checkpoints are allowed to increase in size by.
151 ///
152 /// Intended to prevent sudden large jumps in checkpoint size between reports. The default value of 2 is generally fine for most cases.
153 /// Panics if the factor is set less than 1.
154 pub max_scale_factor: f64,
155
156 /// Specify a maximum duration to run the observer for.
157 ///
158 /// After the duration has passed, the observer will return `None` from `Iterator::next`.
159 /// Setting this value has no effect if using `Observer::tick` directly.
160 pub run_for: Option<Duration>,
161}
162
163impl Default for Options {
164 fn default() -> Self {
165 Self {
166 first_checkpoint: 1,
167 max_checkpoint_size: None,
168 delay: 0,
169 max_scale_factor: 2.0,
170 run_for: None,
171 }
172 }
173}
174
175impl Observer {
176 /// Create an `Observer` with the specified options.
177 ///
178 /// See the [`Options`] struct for more details on the options that may be specified.
179 ///
180 /// ```
181 /// use std::time::Duration;
182 /// use std::iter::once;
183 /// use progress_observer::prelude::*;
184 ///
185 /// // compute the ratio of prime numbers between 1 and n
186 ///
187 /// fn is_prime(n: u64) -> bool {
188 /// once(2)
189 /// .chain((3..=((n as f32).sqrt() as u64)).step_by(2))
190 /// .find(|i| n % i == 0)
191 /// .is_none()
192 /// }
193 ///
194 /// let mut primes = 0;
195 /// for (n, should_print) in
196 /// Observer::new_with(Duration::from_secs(1), Options {
197 /// max_checkpoint_size: Some(200_000),
198 /// ..Default::default()
199 /// })
200 /// .take(10_000_000)
201 /// .enumerate()
202 /// {
203 /// if is_prime(n as u64) {
204 /// primes += 1;
205 /// }
206 /// if should_print {
207 /// println!("{primes} / {n} = {:.4}", primes as f64 / n as f64);
208 /// }
209 /// }
210 /// ```
211 pub fn new_with(
212 frequency_target: Duration,
213 Options {
214 first_checkpoint: checkpoint_size,
215 max_checkpoint_size,
216 delay,
217 max_scale_factor,
218 run_for,
219 }: Options,
220 ) -> Self {
221 if max_scale_factor < 1.0 {
222 panic!("max_scale_factor of {max_scale_factor} is less than 1.0");
223 }
224 Self {
225 frequency_target,
226
227 checkpoint_size,
228 max_checkpoint_size,
229 delay,
230 max_scale_factor,
231 run_for,
232
233 next_checkpoint: checkpoint_size,
234 last_observation: Instant::now(),
235 first_observation: Instant::now(),
236 ticks: 0,
237 finished: false,
238 }
239 }
240
241 /// Create an `Observer` with a specified starting checkpoint.
242 ///
243 /// Setting just the starting checkpoint alone is often desirable, so this convenience
244 /// method is provided to allow setting it without having to specify a full `Options` struct.
245 ///
246 /// See the [`Options`] struct for more details on what values to set the starting checkpoint to.
247 ///
248 /// ```
249 /// use std::time::Duration;
250 /// use std::iter::once;
251 /// use progress_observer::prelude::*;
252 ///
253 /// // compute the ratio of prime numbers between 1 and n
254 ///
255 /// fn is_prime(n: u64) -> bool {
256 /// once(2)
257 /// .chain((3..=((n as f32).sqrt() as u64)).step_by(2))
258 /// .find(|i| n % i == 0)
259 /// .is_none()
260 /// }
261 ///
262 /// let mut primes = 0;
263 /// for (n, should_print) in
264 /// Observer::new_starting_at(Duration::from_secs(1), 300_000)
265 /// .take(10_000_000)
266 /// .enumerate()
267 /// {
268 /// if is_prime(n as u64) {
269 /// primes += 1;
270 /// }
271 /// if should_print {
272 /// println!("{primes} / {n} = {:.4}", primes as f64 / n as f64);
273 /// }
274 /// }
275 /// ```
276 pub fn new_starting_at(frequency_target: Duration, first_checkpoint: u64) -> Self {
277 Self::new_with(
278 frequency_target,
279 Options {
280 first_checkpoint,
281 ..Default::default()
282 },
283 )
284 }
285
286 /// Create a new `Observer` with the specified frequency target and default options.
287 ///
288 /// The observer will attempt to adjust its reports to match the specified target; if you
289 /// specify 1 second, it will attempt to display progress updates in 1 second intervals.
290 ///
291 /// ```
292 /// use std::time::Duration;
293 /// use std::iter::once;
294 /// use progress_observer::prelude::*;
295 ///
296 /// // compute the ratio of prime numbers between 1 and n
297 ///
298 /// fn is_prime(n: u64) -> bool {
299 /// once(2)
300 /// .chain((3..=((n as f32).sqrt() as u64)).step_by(2))
301 /// .find(|i| n % i == 0)
302 /// .is_none()
303 /// }
304 ///
305 /// let mut primes = 0;
306 /// for (n, should_print) in
307 /// Observer::new(Duration::from_secs(1))
308 /// .take(10_000_000)
309 /// .enumerate()
310 /// {
311 /// if is_prime(n as u64) {
312 /// primes += 1;
313 /// }
314 /// if should_print {
315 /// println!("{primes} / {n} = {:.4}", primes as f64 / n as f64);
316 /// }
317 /// }
318 /// ```
319 pub fn new(frequency_target: Duration) -> Self {
320 Self::new_with(frequency_target, Options::default())
321 }
322
323 /// Tick the observer by n iterations at once.
324 ///
325 /// ```
326 /// use std::time::Duration;
327 /// use std::iter::once;
328 /// use progress_observer::prelude::*;
329 ///
330 /// // compute the ratio of prime numbers between 1 and n
331 ///
332 /// fn is_prime(n: u64) -> bool {
333 /// once(2)
334 /// .chain((3..=((n as f32).sqrt() as u64)).step_by(2))
335 /// .find(|i| n % i == 0)
336 /// .is_none()
337 /// }
338 ///
339 /// let mut primes = 0;
340 /// let mut observer = Observer::new(Duration::from_secs(1));
341 /// for n in 0..10_000_000 {
342 /// if is_prime(n as u64) {
343 /// primes += 1;
344 /// }
345 /// if observer.tick_n(1) {
346 /// println!("{primes} / {n} = {:.4}", primes as f64 / n as f64);
347 /// }
348 /// }
349 /// ```
350 pub fn tick_n(&mut self, mut n: u64) -> bool {
351 if self.delay > 0 {
352 let adjustment = n.min(self.delay);
353 self.delay -= adjustment;
354 n -= adjustment;
355 if self.delay > 0 {
356 return false;
357 } else {
358 self.last_observation = Instant::now();
359 self.first_observation = Instant::now();
360 }
361 }
362 self.ticks += n;
363 if self.ticks >= self.next_checkpoint {
364 let observation_time = Instant::now();
365 if self.run_for.is_some_and(|run_for| {
366 observation_time.duration_since(self.first_observation) > run_for
367 }) {
368 self.finished = true;
369 }
370 let time_since_observation = observation_time.duration_since(self.last_observation);
371 let checkpoint_ratio = time_since_observation.div_duration_f64(self.frequency_target);
372 let checkpoint_size = self.checkpoint_size as f64;
373 self.checkpoint_size = ((checkpoint_size / checkpoint_ratio) as u64)
374 .max(1)
375 .min((checkpoint_size * self.max_scale_factor) as u64);
376 if let Some(max_size) = self.max_checkpoint_size {
377 self.checkpoint_size = self.checkpoint_size.min(max_size);
378 }
379 self.next_checkpoint += self.checkpoint_size;
380 self.last_observation = observation_time;
381 true
382 } else {
383 false
384 }
385 }
386
387 /// Tick the observer by 1 iteration.
388 ///
389 /// The `tick` method will report a `true` value every time it thinks a progress update
390 /// should occur. This is based on the passed frequency_target when the observer is created.
391 ///
392 /// ```
393 /// use std::time::Duration;
394 /// use std::iter::once;
395 /// use progress_observer::prelude::*;
396 ///
397 /// // compute the ratio of prime numbers between 1 and n
398 ///
399 /// fn is_prime(n: u64) -> bool {
400 /// once(2)
401 /// .chain((3..=((n as f32).sqrt() as u64)).step_by(2))
402 /// .find(|i| n % i == 0)
403 /// .is_none()
404 /// }
405 ///
406 /// let mut primes = 0;
407 /// let mut observer = Observer::new(Duration::from_secs(1));
408 /// for n in 0..10_000_000 {
409 /// if is_prime(n as u64) {
410 /// primes += 1;
411 /// }
412 /// if observer.tick() {
413 /// println!("{primes} / {n} = {:.4}", primes as f64 / n as f64);
414 /// }
415 /// }
416 /// ```
417 pub fn tick(&mut self) -> bool {
418 self.tick_n(1)
419 }
420}
421
422impl Iterator for Observer {
423 type Item = bool;
424
425 fn next(&mut self) -> Option<Self::Item> {
426 (!self.finished).then(|| self.tick())
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn reprint() {
436 let mut count: u64 = 0;
437 for should_print in Observer::new(Duration::from_secs(1)).take(1_000_000_000) {
438 count += 1;
439 if should_print {
440 reprint!("{count: <20}");
441 count = 0;
442 }
443 }
444 }
445
446 #[test]
447 fn delay() {
448 for (i, should_print) in Observer::new_with(
449 Duration::from_secs(1),
450 Options {
451 max_checkpoint_size: Some(2),
452 delay: 5,
453 ..Default::default()
454 },
455 )
456 .enumerate()
457 .take(10)
458 {
459 println!("{i}: {should_print}");
460 }
461 }
462
463 #[test]
464 fn run_for() {
465 for (i, should_print) in Observer::new_with(
466 Duration::from_secs_f32(0.1),
467 Options {
468 run_for: Some(Duration::from_secs(5)),
469 ..Default::default()
470 },
471 )
472 .enumerate()
473 {
474 if should_print {
475 reprint!("{i}");
476 }
477 }
478 }
479}