steppe/
lib.rs

1//! ## Steppe
2//! This crate is used to track the progress of a task through multiple steps composed of multiple states.
3//! 
4//! The objectives are:
5//! - Have a very simple API to describe the steps composing a task. (create the steps and update the progress)
6//! - Provide an easy way to display the current progress while the process is running.
7//! - Provide a way to get the accumulated durations of each steps to quickly see the bottleneck.
8//! - Don't slow down the main process too much.
9//! 
10//! The crate is composed of only 2 parts:
11//! - The [`Progress`] struct that is used to track the progress of the task.
12//! - The [`Step`] trait that is used to describe the steps composing a task.
13//! 
14//! The [`Progress`] struct is thread-safe, can be cloned cheaply and shared everywhere. While a thread is updating it another can display what we're doing.
15//! The [`Step`] trait is used to describe the steps composing a task.
16//! 
17//! The API of the [`Progress`] is made of three parts:
18//! - Add something to the stack of steps being processed with the [`Progress::update`] method. It accepts any type that implements the [`Step`] trait.
19//! - Get the current progress view with the [`Progress::as_progress_view`] method.
20//! - Get the accumulated durations of each steps with the [`Progress::accumulated_durations`] method.
21//!
22//! Since creating [`Step`]s is a bit tedious, you can use the following helpers:
23//! - [`make_enum_progress`] macro.
24//! - [`make_atomic_progress`] macro.
25//! - Or implement the [`NamedStep`] trait.
26//! 
27//! ```rust
28//! use std::sync::atomic::Ordering;
29//! use steppe::{make_enum_progress, make_atomic_progress, Progress, Step, NamedStep, AtomicSubStep};
30//! 
31//! // This will create a new enum that implements the `Step` trait automatically. Take care it's very case sensitive.
32//! make_enum_progress! {
33//!     pub enum TamosDay {
34//!         PetTheDog,
35//!         WalkTheDog,
36//!         TypeALotOnTheKeyboard,
37//!         WalkTheDogAgain,
38//!     }
39//! }
40//! 
41//! // This create a new struct that implement the `Step` trait automatically.
42//! // It's displayed as "key strokes" and we cannot change its name.
43//! make_atomic_progress!(KeyStrokes alias AtomicKeyStrokesStep => "key strokes");
44//! 
45//! let mut progress = Progress::default();
46//! progress.update(TamosDay::PetTheDog); // We're at 0/4 and 0% of completion
47//! progress.update(TamosDay::WalkTheDog); // We're at 1/4 and 25% of completion
48//! 
49//! progress.update(TamosDay::TypeALotOnTheKeyboard); // We're at 2/4 and 50% of completion
50//! let (atomic, key_strokes) = AtomicKeyStrokesStep::new(1000);
51//! progress.update(key_strokes);
52//! // Here we enqueued a new step that have 1000 total states. Since we don't want to take a lock everytime
53//! // we type on the keyboard we're instead going to increase an atomic without taking the mutex.
54//!
55//! atomic.fetch_add(500, Ordering::Relaxed);
56//! // If we fetch the progress at this point it should be exactly between 50% and 75%.
57//! 
58//! progress.update(TamosDay::WalkTheDogAgain); // We're at 3/4 and 75% of completion
59//! // By enqueuing this new step the progress is going to drop everything that was pushed after the `TamosDay` type was pushed.
60//! ```
61
62use std::any::TypeId;
63use std::borrow::Cow;
64use std::marker::PhantomData;
65use std::sync::atomic::{AtomicU64, Ordering};
66use std::sync::{Arc, RwLock};
67use std::time::{Duration, Instant};
68
69use indexmap::IndexMap;
70use serde::Serialize;
71
72/// The main trait of the crate. That describes an unit of works.
73/// - It contains a name that can change over time.
74/// - A total number of state the step must go through.
75/// - The current state of the step.
76/// 
77/// The `current` should never exceed the `total`.
78pub trait Step: 'static + Send + Sync {
79    fn name(&self) -> Cow<'static, str>;
80    fn current(&self) -> u64;
81    fn total(&self) -> u64;
82}
83
84/// The main struct of the crate.
85/// It stores the current steps we're processing.
86/// It also contains the durations of each steps.
87/// 
88/// The structure is thread-safe, can be cloned cheaply and shared everywhere.
89/// But keep in mind that when you update the step you're at we must take a mutex.
90/// If you need to quickly update a tons of values you may want to use atomic numbers
91/// that you can update without taking the mutex.
92#[derive(Clone, Default)]
93pub struct Progress {
94    steps: Arc<RwLock<InnerProgress>>,
95}
96
97#[derive(Default)]
98struct InnerProgress {
99    /// The hierarchy of steps.
100    steps: Vec<(TypeId, Box<dyn Step>, Instant)>,
101    /// The durations associated to each steps.
102    durations: Vec<(String, Duration)>,
103}
104
105impl Progress {
106    /// Update the progress of the current step.
107    /// 
108    /// If the step is not found, it will be added.
109    /// If the step is found, it will be updated.
110    /// 
111    /// If the step is found and the current is higher than the total, it will be ignored.
112    pub fn update<P: Step>(&self, sub_progress: P) {
113        let mut inner = self.steps.write().unwrap();
114        let InnerProgress { steps, durations } = &mut *inner;
115
116        let now = Instant::now();
117        let step_type = TypeId::of::<P>();
118        if let Some(idx) = steps.iter().position(|(id, _, _)| *id == step_type) {
119            push_steps_durations(steps, durations, now, idx);
120            steps.truncate(idx);
121        }
122
123        steps.push((step_type, Box::new(sub_progress), now));
124    }
125
126    /// Drop all the steps and update the durations.
127    /// 
128    /// This is not mandatory. But if you don't do it and take a lot of time before calling [`Progress::accumulated_durations`] the last step will appear as taking more time than it actually did.
129    /// Directly calling [`Progress::accumulated_durations`] instead of `finish` will give the same result.
130    pub fn finish(&self) {
131        let mut inner = self.steps.write().unwrap();
132        let InnerProgress { steps, durations } = &mut *inner;
133
134        let now = Instant::now();
135        push_steps_durations(steps, durations, now, 0);
136        steps.clear();
137    }
138
139    /// Get the current progress view.
140    /// 
141    /// This is useful to display the progress to the user.
142    /// 
143    /// The view shows a list of steps with their current state, the total number of states for each step and the total percentage of completion at the end:
144    /// ```json5
145    /// {
146    ///     "steps": [
147    ///         {
148    ///             "currentStep": "step1", // The name of the step
149    ///             "finished": 50, // The number of states that have been completed
150    ///             "total": 100 // The total number of states for the step
151    ///         },
152    ///         {
153    ///             "currentStep": "step2",
154    ///             "finished": 0,
155    ///             "total": 100
156    ///         }
157    ///     ],
158    ///     "percentage": 50.0
159    /// }
160    /// ```
161    pub fn as_progress_view(&self) -> ProgressView {
162        let inner = self.steps.read().unwrap();
163        let InnerProgress { steps, .. } = &*inner;
164
165        let mut percentage = 0.0;
166        let mut prev_factors = 1.0;
167
168        let mut step_view = Vec::with_capacity(steps.len());
169        for (_, step, _) in steps.iter() {
170            let total = step.total();
171            prev_factors *= total as f32;
172            percentage += step.current().min(total) as f32 / prev_factors;
173
174            step_view.push(ProgressStepView {
175                current_step: step.name(),
176                finished: step.current(),
177                total: step.total(),
178            });
179        }
180
181        ProgressView {
182            steps: step_view,
183            percentage: percentage * 100.0,
184        }
185    }
186
187    /// Get the accumulated durations of each steps.
188    /// 
189    /// This is useful to see the bottleneck of the process.
190    /// 
191    /// Returns an ordered map of the step name to the duration:
192    /// ```json5
193    /// {
194    ///     "step1 > step2": "1.23s", // The duration of the step2 within the step1
195    ///     "step1": "1.43s", // The total duration of the step1. Here we see that most of the time was spent in step1.
196    /// }
197    pub fn accumulated_durations(&self) -> IndexMap<String, String> {
198        let mut inner = self.steps.write().unwrap();
199        let InnerProgress {
200            steps, durations, ..
201        } = &mut *inner;
202
203        let now = Instant::now();
204        push_steps_durations(steps, durations, now, 0);
205
206        durations
207            .drain(..)
208            .map(|(name, duration)| (name, format!("{duration:.2?}")))
209            .collect()
210    }
211}
212
213/// Generate the names associated with the durations and push them.
214fn push_steps_durations(
215    steps: &[(TypeId, Box<dyn Step>, Instant)],
216    durations: &mut Vec<(String, Duration)>,
217    now: Instant,
218    idx: usize,
219) {
220    for (i, (_, _, started_at)) in steps.iter().skip(idx).enumerate().rev() {
221        let full_name = steps
222            .iter()
223            .take(idx + i + 1)
224            .map(|(_, s, _)| s.name())
225            .collect::<Vec<_>>()
226            .join(" > ");
227        durations.push((full_name, now.duration_since(*started_at)));
228    }
229}
230
231/// This trait lets you use the AtomicSubStep defined right below.
232/// The name must be a const that never changed but that can't be enforced by the type system because it make the trait non object-safe.
233/// By forcing the Default trait + the &'static str we make it harder to miss-use the trait.
234pub trait NamedStep: 'static + Send + Sync + Default {
235    fn name(&self) -> &'static str;
236}
237
238/// Structure to quickly define steps that need very quick, lockless updating of their current step.
239/// You can use this struct if:
240/// - The name of the step doesn't change
241/// - The total number of steps doesn't change
242pub struct AtomicSubStep<Name: NamedStep> {
243    unit_name: Name,
244    current: Arc<AtomicU64>,
245    total: u64,
246}
247
248impl<Name: NamedStep> AtomicSubStep<Name> {
249    pub fn new(total: u64) -> (Arc<AtomicU64>, Self) {
250        let current = Arc::new(AtomicU64::new(0));
251        (
252            current.clone(),
253            Self {
254                current,
255                total,
256                unit_name: Name::default(),
257            },
258        )
259    }
260}
261
262impl<Name: NamedStep> Step for AtomicSubStep<Name> {
263    fn name(&self) -> Cow<'static, str> {
264        self.unit_name.name().into()
265    }
266
267    fn current(&self) -> u64 {
268        self.current.load(Ordering::Relaxed)
269    }
270
271    fn total(&self) -> u64 {
272        self.total
273    }
274}
275
276#[doc(hidden)]
277pub use convert_case as _private_convert_case;
278
279/// Helper to create a new enum that implements the `Step` trait.
280/// It's useful when we're just going to move from one state to another.
281///
282/// ```rust
283/// steppe::make_enum_progress! {
284///     pub enum CustomMainSteps {
285///         TheFirstStep,
286///         TheSecondWeNeverSee,
287///         TheThirdStep,
288///         TheFinalStep,
289///     }
290/// }
291/// ```
292/// Warning: Even though the syntax looks like a rust enum, it's very case sensitive.
293///     All the variants unit, named in CamelCase, and finished by a comma.
294#[macro_export]
295macro_rules! make_enum_progress {
296    ($visibility:vis enum $name:ident { $($variant:ident,)+ }) => {
297        #[repr(u8)]
298        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
299        #[allow(clippy::enum_variant_names)]
300        $visibility enum $name {
301            $($variant),+
302        }
303
304        impl $crate::Step for $name {
305            fn name(&self) -> std::borrow::Cow<'static, str> {
306                use $crate::_private_convert_case::Casing;
307
308                match self {
309                    $(
310                        $name::$variant => stringify!($variant).from_case($crate::_private_convert_case::Case::Camel).to_case($crate::_private_convert_case::Case::Lower).into()
311                    ),+
312                }
313            }
314
315            fn current(&self) -> u64 {
316                *self as u64
317            }
318
319            fn total(&self) -> u64 {
320                use $crate::_internal_count;
321                $crate::_internal_count!($($variant)+) as u64
322            }
323        }
324    };
325}
326
327
328#[doc(hidden)]
329#[macro_export]
330macro_rules! _internal_count {
331    () => (0u64);
332    ( $x:ident ) => (1u64);
333    ( $x:ident $($xs:ident)* ) => (1u64 + $crate::_internal_count!($($xs)*));
334}
335
336/// This macro is used to create a new atomic progress step quickly.
337/// ```rust
338/// steppe::make_atomic_progress!(Document alias AtomicDocumentStep => "document");
339/// ```
340///
341/// This will create a new struct `Document` that implements the `NamedStep` trait and a new type `AtomicDocumentStep` that implements the `Step` trait.
342///
343/// The `AtomicDocumentStep` type can be used to create a new atomic progress step.
344#[macro_export]
345macro_rules! make_atomic_progress {
346    ($struct_name:ident alias $atomic_struct_name:ident => $step_name:literal) => {
347        #[derive(Default, Debug, Clone, Copy)]
348        pub struct $struct_name {}
349        impl $crate::NamedStep for $struct_name {
350            fn name(&self) -> &'static str {
351                $step_name
352            }
353        }
354        pub type $atomic_struct_name = $crate::AtomicSubStep<$struct_name>;
355    };
356}
357
358/// The returned view of the progress.
359#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
360#[derive(Debug, Serialize, Clone)]
361#[serde(rename_all = "camelCase")]
362pub struct ProgressView {
363    pub steps: Vec<ProgressStepView>,
364    pub percentage: f32,
365}
366
367/// The view of the individual steps.
368#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
369#[derive(Debug, Serialize, Clone)]
370#[serde(rename_all = "camelCase")]
371pub struct ProgressStepView {
372    pub current_step: Cow<'static, str>,
373    pub finished: u64,
374    pub total: u64,
375}
376
377/// Used when the name can change but it's still the same step.
378/// To avoid conflicts on the `TypeId`, create a unique type every time you use this step:
379/// ```text
380/// enum UpgradeVersion {}
381///
382/// progress.update(VariableNameStep::<UpgradeVersion>::new(
383///     "v1 to v2",
384///     0,
385///     10,
386/// ));
387/// ```
388pub struct VariableNameStep<U: Send + Sync + 'static> {
389    name: String,
390    current: u64,
391    total: u64,
392    phantom: PhantomData<U>,
393}
394
395impl<U: Send + Sync + 'static> VariableNameStep<U> {
396    pub fn new(name: impl Into<String>, current: u64, total: u64) -> Self {
397        Self {
398            name: name.into(),
399            current,
400            total,
401            phantom: PhantomData,
402        }
403    }
404}
405
406impl<U: Send + Sync + 'static> Step for VariableNameStep<U> {
407    fn name(&self) -> Cow<'static, str> {
408        self.name.clone().into()
409    }
410
411    fn current(&self) -> u64 {
412        self.current
413    }
414
415    fn total(&self) -> u64 {
416        self.total
417    }
418}