tracing_indicatif/
pb_manager.rs

1use std::collections::VecDeque;
2use std::sync::atomic::AtomicUsize;
3use std::sync::Arc;
4use std::time::Duration;
5
6use indicatif::style::ProgressStyle;
7use indicatif::MultiProgress;
8use indicatif::ProgressBar;
9use indicatif::ProgressDrawTarget;
10use indicatif::ProgressState;
11use tracing_core::span;
12use tracing_core::Subscriber;
13use tracing_subscriber::layer;
14use tracing_subscriber::registry::LookupSpan;
15
16use crate::IndicatifSpanContext;
17
18#[derive(Clone)]
19struct RequireDefault;
20
21/// Controls how often progress bars are recalculated and redrawn to the terminal.
22///
23/// This struct must be constructed as
24/// ```
25/// # use tracing_indicatif::TickSettings;
26/// TickSettings {
27///     term_draw_hz: 20,
28///     default_tick_interval: None,
29///     footer_tick_interval: None,
30///     ..Default::default()
31/// }
32/// # ;
33/// ```
34/// as to ensure forward compatibility.
35#[derive(Clone)]
36pub struct TickSettings {
37    /// The rate at which to draw to the terminal.
38    ///
39    /// A value of 20 here means indicatif will redraw the terminal state 20 times a second (i.e.
40    /// once every 50ms).
41    pub term_draw_hz: u8,
42    /// The default interval to pass to `enable_steady_tick` for a new progress bar. This controls
43    /// how often the progress bar state is recalculated. Defaults to
44    /// `Some(Duration::from_millis(100))`.
45    ///
46    /// Note, this does not control how often the progress bar is actually redrawn, that is
47    /// controlled by [`Self::term_draw_hz`].
48    ///
49    /// Using `None` here will disable steady ticks for your progress bars.
50    pub default_tick_interval: Option<Duration>,
51    /// The interval to pass to `enable_steady_tick` for the footer progress bar. This controls
52    /// how often the footer progress bar state is recalculated. Defaults to `None`.
53    ///
54    /// Note, this does not control how often the footer progress bar is actually redrawn, that is
55    /// controlled by [`Self::term_draw_hz`].
56    ///
57    /// Using `None` here will disable steady ticks for the footer progress bar. Unless you have a
58    /// spinner in your footer, you should set this to `None` as we manually redraw the footer
59    /// whenever something changes.
60    pub footer_tick_interval: Option<Duration>,
61    // Exists solely to require `..Default::default()` at the end of constructing this struct.
62    #[doc(hidden)]
63    #[allow(private_interfaces)]
64    pub require_default: RequireDefault,
65}
66
67impl Default for TickSettings {
68    fn default() -> Self {
69        Self {
70            term_draw_hz: 20,
71            default_tick_interval: Some(Duration::from_millis(100)),
72            footer_tick_interval: None,
73            require_default: RequireDefault,
74        }
75    }
76}
77
78pub(crate) struct ProgressBarManager {
79    pub(crate) mp: MultiProgress,
80    active_progress_bars: u64,
81    max_progress_bars: u64,
82    // This is used in the footer progress bar and tracks the actual number of pending progress
83    // bars.
84    pending_progress_bars: Arc<AtomicUsize>,
85    // The `.len()` of this may differ from `pending_progress_bars`. If a span closes before its
86    // progress bar is ever un-hidden, we decrement `pending_progress_bars` but won't clean the
87    // span entry up from this `VecDeque` for performance reasons. Instead, whenever we do un-hide
88    // a progress bar, we'll "garbage collect" closed spans from this then.
89    pending_spans: VecDeque<span::Id>,
90    // If this is `None`, a footer will never be shown.
91    footer_pb: Option<ProgressBar>,
92    tick_settings: TickSettings,
93}
94
95impl ProgressBarManager {
96    pub(crate) fn new(
97        max_progress_bars: u64,
98        footer_progress_style: Option<ProgressStyle>,
99        tick_settings: TickSettings,
100    ) -> Self {
101        let mut s = Self {
102            mp: {
103                let mp = MultiProgress::new();
104                mp.set_draw_target(ProgressDrawTarget::stderr_with_hz(
105                    tick_settings.term_draw_hz,
106                ));
107
108                mp
109            },
110            active_progress_bars: 0,
111            max_progress_bars: 0,
112            pending_progress_bars: Arc::new(AtomicUsize::new(0)),
113            pending_spans: VecDeque::new(),
114            footer_pb: None,
115            tick_settings,
116        };
117
118        s.set_max_progress_bars(max_progress_bars, footer_progress_style);
119
120        s
121    }
122
123    pub(crate) fn set_max_progress_bars(
124        &mut self,
125        max_progress_bars: u64,
126        footer_style: Option<ProgressStyle>,
127    ) {
128        self.max_progress_bars = max_progress_bars;
129
130        let pending_progress_bars = self.pending_progress_bars.clone();
131        self.footer_pb = footer_style.map(move |style| {
132            ProgressBar::hidden().with_style(style.with_key(
133                "pending_progress_bars",
134                move |_: &ProgressState, writer: &mut dyn std::fmt::Write| {
135                    let _ = write!(
136                        writer,
137                        "{}",
138                        pending_progress_bars.load(std::sync::atomic::Ordering::Acquire)
139                    );
140                },
141            ))
142        });
143    }
144
145    pub(crate) fn set_tick_settings(&mut self, tick_settings: TickSettings) {
146        self.mp.set_draw_target(ProgressDrawTarget::stderr_with_hz(
147            tick_settings.term_draw_hz,
148        ));
149        self.tick_settings = tick_settings;
150    }
151
152    fn decrement_pending_pb(&mut self) {
153        let prev_val = self
154            .pending_progress_bars
155            .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
156
157        if let Some(footer_pb) = self.footer_pb.as_ref() {
158            // If this span was the last one pending, clear the footer (if it was active).
159            if prev_val == 1 {
160                debug_assert!(
161                    !footer_pb.is_hidden(),
162                    "footer progress bar was hidden despite there being pending progress bars"
163                );
164
165                if self.tick_settings.footer_tick_interval.is_some() {
166                    footer_pb.disable_steady_tick();
167                }
168
169                // Appears to have broken with
170                // https://github.com/console-rs/indicatif/pull/648
171                // self.mp.set_move_cursor(false);
172                footer_pb.finish_and_clear();
173                self.mp.remove(footer_pb);
174            } else {
175                footer_pb.tick();
176            }
177        }
178    }
179
180    fn add_pending_pb(&mut self, span_id: &span::Id) {
181        let prev_val = self
182            .pending_progress_bars
183            .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
184        self.pending_spans.push_back(span_id.clone());
185
186        // Show the footer progress bar.
187        if let Some(footer_pb) = self.footer_pb.as_ref() {
188            if prev_val == 0 {
189                debug_assert!(
190                    footer_pb.is_hidden(),
191                    "footer progress bar was not hidden despite there being no pending progress bars"
192                );
193
194                footer_pb.reset();
195
196                if let Some(tick_interval) = self.tick_settings.footer_tick_interval {
197                    footer_pb.enable_steady_tick(tick_interval);
198                }
199
200                self.mp.add(footer_pb.clone());
201                // Appears to have broken with
202                // https://github.com/console-rs/indicatif/pull/648
203                // self.mp.set_move_cursor(true);
204            }
205
206            footer_pb.tick();
207        }
208    }
209
210    pub(crate) fn show_progress_bar(
211        &mut self,
212        pb_span_ctx: &mut IndicatifSpanContext,
213        span_id: &span::Id,
214    ) {
215        if self.active_progress_bars < self.max_progress_bars {
216            let pb = match pb_span_ctx.parent_progress_bar {
217                // TODO(emersonford): fix span ordering in progress bar, because we use
218                // `insert_after`, we end up showing the child progress bars in reverse order.
219                Some(ref parent_pb) => self
220                    .mp
221                    .insert_after(parent_pb, pb_span_ctx.progress_bar.take().unwrap()),
222                None => {
223                    if self
224                        .footer_pb
225                        .as_ref()
226                        .map(|footer_pb| !footer_pb.is_hidden())
227                        .unwrap_or(false)
228                    {
229                        self.mp
230                            .insert_from_back(1, pb_span_ctx.progress_bar.take().unwrap())
231                    } else {
232                        self.mp.add(pb_span_ctx.progress_bar.take().unwrap())
233                    }
234                }
235            };
236
237            self.active_progress_bars += 1;
238
239            if let Some(tick_interval) = self.tick_settings.default_tick_interval {
240                pb.enable_steady_tick(tick_interval);
241            }
242
243            pb.tick();
244
245            pb_span_ctx.progress_bar = Some(pb);
246        } else {
247            self.add_pending_pb(span_id);
248        }
249    }
250
251    pub(crate) fn finish_progress_bar<S>(
252        &mut self,
253        pb_span_ctx: &mut IndicatifSpanContext,
254        ctx: &layer::Context<'_, S>,
255    ) where
256        S: Subscriber + for<'a> LookupSpan<'a>,
257    {
258        let Some(pb) = pb_span_ctx.progress_bar.take() else {
259            // Span was never entered.
260            return;
261        };
262
263        // The span closed before we had a chance to show its progress bar.
264        if pb.is_hidden() {
265            self.decrement_pending_pb();
266            return;
267        }
268
269        // This span had an active/shown progress bar.
270        pb.finish_and_clear();
271        self.mp.remove(&pb);
272        self.active_progress_bars -= 1;
273
274        loop {
275            let Some(span_id) = self.pending_spans.pop_front() else {
276                break;
277            };
278
279            match ctx.span(&span_id) {
280                Some(next_eligible_span) => {
281                    let mut ext = next_eligible_span.extensions_mut();
282                    let indicatif_span_ctx = ext
283                        .get_mut::<IndicatifSpanContext>()
284                        .expect("No IndicatifSpanContext found; this is a bug");
285
286                    // It possible `on_close` has been called on a span but it has not yet been
287                    // removed from `ctx.span` (e.g., tracing may still be iterating through each
288                    // layer's `on_close` method and cannot remove the span from the registry until
289                    // it has finished `on_close` for each layer). So we may successfully fetch the
290                    // span, despite having closed out its progress bar.
291                    if indicatif_span_ctx.progress_bar.is_none() {
292                        continue;
293                    }
294
295                    self.decrement_pending_pb();
296                    self.show_progress_bar(indicatif_span_ctx, &span_id);
297                    break;
298                }
299                None => {
300                    // Span was closed earlier, we "garbage collect" it from the queue here.
301                    continue;
302                }
303            }
304        }
305    }
306}