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}