how_far/pulse.rs
1//! Phase planning and the combined cancellation/progress interface.
2
3use crate::Report;
4use alloc::{boxed::Box, sync::Arc, vec::Vec};
5use core::{fmt, num::NonZeroUsize};
6use enough::{Stop, StopReason};
7
8/// The denominator for completed work in one phase.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum Total {
12 /// A known count. Counting past it is recorded as an overrun.
13 Exact(u64),
14 /// A revisable estimate. Reaching it does not finish the phase.
15 Estimated(u64),
16 /// No useful denominator yet.
17 Unknown,
18}
19
20/// How a phase's work is scheduled. Declaring it schedules nothing.
21///
22/// On a leaf, it describes the leaf's own units: `WorkPool` means several
23/// workers share one count. On a branch, it describes how the children run.
24/// When a phase splits, the execution passed to [`Pulse::split`] replaces the
25/// one in its [`PhaseSpec`].
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Execution {
29 /// No scheduling relationship is declared.
30 #[default]
31 Unspecified,
32 /// One after another, in declared order.
33 Sequence,
34 /// Concurrently; all join before the parent finishes.
35 ForkJoin,
36 /// Shared by a pool of at most `max_parallelism` workers.
37 /// Build it with [`Execution::work_pool`].
38 #[non_exhaustive]
39 WorkPool {
40 /// The pool's concurrency ceiling, not a count of dedicated cores.
41 max_parallelism: NonZeroUsize,
42 },
43}
44
45impl Execution {
46 /// Work shared by a pool of at most `max_parallelism` workers, such as
47 /// `std::thread::available_parallelism()` or a Rayon pool's thread count.
48 pub const fn work_pool(max_parallelism: NonZeroUsize) -> Self {
49 Self::WorkPool { max_parallelism }
50 }
51}
52
53/// How a phase ended. Reaching a count total is not an outcome.
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum Outcome {
57 /// All of the phase's work completed.
58 Succeeded,
59 /// The planned work turned out to be unnecessary.
60 Skipped,
61 /// Work stopped cooperatively, after a cancellation request or timeout.
62 Cancelled,
63 /// Work failed for a reason other than a stop request.
64 Failed,
65 /// The phase's owner went away without publishing an outcome.
66 Abandoned,
67 /// Prevented from running by an earlier error; earns no completed share.
68 NotRun,
69}
70
71impl Outcome {
72 /// The outcome of an operation's result: `Succeeded` for `Ok`, `Cancelled`
73 /// for an error `is_stop` accepts, and `Failed` for any other error.
74 ///
75 /// ```
76 /// use how_far::{Outcome, StopReason};
77 ///
78 /// let stopped: Result<(), StopReason> = Err(StopReason::Cancelled);
79 /// assert_eq!(Outcome::from_result(&stopped, |_| true), Outcome::Cancelled);
80 /// ```
81 pub fn from_result<T, E>(result: &Result<T, E>, is_stop: impl FnOnce(&E) -> bool) -> Self {
82 match result {
83 Ok(_) => Self::Succeeded,
84 Err(error) if is_stop(error) => Self::Cancelled,
85 Err(_) => Self::Failed,
86 }
87 }
88}
89
90/// A phase plan that cannot be applied, or a lifecycle step out of order.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum PlanError {
94 /// A split needs at least one part, and every weight must be positive.
95 EmptyOrZeroWeight,
96 /// The weights' sum or the job's phase identifiers overflowed.
97 Overflow,
98 /// The phase already counted work or planned children. Plan once, first.
99 AlreadyInUse,
100 /// The phase has children; only a leaf has its own total.
101 NotALeaf,
102 /// This pulse cannot plan children.
103 Unsupported,
104 /// This borrowed implementation cannot provide an owned pulse.
105 NotShareable,
106 /// The phase already has an outcome.
107 Finished,
108 /// Every child must finish before its parent.
109 UnfinishedChildren,
110 /// Every declared stage has already run.
111 NoMoreStages,
112 /// A stage reservation does not match the next declaration in this sequence.
113 StageMismatch,
114}
115
116impl fmt::Display for PlanError {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(match self {
119 Self::EmptyOrZeroWeight => "a split needs at least one part with a positive weight",
120 Self::Overflow => "phase weights or identifiers overflowed",
121 Self::AlreadyInUse => "the phase already counted work or planned children",
122 Self::NotALeaf => "only a leaf phase has its own total",
123 Self::Unsupported => "this pulse cannot plan children",
124 Self::NotShareable => "this pulse cannot provide an owned view",
125 Self::Finished => "the phase already has an outcome",
126 Self::UnfinishedChildren => "every child must finish before its parent",
127 Self::NoMoreStages => "every declared stage has already run",
128 Self::StageMismatch => "stage reservation does not match the next declaration",
129 })
130 }
131}
132
133impl core::error::Error for PlanError {}
134
135/// One child in a split: a name, a weight relative to its siblings, a total,
136/// and optionally a unit name and an execution model.
137///
138/// Weights are relative, so `[35, 30, 35]` gives the middle child exactly 30%
139/// of its parent however many workers it later uses. Build it with
140/// [`PhaseSpec::new`]; the fields stay readable, and new planning options can
141/// be added without breaking existing code.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct PhaseSpec<'a> {
145 /// Human-readable phase name.
146 pub name: &'a str,
147 /// Positive weight relative to the siblings.
148 pub weight: u64,
149 /// Denominator for this phase's completed units.
150 pub total: Total,
151 /// Name of the counted unit, such as `rows` or `superblocks`.
152 pub units: &'a str,
153 /// How this phase's work is scheduled.
154 pub execution: Execution,
155}
156
157impl<'a> PhaseSpec<'a> {
158 /// Describe one weighted child that counts `items`.
159 pub const fn new(name: &'a str, weight: u64, total: Total) -> Self {
160 Self {
161 name,
162 weight,
163 total,
164 units: "items",
165 execution: Execution::Unspecified,
166 }
167 }
168
169 /// Name the counted unit.
170 pub const fn units(mut self, units: &'a str) -> Self {
171 self.units = units;
172 self
173 }
174
175 /// Declare how this phase's work is scheduled.
176 pub const fn execution(mut self, execution: Execution) -> Self {
177 self.execution = execution;
178 self
179 }
180
181 /// Check a split's parts the way every [`Pulse::split`] must, and return
182 /// the sum of their weights.
183 ///
184 /// Fails with [`PlanError::EmptyOrZeroWeight`] for no parts or a zero
185 /// weight, and [`PlanError::Overflow`] if the weights' sum overflows. Call
186 /// it first in a `split` implementation, so every pulse rejects the same
187 /// plans with the same errors.
188 pub fn validate_split(parts: &[PhaseSpec<'_>]) -> Result<u64, PlanError> {
189 // Plain loops keep this crate's compiled code small.
190 if parts.is_empty() {
191 return Err(PlanError::EmptyOrZeroWeight);
192 }
193 let mut sum = 0_u64;
194 for part in parts {
195 if part.weight == 0 {
196 return Err(PlanError::EmptyOrZeroWeight);
197 }
198 sum = sum.checked_add(part.weight).ok_or(PlanError::Overflow)?;
199 }
200 Ok(sum)
201 }
202}
203
204/// Cancellation, completed work, and nested phase planning, in one value.
205///
206/// A library accepts `&dyn Pulse` and uses it three ways:
207///
208/// - `check()` asks whether to stop. It is the cheapest call, unless the
209/// caller made it run code of its own, as a checkpoint
210/// callback does; check once per row, block or tile, not per byte.
211/// - `advance(n)` counts finished units; [`ProgressExt::step`](crate::ProgressExt::step)
212/// counts and then checks.
213/// - [`split`](Self::split) declares weighted children. The library owns the
214/// returned [`Child`] handles and finishes each one. It never finishes the
215/// pulse it was given; that pulse belongs to its caller.
216///
217/// Splitting and counting are exclusive: a phase either counts its own units
218/// (a leaf) or delegates to children (a branch). Plan before the first report.
219///
220/// Wrappers that add measurement or forwarding implement this trait, so it is
221/// open. Methods added in later compatible releases will have default bodies.
222///
223/// # Implementing
224///
225/// A tracker or checkpoint callback adapter from `how-far-along` usually
226/// suffices. Implementations must keep these rules:
227///
228/// - `check` and `advance` may run on many threads at once, and `advance` must
229/// not block. `may_stop` and `may_report` return `false` only when that can
230/// never change.
231/// - `split` starts with [`PhaseSpec::validate_split`]. It fails with
232/// [`PlanError::AlreadyInUse`] if this phase already counted work or split,
233/// and otherwise returns one [`Child::new`] per part, in order, each
234/// checking and counting like any pulse. Children from [`Child::inert`]
235/// cannot be stopped or observed, so they suit only parts nobody watches.
236/// - [`share`](Self::share) returns an owned view that checks the same stop,
237/// counts into the same phase and plans its children, or fails with
238/// [`PlanError::NotShareable`]; it never silently drops cancellation.
239/// - [`ChildPulse::finish`] fails with [`PlanError::UnfinishedChildren`] while
240/// a descendant is unfinished. Completed failed attempts may belong to a
241/// successful parent. A dropped unfinished child records `Abandoned`.
242pub trait Pulse: Stop + Report {
243 /// Record a reporting-protocol problem for optional diagnostics. This never
244 /// changes cancellation or the operation's return value. Ordinary sinks may
245 /// ignore it; diagnostic wrappers retain bounded evidence.
246 #[track_caller]
247 fn record_issue(&self, _error: PlanError) {}
248 /// Split this phase into weighted children, in declared order.
249 ///
250 /// Returns exactly one [`Child`] per part. The caller owns them: pass
251 /// `&child` to code that does the child's work, then finish the child once
252 /// that work has joined. Fails if the parts are invalid, if this phase
253 /// already counted work or split, or if the pulse cannot plan children.
254 fn split(
255 &self,
256 execution: Execution,
257 parts: &[PhaseSpec<'_>],
258 ) -> Result<Vec<Child<'_>>, PlanError>;
259
260 /// Mark this phase running, without counting or ending planning.
261 /// Implementations without lifecycle observations may ignore this.
262 fn start(&self) -> Result<(), PlanError> {
263 Ok(())
264 }
265
266 /// Revise a leaf's denominator without changing completed units or weights.
267 /// Unsupported implementations return an error instead of losing the update.
268 fn set_total(&self, _total: Total) -> Result<(), PlanError> {
269 Err(PlanError::Unsupported)
270 }
271
272 /// Share all pulse capabilities with owned work, but never completion rights.
273 ///
274 /// The [`SharedPulse`] is `'static`, cloneable and thread-safe, and works
275 /// wherever a `Stop` or `Report` is expected: give it to a codec context that
276 /// stores `impl Stop + 'static`, a spawned thread, or an async task. Join
277 /// such work before finishing the original owner. Unsupported ownership is
278 /// explicit; this method never silently replaces cancellation with a no-op.
279 fn share(&self) -> Result<SharedPulse, PlanError> {
280 Err(PlanError::NotShareable)
281 }
282}
283
284// A reference, box or `Arc` of a pulse is a pulse, as for `Stop` and `Report`,
285// so generic and owned code can hold any pulse. Checkpoints recognize only
286// `&NoPulse` itself, so `&&NoPulse` works but pays for its calls.
287
288/// Forward every `Pulse` method through a pointer-like wrapper.
289macro_rules! forward_pulse {
290 ($($wrapper:ty),*) => {$(
291 impl<P: Pulse + ?Sized> Pulse for $wrapper {
292 #[track_caller]
293 fn record_issue(&self, error: PlanError) {
294 (**self).record_issue(error);
295 }
296 #[inline]
297 fn split(
298 &self,
299 execution: Execution,
300 parts: &[PhaseSpec<'_>],
301 ) -> Result<Vec<Child<'_>>, PlanError> {
302 (**self).split(execution, parts)
303 }
304 fn start(&self) -> Result<(), PlanError> {
305 (**self).start()
306 }
307 fn set_total(&self, total: Total) -> Result<(), PlanError> {
308 (**self).set_total(total)
309 }
310 fn share(&self) -> Result<SharedPulse, PlanError> {
311 (**self).share()
312 }
313 }
314 )*};
315}
316forward_pulse!(&P, &mut P, Box<P>, Arc<P>);
317
318/// What a pulse's children add: publishing a terminal outcome.
319///
320/// Implement this for the type your [`Pulse::split`] returns, and wrap each
321/// child with [`Child::new`]. Library code never calls it directly; it calls
322/// [`Child::finish`], which consumes the handle so a child is finished at most
323/// once.
324pub trait ChildPulse: Pulse {
325 /// Resolve an untouched phase from its enclosing result. The default uses
326 /// ordinary completion; trackers can retain the inference as evidence.
327 fn complete_inferred(self: Box<Self>, outcome: Outcome) {
328 self.complete_as(outcome);
329 }
330 /// Complete best-effort observation. Trackers may infer untouched descendants
331 /// from this result; the default preserves older implementors' finish logic.
332 fn complete_as(self: Box<Self>, outcome: Outcome) {
333 let _ = self.finish(outcome);
334 }
335 /// Publish this child's terminal outcome.
336 fn finish(self: Box<Self>, outcome: Outcome) -> Result<(), PlanError>;
337}
338
339/// A planned child phase, owned by the code that called [`Pulse::split`].
340///
341/// A `Child` is a [`Pulse`]: pass `&child` wherever `&dyn Pulse` is expected,
342/// or call `check`, `step` and `split` on it directly. Finish it once, after
343/// its work has joined. A tracker records `Outcome::Abandoned` for a child that
344/// is dropped unfinished, including on unwinding. It is two words wide.
345/// Completion consumes the owner, so it cannot be repeated:
346///
347/// ```compile_fail,E0382
348/// use how_far::{Child, Outcome};
349/// let child = Child::inert();
350/// child.finish(Outcome::Succeeded).unwrap();
351/// child.finish(Outcome::Succeeded).unwrap();
352/// ```
353#[must_use = "finish the child after its work joins; dropping a tracked child abandons it"]
354pub struct Child<'a> {
355 /// `None` for a child of [`NoPulse`], which needs no allocation.
356 pulse: Option<Box<dyn ChildPulse + 'a>>,
357}
358
359impl<'a> Child<'a> {
360 /// Resolve an unused phase from its enclosing result, retaining inference evidence.
361 pub fn complete_inferred(self, outcome: Outcome) {
362 if let Some(pulse) = self.pulse {
363 pulse.complete_inferred(outcome);
364 }
365 }
366 /// A cancellation/checkpoint-preserving view without progress accounting.
367 pub(crate) fn untracked(parent: &'a dyn Pulse) -> Self {
368 Self::new(Untracked { inner: parent })
369 }
370
371 /// Plan nested observations without introducing a new work error.
372 #[track_caller]
373 pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
374 self.pulse().plan(execution, parts)
375 }
376 /// Wrap one child returned from a [`Pulse::split`] implementation.
377 pub fn new(pulse: impl ChildPulse + 'a) -> Self {
378 Self {
379 pulse: Some(Box::new(pulse)),
380 }
381 }
382
383 /// A child like those [`NoPulse`] hands out: it never stops, discards
384 /// reports, and finishing it does nothing. Its work cannot be stopped
385 /// through it and nobody sees its progress, so return it from
386 /// [`Pulse::split`] only for parts that nobody needs to observe or stop.
387 pub const fn inert() -> Self {
388 Self { pulse: None }
389 }
390
391 /// Publish this child's outcome. Its parent can finish only after every
392 /// child has finished.
393 ///
394 /// Fails without recording `outcome` if the child's own children are still
395 /// running; the consumed child is then dropped, so a tracker records it as
396 /// abandoned. Finished children's outcomes need not match: a successful
397 /// child may retain a failed attempt it recovered from.
398 pub fn finish(self, outcome: Outcome) -> Result<(), PlanError> {
399 match self.pulse {
400 Some(pulse) => pulse.finish(outcome),
401 None => Ok(()),
402 }
403 }
404
405 /// The child's own pulse, without the forwarding a `Child` adds when it is
406 /// itself used as a `&dyn Pulse`: the static [`NoPulse`] for its children.
407 #[inline]
408 pub(crate) fn pulse(&self) -> &(dyn Pulse + 'a) {
409 match &self.pulse {
410 Some(pulse) => &**pulse,
411 None => &NoPulse,
412 }
413 }
414}
415
416impl fmt::Debug for Child<'_> {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 f.debug_struct("Child").finish_non_exhaustive()
419 }
420}
421
422impl crate::Complete for Child<'_> {
423 fn complete_as(self, outcome: Outcome) {
424 if let Some(pulse) = self.pulse {
425 pulse.complete_as(outcome);
426 }
427 }
428}
429
430impl dyn Pulse + '_ {
431 /// Best-effort planning. If the sink rejects the plan, returns one untracked
432 /// child per part, all retaining this pulse's stop/checkpoint policy.
433 #[track_caller]
434 pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
435 match self.split(execution, parts) {
436 Ok(children) if children.len() == parts.len() => children,
437 Ok(_) => {
438 self.record_issue(PlanError::Unsupported);
439 untracked_children(self, parts.len())
440 }
441 Err(error) => {
442 self.record_issue(error);
443 untracked_children(self, parts.len())
444 }
445 }
446 }
447}
448
449/// `count` unobserved children that all check `parent`'s stop policy.
450pub(crate) fn untracked_children(parent: &dyn Pulse, count: usize) -> Vec<Child<'_>> {
451 let mut children = Vec::with_capacity(count);
452 for _ in 0..count {
453 children.push(Child::untracked(parent));
454 }
455 children
456}
457
458struct Untracked<P> {
459 inner: P,
460}
461impl<P: Pulse> Stop for Untracked<P> {
462 #[track_caller]
463 fn check(&self) -> Result<(), StopReason> {
464 self.inner.check()
465 }
466 fn may_stop(&self) -> bool {
467 self.inner.may_stop()
468 }
469}
470impl<P: Pulse> Report for Untracked<P> {
471 fn advance(&self, _: u64) {}
472 fn may_report(&self) -> bool {
473 false
474 }
475}
476impl<P: Pulse> Pulse for Untracked<P> {
477 fn split(&self, _: Execution, parts: &[PhaseSpec<'_>]) -> Result<Vec<Child<'_>>, PlanError> {
478 PhaseSpec::validate_split(parts)?;
479 Ok(untracked_children(&self.inner, parts.len()))
480 }
481 /// Nobody observes an untracked phase, as with `NoPulse`.
482 fn set_total(&self, _: Total) -> Result<(), PlanError> {
483 Ok(())
484 }
485 fn share(&self) -> Result<SharedPulse, PlanError> {
486 Ok(SharedPulse::new(Untracked {
487 inner: self.inner.share()?,
488 }))
489 }
490 #[track_caller]
491 fn record_issue(&self, error: PlanError) {
492 self.inner.record_issue(error);
493 }
494}
495impl<P: Pulse> ChildPulse for Untracked<P> {
496 fn finish(self: Box<Self>, _: Outcome) -> Result<(), PlanError> {
497 Ok(())
498 }
499}
500
501impl Stop for Child<'_> {
502 #[inline]
503 #[track_caller]
504 fn check(&self) -> Result<(), StopReason> {
505 match &self.pulse {
506 Some(pulse) => pulse.check(),
507 None => Ok(()),
508 }
509 }
510 #[inline]
511 fn may_stop(&self) -> bool {
512 matches!(&self.pulse, Some(pulse) if pulse.may_stop())
513 }
514}
515
516impl Report for Child<'_> {
517 #[inline]
518 #[track_caller]
519 fn advance(&self, completed: u64) {
520 if let Some(pulse) = &self.pulse {
521 pulse.advance(completed);
522 }
523 }
524 #[inline]
525 fn may_report(&self) -> bool {
526 matches!(&self.pulse, Some(pulse) if pulse.may_report())
527 }
528}
529
530impl Pulse for Child<'_> {
531 #[track_caller]
532 fn record_issue(&self, error: PlanError) {
533 self.pulse().record_issue(error);
534 }
535 fn split(
536 &self,
537 execution: Execution,
538 parts: &[PhaseSpec<'_>],
539 ) -> Result<Vec<Child<'_>>, PlanError> {
540 self.pulse().split(execution, parts)
541 }
542 fn start(&self) -> Result<(), PlanError> {
543 self.pulse().start()
544 }
545 fn set_total(&self, total: Total) -> Result<(), PlanError> {
546 self.pulse().set_total(total)
547 }
548 fn share(&self) -> Result<SharedPulse, PlanError> {
549 self.pulse().share()
550 }
551}
552
553/// The type of [`NoPulse`]. It has exactly one value, that static: nothing
554/// outside this crate can build, copy or move one.
555pub struct Inert {
556 /// One byte, so that the static has an address of its own.
557 _unique: u8,
558}
559
560/// Never stops and discards reports, including in every nested phase.
561///
562/// Pass `&NoPulse`. [`step`](crate::ProgressExt::step),
563/// [`live`](crate::ProgressExt::live) and [`Paced`](crate::Paced) recognize it
564/// by its address, so a loop that steps on every iteration runs with no
565/// checkpoint code when nobody listens: the one comparison moves out of the
566/// loop. Where it cannot, it is a comparison and a branch. A bare `check()` or
567/// `advance()` through a `&dyn Pulse` still makes one call, to a function that
568/// returns at once. A pulse that wraps it, such as `&&NoPulse` or a
569/// `Box<&NoPulse>`, works but is not recognized, and pays for both calls.
570///
571/// Its children and stages are free the same way and need no allocation. It
572/// still validates plans, so a library's planning mistakes surface even when
573/// nobody observes it.
574#[allow(non_upper_case_globals)]
575pub static NoPulse: Inert = Inert { _unique: 0 };
576
577impl fmt::Debug for Inert {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 f.write_str("NoPulse")
580 }
581}
582
583impl Stop for Inert {
584 #[inline(always)]
585 fn check(&self) -> Result<(), StopReason> {
586 Ok(())
587 }
588 #[inline(always)]
589 fn may_stop(&self) -> bool {
590 false
591 }
592}
593
594impl Report for Inert {
595 #[inline(always)]
596 fn advance(&self, _: u64) {}
597 #[inline(always)]
598 fn may_report(&self) -> bool {
599 false
600 }
601}
602
603impl Pulse for Inert {
604 fn split(&self, _: Execution, parts: &[PhaseSpec<'_>]) -> Result<Vec<Child<'_>>, PlanError> {
605 PhaseSpec::validate_split(parts)?;
606 let mut children = Vec::with_capacity(parts.len());
607 for _ in parts {
608 children.push(Child::inert());
609 }
610 Ok(children)
611 }
612 fn set_total(&self, _: Total) -> Result<(), PlanError> {
613 Ok(())
614 }
615 fn share(&self) -> Result<SharedPulse, PlanError> {
616 Ok(SharedPulse::default())
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::ProgressExt;
624 use alloc::format;
625
626 #[test]
627 fn no_pulse_children_are_the_static_and_need_no_allocation() {
628 let parts = [
629 PhaseSpec::new("a", 1, Total::Exact(1)),
630 PhaseSpec::new("b", 1, Total::Exact(1)),
631 ];
632 let children = NoPulse.split(Execution::Sequence, &parts).unwrap();
633 for child in children {
634 assert!(child.pulse.is_none(), "no boxed pulse");
635 assert!(core::ptr::addr_eq(child.pulse(), &NoPulse));
636 assert!(!child.may_stop() && !child.may_report());
637 assert!(child.live().is_none());
638 assert_eq!(child.step(5), Ok(()));
639 assert_eq!(
640 format!("{:?}", child.paced(1)),
641 "Paced { live: false, pending: 0, every: 1 }"
642 );
643 child.finish(Outcome::Succeeded).unwrap();
644 }
645 assert_eq!(
646 NoPulse.split(Execution::Sequence, &[]).err(),
647 Some(PlanError::EmptyOrZeroWeight)
648 );
649 }
650}
651
652/// An owned, cloneable pulse that retains planning and cancellation capabilities.
653/// The original phase owner alone may finish it. Requires pointer atomics.
654///
655/// A shared view has no completion operation:
656///
657/// ```compile_fail,E0599
658/// use how_far::{Outcome, SharedPulse};
659/// let shared = SharedPulse::default();
660/// shared.finish(Outcome::Succeeded).unwrap();
661/// ```
662#[derive(Clone, Default)]
663pub struct SharedPulse(Option<Arc<dyn Pulse>>);
664impl SharedPulse {
665 /// Erase an owned implementation. The implementation must not own the
666 /// phase's completion token: dropping clones must not finish the phase.
667 pub fn new(pulse: impl Pulse + 'static) -> Self {
668 Self(Some(Arc::new(pulse)))
669 }
670 /// Borrow the underlying pulse, preserving the canonical no-op fast path.
671 pub fn as_pulse(&self) -> &dyn Pulse {
672 match &self.0 {
673 Some(p) => &**p,
674 None => &NoPulse,
675 }
676 }
677
678 /// Best-effort planning, as for a borrowed pulse; see `<dyn Pulse>::plan`.
679 #[track_caller]
680 pub fn plan(&self, execution: Execution, parts: &[PhaseSpec<'_>]) -> Vec<Child<'_>> {
681 self.as_pulse().plan(execution, parts)
682 }
683}
684impl fmt::Debug for SharedPulse {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 f.debug_struct("SharedPulse")
687 .field("live", &self.0.is_some())
688 .finish()
689 }
690}
691impl Stop for SharedPulse {
692 #[track_caller]
693 fn check(&self) -> Result<(), StopReason> {
694 self.as_pulse().check()
695 }
696 fn may_stop(&self) -> bool {
697 self.as_pulse().may_stop()
698 }
699}
700impl Report for SharedPulse {
701 #[track_caller]
702 fn advance(&self, n: u64) {
703 self.as_pulse().advance(n)
704 }
705 fn may_report(&self) -> bool {
706 self.as_pulse().may_report()
707 }
708}
709impl Pulse for SharedPulse {
710 #[track_caller]
711 fn record_issue(&self, error: PlanError) {
712 self.as_pulse().record_issue(error);
713 }
714 fn split(
715 &self,
716 execution: Execution,
717 parts: &[PhaseSpec<'_>],
718 ) -> Result<Vec<Child<'_>>, PlanError> {
719 self.as_pulse().split(execution, parts)
720 }
721 fn start(&self) -> Result<(), PlanError> {
722 self.as_pulse().start()
723 }
724 fn set_total(&self, total: Total) -> Result<(), PlanError> {
725 self.as_pulse().set_total(total)
726 }
727 fn share(&self) -> Result<SharedPulse, PlanError> {
728 Ok(self.clone())
729 }
730}