Skip to main content

qubit_progress/
progress.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11    thread,
12    time::{
13        Duration,
14        Instant,
15    },
16};
17
18use crate::{
19    model::{
20        ProgressCounters,
21        ProgressEvent,
22        ProgressPhase,
23        ProgressStage,
24    },
25    reporter::ProgressReporter,
26    running::{
27        RunningProgressGuard,
28        RunningProgressLoop,
29    },
30};
31
32/// Tracks one progress-producing operation and reports lifecycle events.
33///
34/// `Progress` owns no operation-specific counters. Callers keep their own
35/// domain state and pass freshly built [`ProgressCounters`] when reporting.
36/// The run only manages elapsed time, periodic running-event throttling,
37/// optional stage metadata, and forwarding immutable events to a reporter.
38///
39/// # Examples
40///
41/// ```
42/// use std::time::Duration;
43///
44/// use qubit_progress::{
45///     ProgressCounters,
46///     Progress,
47///     WriterProgressReporter,
48/// };
49///
50/// let reporter = WriterProgressReporter::from_writer(std::io::stdout());
51/// let mut progress = Progress::new(&reporter, Duration::from_secs(5));
52///
53/// let started = progress.report_started(ProgressCounters::new(Some(2)));
54/// assert!(started.elapsed().is_zero());
55///
56/// let running = ProgressCounters::new(Some(2))
57///     .with_completed_count(1)
58///     .with_active_count(1);
59/// let _reported = progress.report_running_if_due(running);
60///
61/// let finished = ProgressCounters::new(Some(2))
62///     .with_completed_count(2)
63///     .with_succeeded_count(2);
64/// let finished_event = progress.report_finished(finished);
65/// assert!(finished_event.elapsed() >= started.elapsed());
66/// ```
67pub struct Progress<'a> {
68    /// Reporter receiving lifecycle callbacks for this run.
69    reporter: &'a dyn ProgressReporter,
70    /// Monotonic start time used to compute elapsed durations.
71    started_at: Instant,
72    /// Minimum interval between due-based running callbacks.
73    report_interval: Duration,
74    /// Next monotonic instant at which a due-based running callback may fire.
75    next_running_at: Instant,
76    /// Optional stage metadata attached to every event emitted by this run.
77    stage: Option<ProgressStage>,
78}
79
80impl<'a> Progress<'a> {
81    /// Creates a progress run starting at the current instant.
82    ///
83    /// # Parameters
84    ///
85    /// * `reporter` - Reporter receiving progress events.
86    /// * `report_interval` - Minimum delay between due-based running events.
87    ///
88    /// # Returns
89    ///
90    /// A progress run whose elapsed time is measured from now.
91    #[inline]
92    pub fn new(reporter: &'a dyn ProgressReporter, report_interval: Duration) -> Self {
93        Self::from_start(reporter, report_interval, Instant::now())
94    }
95
96    /// Creates a progress run from an explicit start instant.
97    ///
98    /// # Parameters
99    ///
100    /// * `reporter` - Reporter receiving progress events.
101    /// * `report_interval` - Minimum delay between due-based running events.
102    /// * `started_at` - Monotonic instant representing operation start.
103    ///
104    /// # Returns
105    ///
106    /// A progress run using `started_at` for elapsed-time calculations.
107    #[inline]
108    fn from_start(
109        reporter: &'a dyn ProgressReporter,
110        report_interval: Duration,
111        started_at: Instant,
112    ) -> Self {
113        Self {
114            reporter,
115            started_at,
116            report_interval,
117            next_running_at: next_instant(started_at, report_interval),
118            stage: None,
119        }
120    }
121
122    /// Returns a copy configured with stage metadata.
123    ///
124    /// # Parameters
125    ///
126    /// * `stage` - Stage metadata attached to subsequently reported events.
127    ///
128    /// # Returns
129    ///
130    /// This progress run with `stage` recorded.
131    #[inline]
132    pub fn with_stage(mut self, stage: ProgressStage) -> Self {
133        self.stage = Some(stage);
134        self
135    }
136
137    /// Returns a copy with stage metadata removed.
138    ///
139    /// # Returns
140    ///
141    /// This progress run without stage metadata.
142    #[inline]
143    pub fn without_stage(mut self) -> Self {
144        self.stage = None;
145        self
146    }
147
148    /// Reports a started lifecycle event.
149    ///
150    /// # Parameters
151    ///
152    /// * `counters` - Initial counters for the operation.
153    ///
154    /// # Panics
155    ///
156    /// Propagates panics from the configured reporter.
157    #[inline]
158    pub fn report_started(&self, counters: ProgressCounters) -> ProgressEvent {
159        self.report_with_elapsed(ProgressPhase::Started, counters, Duration::ZERO)
160    }
161
162    /// Reports a running lifecycle event immediately.
163    ///
164    /// # Parameters
165    ///
166    /// * `counters` - Current counters for the operation.
167    ///
168    /// # Panics
169    ///
170    /// Propagates panics from the configured reporter.
171    pub fn report_running(&mut self, counters: ProgressCounters) -> ProgressEvent {
172        let now = Instant::now();
173        let event = self.report_with_elapsed(
174            ProgressPhase::Running,
175            counters,
176            now.saturating_duration_since(self.started_at),
177        );
178        self.next_running_at = next_instant(now, self.report_interval);
179        event
180    }
181
182    /// Reports a running lifecycle event if the configured interval has passed.
183    ///
184    /// # Parameters
185    ///
186    /// * `counters` - Current counters for the operation.
187    ///
188    /// # Returns
189    ///
190    /// `Some(event)` when a running event was emitted, or `None` when the next
191    /// running-event deadline has not been reached.
192    ///
193    /// This method does not block waiting for the next deadline. It returns
194    /// immediately when not due, and when due it synchronously calls the
195    /// configured reporter. Any blocking behavior therefore comes from the
196    /// reporter implementation.
197    ///
198    /// # Panics
199    ///
200    /// Propagates panics from the configured reporter when an event is due.
201    pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> Option<ProgressEvent> {
202        let now = Instant::now();
203        if now < self.next_running_at {
204            return None;
205        }
206        let event = self.report_with_elapsed(
207            ProgressPhase::Running,
208            counters,
209            now.saturating_duration_since(self.started_at),
210        );
211        self.next_running_at = next_instant(now, self.report_interval);
212        Some(event)
213    }
214
215    /// Reports a finished lifecycle event.
216    ///
217    /// # Parameters
218    ///
219    /// * `counters` - Final counters for a successfully completed operation.
220    ///
221    /// # Panics
222    ///
223    /// Propagates panics from the configured reporter.
224    #[inline]
225    pub fn report_finished(&self, counters: ProgressCounters) -> ProgressEvent {
226        self.report_with_elapsed(ProgressPhase::Finished, counters, self.elapsed())
227    }
228
229    /// Reports a failed lifecycle event.
230    ///
231    /// # Parameters
232    ///
233    /// * `counters` - Final or current counters for a failed operation.
234    ///
235    /// # Panics
236    ///
237    /// Propagates panics from the configured reporter.
238    #[inline]
239    pub fn report_failed(&self, counters: ProgressCounters) -> ProgressEvent {
240        self.report_with_elapsed(ProgressPhase::Failed, counters, self.elapsed())
241    }
242
243    /// Reports a canceled lifecycle event.
244    ///
245    /// # Parameters
246    ///
247    /// * `counters` - Final or current counters for a canceled operation.
248    ///
249    /// # Panics
250    ///
251    /// Propagates panics from the configured reporter.
252    #[inline]
253    pub fn report_canceled(&self, counters: ProgressCounters) -> ProgressEvent {
254        self.report_with_elapsed(ProgressPhase::Canceled, counters, self.elapsed())
255    }
256
257    /// Spawns a scoped background reporter for periodic running events.
258    ///
259    /// The background reporter shares this progress run's reporter, start time,
260    /// interval, and stage metadata. Worker threads should update their own
261    /// domain state first, then call
262    /// [`RunningProgressPointHandle::report`](crate::RunningProgressPointHandle::report)
263    /// on the handle returned by the guard. The guard must be stopped and
264    /// joined before the thread scope exits.
265    ///
266    /// # Parameters
267    ///
268    /// * `scope` - Thread scope that owns the background reporter thread.
269    /// * `snapshot` - Closure that builds fresh counters from caller-owned
270    ///   domain state whenever a running event may be due.
271    ///
272    /// # Returns
273    ///
274    /// A guard that owns the background reporter thread and can create
275    /// worker-side point handles.
276    ///
277    /// # Panics
278    ///
279    /// Panics raised by the reporter thread are propagated by
280    /// [`RunningProgressGuard::stop_and_join`].
281    ///
282    /// # Examples
283    ///
284    /// ```
285    /// use std::{
286    ///     sync::{
287    ///         Arc,
288    ///         atomic::{
289    ///             AtomicUsize,
290    ///             Ordering,
291    ///         },
292    ///     },
293    ///     thread,
294    ///     time::Duration,
295    /// };
296    ///
297    /// use qubit_progress::{
298    ///     NoOpProgressReporter,
299    ///     Progress,
300    ///     ProgressCounters,
301    /// };
302    ///
303    /// let reporter = NoOpProgressReporter;
304    /// let completed = Arc::new(AtomicUsize::new(0));
305    /// let progress = Progress::new(&reporter, Duration::ZERO);
306    ///
307    /// thread::scope(|scope| {
308    ///     let loop_completed = Arc::clone(&completed);
309    ///     let running = progress.spawn_running_reporter(scope, move || {
310    ///         ProgressCounters::new(Some(3))
311    ///             .with_completed_count(loop_completed.load(Ordering::Acquire))
312    ///     });
313    ///     let point = running.point_handle();
314    ///
315    ///     let mut handles = Vec::new();
316    ///     for _ in 0..3 {
317    ///         let c = Arc::clone(&completed);
318    ///         let p = point.clone();
319    ///         handles.push(scope.spawn(move || {
320    ///             c.fetch_add(1, Ordering::AcqRel);
321    ///             assert!(p.report());
322    ///         }));
323    ///     }
324    ///     for h in handles {
325    ///         h.join().unwrap();
326    ///     }
327    ///
328    ///     running.stop_and_join();
329    /// });
330    /// ```
331    pub fn spawn_running_reporter<'scope, 'env, F>(
332        &self,
333        scope: &'scope thread::Scope<'scope, 'env>,
334        snapshot: F,
335    ) -> RunningProgressGuard<'scope>
336    where
337        'a: 'scope,
338        F: FnMut() -> ProgressCounters + Send + 'scope,
339    {
340        RunningProgressLoop::spawn_scoped(scope, self.fork_for_running(), snapshot)
341    }
342
343    /// Reports a lifecycle event with an explicit elapsed duration.
344    ///
345    /// # Parameters
346    ///
347    /// * `phase` - Lifecycle phase to report.
348    /// * `counters` - Counters carried by the event.
349    /// * `elapsed` - Elapsed duration carried by the event.
350    ///
351    /// # Returns
352    ///
353    /// The event sent to the configured reporter.
354    ///
355    /// # Panics
356    ///
357    /// Propagates panics from the configured reporter.
358    fn report_with_elapsed(
359        &self,
360        phase: ProgressPhase,
361        counters: ProgressCounters,
362        elapsed: Duration,
363    ) -> ProgressEvent {
364        let event = self.event_with_elapsed(phase, counters, elapsed);
365        self.reporter.report(&event);
366        event
367    }
368
369    /// Returns the elapsed duration since this run started.
370    ///
371    /// # Returns
372    ///
373    /// The monotonic elapsed duration for this progress run.
374    #[inline]
375    pub fn elapsed(&self) -> Duration {
376        self.started_at.elapsed()
377    }
378
379    /// Returns the start instant for this run.
380    ///
381    /// # Returns
382    ///
383    /// The monotonic instant used as this run's start time.
384    #[inline]
385    pub const fn started_at(&self) -> Instant {
386        self.started_at
387    }
388
389    /// Returns the configured running-event interval.
390    ///
391    /// # Returns
392    ///
393    /// The minimum delay between due-based running events.
394    #[inline]
395    pub const fn report_interval(&self) -> Duration {
396        self.report_interval
397    }
398
399    /// Returns the optional stage metadata attached to events.
400    ///
401    /// # Returns
402    ///
403    /// `Some(stage)` when stage metadata is configured, otherwise `None`.
404    #[inline]
405    pub const fn stage(&self) -> Option<&ProgressStage> {
406        self.stage.as_ref()
407    }
408
409    /// Creates a background-thread copy that reports running events for this run.
410    ///
411    /// # Returns
412    ///
413    /// A progress run with the same reporter, start time, interval, stage, and
414    /// next running deadline as this run.
415    fn fork_for_running(&self) -> Self {
416        Self {
417            reporter: self.reporter,
418            started_at: self.started_at,
419            report_interval: self.report_interval,
420            next_running_at: self.next_running_at,
421            stage: self.stage.clone(),
422        }
423    }
424
425    /// Builds a progress event with optional stage metadata.
426    ///
427    /// # Parameters
428    ///
429    /// * `phase` - Lifecycle phase for the event.
430    /// * `counters` - Counters carried by the event.
431    /// * `elapsed` - Elapsed duration carried by the event.
432    ///
433    /// # Returns
434    ///
435    /// A progress event ready to be sent to the reporter.
436    fn event_with_elapsed(
437        &self,
438        phase: ProgressPhase,
439        counters: ProgressCounters,
440        elapsed: Duration,
441    ) -> ProgressEvent {
442        let event = ProgressEvent::from_phase(phase, counters, elapsed);
443        match self.stage.clone() {
444            Some(stage) => event.with_stage(stage),
445            None => event,
446        }
447    }
448}
449
450/// Computes the next reporting instant while avoiding overflow panics.
451///
452/// # Parameters
453///
454/// * `base` - Base instant for the deadline.
455/// * `interval` - Duration added to `base`.
456///
457/// # Returns
458///
459/// `base + interval`, or `base` when the addition overflows.
460fn next_instant(base: Instant, interval: Duration) -> Instant {
461    base.checked_add(interval).unwrap_or(base)
462}