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}