atomic_progress/stack.rs
1//! A thread-safe collection for managing multiple progress indicators.
2//!
3//! The [`ProgressStack`] serves as the central registry for "Multi-Bar" applications.
4//! It allows a renderer to iterate over a dynamic list of active tasks without fighting
5//! the worker threads for locks.
6//!
7//! # Synchronization Strategy
8//!
9//! The stack uses a coarse-grained [`RwLock`](parking_lot::RwLock) to protect the *list*
10//! of handles. Individual progress updates (incrementing counters) do **not** lock the stack.
11//!
12//! * **Workers:** Only acquire the stack lock when adding/removing a bar (rare).
13//! * **Renderers:** Acquire a read lock on the stack once per frame to clone the handles,
14//! then iterate independently.
15
16use std::{fmt, sync::Arc};
17
18use compact_str::CompactString;
19use parking_lot::RwLock;
20
21use crate::{Progress, ProgressSnapshot};
22
23/// A thread-safe, shared-clonable collection of [`Progress`] instances.
24///
25/// `ProgressStack` is designed to manage a dynamic list of progress indicators
26/// (bars, spinners, etc.) that typically render together (e.g., a multi-bar CLI).
27///
28/// # Concurrency
29///
30/// This struct uses internal synchronization (via [`Arc`] and [`RwLock`]), making it
31/// safe to clone and share across threads. Cloning is cheap (pointer copy), and
32/// mutations on one clone are visible to all others.
33#[derive(Clone, Default)]
34pub struct ProgressStack {
35 /// The shared list of progress items.
36 ///
37 /// We use `parking_lot::RwLock` for efficient, fair locking.
38 inner: Arc<RwLock<Vec<Progress>>>,
39}
40
41impl fmt::Debug for ProgressStack {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 // We only print metadata to avoid locking the inner items during debug formatting
44 f.debug_struct("ProgressStack")
45 .field("count", &self.len())
46 .finish()
47 }
48}
49
50impl ProgressStack {
51 /// Creates a new, empty `ProgressStack`.
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// use atomic_progress::ProgressStack;
57 ///
58 /// let stack = ProgressStack::new();
59 /// assert!(stack.is_empty());
60 /// ```
61 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 /// Adds a [`Progress`] instance to the stack.
67 ///
68 /// The progress item is appended to the end of the collection.
69 ///
70 /// # Parameters
71 ///
72 /// * `progress`: The progress indicator to add.
73 pub fn push(&self, progress: Progress) {
74 self.inner.write().push(progress);
75 }
76
77 /// Creates a new progress bar, adds it to the stack, and returns the handle.
78 ///
79 /// This is a shorthand for creating a [`Progress`] via [`Progress::new_pb`]
80 /// and calling [`push`](Self::push).
81 ///
82 /// # Parameters
83 ///
84 /// * `name`: The display name for the bar.
85 /// * `total`: The total count for the bar.
86 #[must_use]
87 pub fn add_pb(&self, name: impl Into<CompactString>, total: impl Into<u64>) -> Progress {
88 let progress = Progress::new_pb(name, total);
89 self.push(progress.clone());
90 progress
91 }
92
93 /// Creates a new spinner, adds it to the stack, and returns the handle.
94 ///
95 /// This is a shorthand for creating a [`Progress`] via [`Progress::new_spinner`]
96 /// and calling [`push`](Self::push).
97 ///
98 /// # Parameters
99 ///
100 /// * `name`: The display name for the spinner.
101 #[must_use]
102 pub fn add_spinner(&self, name: impl Into<CompactString>) -> Progress {
103 let progress = Progress::new_spinner(name);
104 self.push(progress.clone());
105 progress
106 }
107
108 /// Returns a snapshot of the state of all tracked progress instances.
109 ///
110 /// This aggregates the current state of every progress bar in the stack.
111 /// It is typically used by renderers to draw the current UI frame.
112 ///
113 /// # Performance
114 ///
115 /// This acquires a read lock on the stack collection, and subsequently
116 /// acquires read locks on each individual `Progress` state to copy their data.
117 #[must_use]
118 pub fn snapshot(&self) -> ProgressStackSnapshot {
119 // 1. Quick lock just to clone the Arcs
120 let items: Vec<Progress> = self.inner.read().clone();
121
122 // 2. Do the heavy lifting (locking individual bars) without holding the stack lock
123 let snapshots = items.iter().map(Progress::snapshot).collect();
124
125 ProgressStackSnapshot(snapshots)
126 }
127
128 /// Returns a list of handles to all [`Progress`] instances in the stack.
129 ///
130 /// Since [`Progress`] is a cheap-to-clone handle (`Arc`), this returns
131 /// a new vector containing clones of the handles. This allows you to retain
132 /// access to the bars even if the stack is cleared or modified later.
133 #[must_use]
134 pub fn items(&self) -> Vec<Progress> {
135 self.inner.read().clone()
136 }
137
138 /// Checks if all progress instances in the stack are marked as finished.
139 ///
140 /// Returns `true` if the stack is empty or if `is_finished()` returns true
141 /// for every item.
142 #[must_use]
143 pub fn is_all_finished(&self) -> bool {
144 self.inner.read().iter().all(Progress::is_finished)
145 }
146
147 /// Removes all progress instances from the stack.
148 ///
149 /// This clears the internal collection.
150 pub fn clear(&self) {
151 self.inner.write().clear();
152 }
153
154 /// Returns the number of progress instances currently in the stack.
155 #[must_use]
156 pub fn len(&self) -> usize {
157 self.inner.read().len()
158 }
159
160 /// Returns `true` if the stack contains no progress instances.
161 #[must_use]
162 pub fn is_empty(&self) -> bool {
163 self.inner.read().is_empty()
164 }
165}
166
167/// A snapshot of the state of the entire progress stack at a specific point in time.
168///
169/// This wrapper allows for future extension of aggregate calculations (e.g. global ETA,
170/// total throughput) without changing the return signature.
171#[derive(Clone, Debug, Default, Eq, PartialEq)]
172#[cfg_attr(
173 feature = "rkyv",
174 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
175)]
176#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
177#[cfg_attr(feature = "rkyv", rkyv(derive(Debug, Eq, PartialEq)))]
178pub struct ProgressStackSnapshot(pub Vec<ProgressSnapshot>);
179
180#[cfg(test)]
181mod tests {
182 use super::ProgressStack;
183
184 /// Stack Management
185 /// Verifies adding items and checking aggregate state.
186 #[test]
187 fn test_stack_operations() {
188 let stack = ProgressStack::new();
189 assert!(stack.is_empty());
190
191 let _pb = stack.add_pb("bar", 100u64);
192 let _sp = stack.add_spinner("spin");
193
194 assert_eq!(stack.len(), 2);
195 assert!(!stack.is_all_finished());
196
197 let items = stack.items();
198 items[0].finish();
199 items[1].finish();
200
201 assert!(stack.is_all_finished());
202 }
203
204 /// Snapshot Isolation
205 /// Verifies that a snapshot is an owned copy of data at that instant.
206 #[test]
207 fn test_snapshot_isolation() {
208 let stack = ProgressStack::new();
209 let pb = stack.add_pb("test", 100u64);
210
211 pb.inc(10u64);
212 let snap_1 = stack.snapshot();
213
214 pb.inc(20u64);
215 let snap_2 = stack.snapshot();
216
217 assert_eq!(
218 snap_1.0[0].position(),
219 10,
220 "Old snapshot should remain immutable"
221 );
222 assert_eq!(snap_2.0[0].position(), 30, "New snapshot reflects updates");
223 }
224}