Skip to main content

how_far/
paced.rs

1//! Checkpoints that reach the pulse only every so many units.
2
3use crate::{ProgressExt, Pulse, StopReason};
4use core::fmt;
5
6/// Counts completed work locally and reaches its pulse once every `every`
7/// units: it reports the units counted so far, then checks for cancellation.
8///
9/// Reaching a live tree costs a few nanoseconds per checkpoint; a step that
10/// does not reach it is a subtraction and a branch. Choose `every` so that the
11/// work between checkpoints takes at least a microsecond, and no longer than
12/// the cancellation latency you need. A pulse that can neither stop nor
13/// report, such as [`NoPulse`](crate::NoPulse), is never reached, and in a
14/// loop its steps compile to nothing.
15///
16/// Make one per worker, from the `&dyn Pulse` the work was given:
17///
18/// ```
19/// use how_far::{Pulse, StopReason};
20///
21/// fn defilter(rows: &mut [Vec<u8>], pulse: &dyn Pulse) -> Result<(), StopReason> {
22///     let mut pace = pulse.paced(64 * 1024);
23///     for row in rows {
24///         for i in 4..row.len() {
25///             row[i] = row[i].wrapping_add(row[i - 4]);
26///         }
27///         pace.step(row.len() as u64)?;
28///     }
29///     pace.finish()
30/// }
31///
32/// defilter(&mut vec![vec![0; 64]; 3], &how_far::NoPulse)?;
33/// # Ok::<(), StopReason>(())
34/// ```
35///
36/// Units not yet reported are reported when the `Paced` is dropped, including
37/// after an early return, so finished work is always counted. Flush before
38/// splitting the pulse it reports to: a phase that split ignores reports.
39/// Accidentally discarding a new guard is diagnosed; retaining one still
40/// requires an explicit `finish` call to check the final partial batch.
41///
42/// ```compile_fail
43/// #![deny(unused_must_use)]
44/// use how_far::{NoPulse, Paced};
45/// Paced::new(&NoPulse, 64);
46/// ```
47#[must_use = "call finish to flush and check cancellation; Drop only flushes counts"]
48pub struct Paced<'a> {
49    pulse: Option<&'a dyn Pulse>,
50    /// Units still to count before the next reach: `every` minus the pending
51    /// units, so a step is one comparison and one subtraction.
52    left: u64,
53    every: u64,
54}
55
56impl fmt::Debug for Paced<'_> {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.debug_struct("Paced")
59            .field("live", &self.pulse.is_some())
60            .field("pending", &(self.every - self.left))
61            .field("every", &self.every)
62            .finish()
63    }
64}
65
66impl<'a> Paced<'a> {
67    /// Reach `pulse` once every `every` units (at least 1).
68    pub fn new(pulse: &'a dyn Pulse, every: u64) -> Self {
69        let every = every.max(1);
70        Self {
71            pulse: pulse.live(),
72            left: every,
73            every,
74        }
75    }
76
77    /// Count `completed` finished units. Once `every` units are pending,
78    /// report them and check for cancellation.
79    #[inline]
80    #[track_caller]
81    pub fn step(&mut self, completed: u64) -> Result<(), StopReason> {
82        // Nothing below writes `pulse`, so in a loop this test is hoisted and
83        // a pulse that cannot stop or report costs nothing per step.
84        let Some(pulse) = self.pulse else {
85            return Ok(());
86        };
87        if completed < self.left {
88            self.left -= completed;
89            Ok(())
90        } else {
91            let pending = (self.every - self.left).saturating_add(completed);
92            self.left = self.every;
93            reach(pulse, pending)
94        }
95    }
96
97    /// Check for cancellation now, whatever is pending.
98    #[inline]
99    #[track_caller]
100    pub fn check(&self) -> Result<(), StopReason> {
101        match self.pulse {
102            Some(pulse) => pulse.check(),
103            None => Ok(()),
104        }
105    }
106
107    /// Flush the final batch and check cancellation before returning success.
108    /// Drop only flushes counts and cannot propagate cancellation.
109    #[track_caller]
110    pub fn finish(mut self) -> Result<(), StopReason> {
111        self.flush();
112        self.check()
113    }
114
115    /// Report every pending unit now.
116    #[inline]
117    #[track_caller]
118    pub fn flush(&mut self) {
119        if let Some(pulse) = self.pulse
120            && self.left != self.every
121        {
122            let pending = self.every - self.left;
123            self.left = self.every;
124            pulse.advance(pending);
125        }
126    }
127}
128
129/// The slow path of [`Paced::step`], kept out of the caller's loop. It takes
130/// the units by value, so no `Paced` escapes into it and the caller can keep
131/// one in registers.
132#[cold]
133#[inline(never)]
134#[track_caller]
135fn reach(pulse: &dyn Pulse, pending: u64) -> Result<(), StopReason> {
136    pulse.advance(pending);
137    pulse.check()
138}
139
140impl Drop for Paced<'_> {
141    #[inline]
142    fn drop(&mut self) {
143        self.flush();
144    }
145}
146
147impl<'a> dyn Pulse + 'a {
148    /// Checkpoints that reach this pulse once every `every` units; see
149    /// [`Paced`].
150    pub fn paced(&self, every: u64) -> Paced<'_> {
151        Paced::new(self, every)
152    }
153}
154
155impl crate::Child<'_> {
156    /// Checkpoints that reach this child's pulse once every `every` units; see
157    /// [`Paced`].
158    pub fn paced(&self, every: u64) -> Paced<'_> {
159        Paced::new(self.pulse(), every)
160    }
161}
162
163impl crate::SharedPulse {
164    /// Checkpoints that reach this shared pulse once every `every` units; see
165    /// [`Paced`]. One per worker, like any `Paced`.
166    pub fn paced(&self, every: u64) -> Paced<'_> {
167        Paced::new(self.as_pulse(), every)
168    }
169}