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}