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::time::{
11 Duration,
12 Instant,
13};
14
15use crate::{
16 model::{
17 ProgressCounters,
18 ProgressEvent,
19 ProgressPhase,
20 ProgressStage,
21 },
22 reporter::ProgressReporter,
23};
24
25/// Tracks one progress-producing operation and reports lifecycle events.
26///
27/// `Progress` owns no operation-specific counters. Callers keep their own
28/// domain state and pass freshly built [`ProgressCounters`] when reporting.
29/// The run only manages elapsed time, periodic running-event throttling,
30/// optional stage metadata, and forwarding immutable events to a reporter.
31///
32/// # Examples
33///
34/// ```
35/// use std::time::Duration;
36///
37/// use qubit_progress::{
38/// ProgressCounters,
39/// Progress,
40/// WriterProgressReporter,
41/// };
42///
43/// let reporter = WriterProgressReporter::from_writer(std::io::stdout());
44/// let mut progress = Progress::new(&reporter, Duration::from_secs(5));
45///
46/// progress.report_started(ProgressCounters::new(Some(2)));
47///
48/// let running = ProgressCounters::new(Some(2))
49/// .with_completed_count(1)
50/// .with_active_count(1);
51/// let _reported = progress.report_running_if_due(running);
52///
53/// let finished = ProgressCounters::new(Some(2))
54/// .with_completed_count(2)
55/// .with_succeeded_count(2);
56/// progress.report_finished(finished);
57/// ```
58pub struct Progress<'a> {
59 /// Reporter receiving lifecycle callbacks for this run.
60 reporter: &'a dyn ProgressReporter,
61 /// Monotonic start time used to compute elapsed durations.
62 started_at: Instant,
63 /// Minimum interval between due-based running callbacks.
64 report_interval: Duration,
65 /// Next monotonic instant at which a due-based running callback may fire.
66 next_running_at: Instant,
67 /// Optional stage metadata attached to every event emitted by this run.
68 stage: Option<ProgressStage>,
69}
70
71impl<'a> Progress<'a> {
72 /// Creates a progress run starting at the current instant.
73 ///
74 /// # Parameters
75 ///
76 /// * `reporter` - Reporter receiving progress events.
77 /// * `report_interval` - Minimum delay between due-based running events.
78 ///
79 /// # Returns
80 ///
81 /// A progress run whose elapsed time is measured from now.
82 #[inline]
83 pub fn new(reporter: &'a dyn ProgressReporter, report_interval: Duration) -> Self {
84 Self::from_start(reporter, report_interval, Instant::now())
85 }
86
87 /// Creates a progress run from an explicit start instant.
88 ///
89 /// # Parameters
90 ///
91 /// * `reporter` - Reporter receiving progress events.
92 /// * `report_interval` - Minimum delay between due-based running events.
93 /// * `started_at` - Monotonic instant representing operation start.
94 ///
95 /// # Returns
96 ///
97 /// A progress run using `started_at` for elapsed-time calculations.
98 #[inline]
99 pub fn from_start(
100 reporter: &'a dyn ProgressReporter,
101 report_interval: Duration,
102 started_at: Instant,
103 ) -> Self {
104 Self {
105 reporter,
106 started_at,
107 report_interval,
108 next_running_at: next_instant(started_at, report_interval),
109 stage: None,
110 }
111 }
112
113 /// Returns a copy configured with stage metadata.
114 ///
115 /// # Parameters
116 ///
117 /// * `stage` - Stage metadata attached to subsequently reported events.
118 ///
119 /// # Returns
120 ///
121 /// This progress run with `stage` recorded.
122 #[inline]
123 pub fn with_stage(mut self, stage: ProgressStage) -> Self {
124 self.stage = Some(stage);
125 self
126 }
127
128 /// Returns a copy with stage metadata removed.
129 ///
130 /// # Returns
131 ///
132 /// This progress run without stage metadata.
133 #[inline]
134 pub fn without_stage(mut self) -> Self {
135 self.stage = None;
136 self
137 }
138
139 /// Reports a started lifecycle event.
140 ///
141 /// # Parameters
142 ///
143 /// * `counters` - Initial counters for the operation.
144 ///
145 /// # Panics
146 ///
147 /// Propagates panics from the configured reporter.
148 #[inline]
149 pub fn report_started(&self, counters: ProgressCounters) {
150 self.report(ProgressPhase::Started, counters);
151 }
152
153 /// Reports a running lifecycle event immediately.
154 ///
155 /// # Parameters
156 ///
157 /// * `counters` - Current counters for the operation.
158 ///
159 /// # Panics
160 ///
161 /// Propagates panics from the configured reporter.
162 #[inline]
163 pub fn report_running(&self, counters: ProgressCounters) {
164 self.report(ProgressPhase::Running, counters);
165 }
166
167 /// Reports a running lifecycle event if the configured interval has passed.
168 ///
169 /// # Parameters
170 ///
171 /// * `counters` - Current counters for the operation.
172 ///
173 /// # Returns
174 ///
175 /// `true` when a running event was emitted, or `false` when the next
176 /// running-event deadline has not been reached.
177 ///
178 /// This method does not block waiting for the next deadline. It returns
179 /// immediately when not due, and when due it synchronously calls the
180 /// configured reporter. Any blocking behavior therefore comes from the
181 /// reporter implementation.
182 ///
183 /// # Panics
184 ///
185 /// Propagates panics from the configured reporter when an event is due.
186 pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> bool {
187 let now = Instant::now();
188 if now < self.next_running_at {
189 return false;
190 }
191 self.report_with_elapsed(
192 ProgressPhase::Running,
193 counters,
194 now.saturating_duration_since(self.started_at),
195 );
196 self.next_running_at = next_instant(now, self.report_interval);
197 true
198 }
199
200 /// Reports a finished lifecycle event.
201 ///
202 /// # Parameters
203 ///
204 /// * `counters` - Final counters for a successfully completed operation.
205 ///
206 /// # Panics
207 ///
208 /// Propagates panics from the configured reporter.
209 #[inline]
210 pub fn report_finished(&self, counters: ProgressCounters) {
211 self.report(ProgressPhase::Finished, counters);
212 }
213
214 /// Reports a failed lifecycle event.
215 ///
216 /// # Parameters
217 ///
218 /// * `counters` - Final or current counters for a failed operation.
219 ///
220 /// # Panics
221 ///
222 /// Propagates panics from the configured reporter.
223 #[inline]
224 pub fn report_failed(&self, counters: ProgressCounters) {
225 self.report(ProgressPhase::Failed, counters);
226 }
227
228 /// Reports a canceled lifecycle event.
229 ///
230 /// # Parameters
231 ///
232 /// * `counters` - Final or current counters for a canceled operation.
233 ///
234 /// # Panics
235 ///
236 /// Propagates panics from the configured reporter.
237 #[inline]
238 pub fn report_canceled(&self, counters: ProgressCounters) {
239 self.report(ProgressPhase::Canceled, counters);
240 }
241
242 /// Reports a lifecycle event with the run's current elapsed duration.
243 ///
244 /// # Parameters
245 ///
246 /// * `phase` - Lifecycle phase to report.
247 /// * `counters` - Counters carried by the event.
248 ///
249 /// # Panics
250 ///
251 /// Propagates panics from the configured reporter.
252 #[inline]
253 pub fn report(&self, phase: ProgressPhase, counters: ProgressCounters) {
254 self.report_with_elapsed(phase, counters, self.elapsed());
255 }
256
257 /// Reports a lifecycle event with an explicit elapsed duration.
258 ///
259 /// # Parameters
260 ///
261 /// * `phase` - Lifecycle phase to report.
262 /// * `counters` - Counters carried by the event.
263 /// * `elapsed` - Elapsed duration carried by the event.
264 ///
265 /// # Panics
266 ///
267 /// Propagates panics from the configured reporter.
268 pub fn report_with_elapsed(
269 &self,
270 phase: ProgressPhase,
271 counters: ProgressCounters,
272 elapsed: Duration,
273 ) {
274 let event = self.event_with_elapsed(phase, counters, elapsed);
275 self.reporter.report(&event);
276 }
277
278 /// Returns the elapsed duration since this run started.
279 ///
280 /// # Returns
281 ///
282 /// The monotonic elapsed duration for this progress run.
283 #[inline]
284 pub fn elapsed(&self) -> Duration {
285 self.started_at.elapsed()
286 }
287
288 /// Returns the start instant for this run.
289 ///
290 /// # Returns
291 ///
292 /// The monotonic instant used as this run's start time.
293 #[inline]
294 pub const fn started_at(&self) -> Instant {
295 self.started_at
296 }
297
298 /// Returns the configured running-event interval.
299 ///
300 /// # Returns
301 ///
302 /// The minimum delay between due-based running events.
303 #[inline]
304 pub const fn report_interval(&self) -> Duration {
305 self.report_interval
306 }
307
308 /// Returns the optional stage metadata attached to events.
309 ///
310 /// # Returns
311 ///
312 /// `Some(stage)` when stage metadata is configured, otherwise `None`.
313 #[inline]
314 pub const fn stage(&self) -> Option<&ProgressStage> {
315 self.stage.as_ref()
316 }
317
318 /// Builds a progress event with optional stage metadata.
319 ///
320 /// # Parameters
321 ///
322 /// * `phase` - Lifecycle phase for the event.
323 /// * `counters` - Counters carried by the event.
324 /// * `elapsed` - Elapsed duration carried by the event.
325 ///
326 /// # Returns
327 ///
328 /// A progress event ready to be sent to the reporter.
329 fn event_with_elapsed(
330 &self,
331 phase: ProgressPhase,
332 counters: ProgressCounters,
333 elapsed: Duration,
334 ) -> ProgressEvent {
335 let event = ProgressEvent::from_phase(phase, counters, elapsed);
336 match self.stage.clone() {
337 Some(stage) => event.with_stage(stage),
338 None => event,
339 }
340 }
341}
342
343/// Computes the next reporting instant while avoiding overflow panics.
344///
345/// # Parameters
346///
347/// * `base` - Base instant for the deadline.
348/// * `interval` - Duration added to `base`.
349///
350/// # Returns
351///
352/// `base + interval`, or `base` when the addition overflows.
353fn next_instant(base: Instant, interval: Duration) -> Instant {
354 base.checked_add(interval).unwrap_or(base)
355}