qubit_progress/runtime/progress_run.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/// `ProgressRun` 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/// NoOpProgressReporter,
39/// ProgressCounters,
40/// ProgressRun,
41/// };
42///
43/// let reporter = NoOpProgressReporter;
44/// let mut progress = ProgressRun::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 ProgressRun<'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> ProgressRun<'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 /// # Panics
179 ///
180 /// Propagates panics from the configured reporter when an event is due.
181 pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> bool {
182 let now = Instant::now();
183 if now < self.next_running_at {
184 return false;
185 }
186 self.report_with_elapsed(
187 ProgressPhase::Running,
188 counters,
189 now.saturating_duration_since(self.started_at),
190 );
191 self.next_running_at = next_instant(now, self.report_interval);
192 true
193 }
194
195 /// Reports a finished lifecycle event.
196 ///
197 /// # Parameters
198 ///
199 /// * `counters` - Final counters for a successfully completed operation.
200 ///
201 /// # Panics
202 ///
203 /// Propagates panics from the configured reporter.
204 #[inline]
205 pub fn report_finished(&self, counters: ProgressCounters) {
206 self.report(ProgressPhase::Finished, counters);
207 }
208
209 /// Reports a failed lifecycle event.
210 ///
211 /// # Parameters
212 ///
213 /// * `counters` - Final or current counters for a failed operation.
214 ///
215 /// # Panics
216 ///
217 /// Propagates panics from the configured reporter.
218 #[inline]
219 pub fn report_failed(&self, counters: ProgressCounters) {
220 self.report(ProgressPhase::Failed, counters);
221 }
222
223 /// Reports a canceled lifecycle event.
224 ///
225 /// # Parameters
226 ///
227 /// * `counters` - Final or current counters for a canceled operation.
228 ///
229 /// # Panics
230 ///
231 /// Propagates panics from the configured reporter.
232 #[inline]
233 pub fn report_canceled(&self, counters: ProgressCounters) {
234 self.report(ProgressPhase::Canceled, counters);
235 }
236
237 /// Reports a lifecycle event with the run's current elapsed duration.
238 ///
239 /// # Parameters
240 ///
241 /// * `phase` - Lifecycle phase to report.
242 /// * `counters` - Counters carried by the event.
243 ///
244 /// # Panics
245 ///
246 /// Propagates panics from the configured reporter.
247 #[inline]
248 pub fn report(&self, phase: ProgressPhase, counters: ProgressCounters) {
249 self.report_with_elapsed(phase, counters, self.elapsed());
250 }
251
252 /// Reports a lifecycle event with an explicit elapsed duration.
253 ///
254 /// # Parameters
255 ///
256 /// * `phase` - Lifecycle phase to report.
257 /// * `counters` - Counters carried by the event.
258 /// * `elapsed` - Elapsed duration carried by the event.
259 ///
260 /// # Panics
261 ///
262 /// Propagates panics from the configured reporter.
263 pub fn report_with_elapsed(
264 &self,
265 phase: ProgressPhase,
266 counters: ProgressCounters,
267 elapsed: Duration,
268 ) {
269 let event = self.event_with_elapsed(phase, counters, elapsed);
270 self.reporter.report(&event);
271 }
272
273 /// Returns the elapsed duration since this run started.
274 ///
275 /// # Returns
276 ///
277 /// The monotonic elapsed duration for this progress run.
278 #[inline]
279 pub fn elapsed(&self) -> Duration {
280 self.started_at.elapsed()
281 }
282
283 /// Returns the start instant for this run.
284 ///
285 /// # Returns
286 ///
287 /// The monotonic instant used as this run's start time.
288 #[inline]
289 pub const fn started_at(&self) -> Instant {
290 self.started_at
291 }
292
293 /// Returns the configured running-event interval.
294 ///
295 /// # Returns
296 ///
297 /// The minimum delay between due-based running events.
298 #[inline]
299 pub const fn report_interval(&self) -> Duration {
300 self.report_interval
301 }
302
303 /// Returns the optional stage metadata attached to events.
304 ///
305 /// # Returns
306 ///
307 /// `Some(stage)` when stage metadata is configured, otherwise `None`.
308 #[inline]
309 pub const fn stage(&self) -> Option<&ProgressStage> {
310 self.stage.as_ref()
311 }
312
313 /// Builds a progress event with optional stage metadata.
314 ///
315 /// # Parameters
316 ///
317 /// * `phase` - Lifecycle phase for the event.
318 /// * `counters` - Counters carried by the event.
319 /// * `elapsed` - Elapsed duration carried by the event.
320 ///
321 /// # Returns
322 ///
323 /// A progress event ready to be sent to the reporter.
324 fn event_with_elapsed(
325 &self,
326 phase: ProgressPhase,
327 counters: ProgressCounters,
328 elapsed: Duration,
329 ) -> ProgressEvent {
330 let event = ProgressEvent::from_phase(phase, counters, elapsed);
331 match self.stage.clone() {
332 Some(stage) => event.with_stage(stage),
333 None => event,
334 }
335 }
336}
337
338/// Computes the next reporting instant while avoiding overflow panics.
339///
340/// # Parameters
341///
342/// * `base` - Base instant for the deadline.
343/// * `interval` - Duration added to `base`.
344///
345/// # Returns
346///
347/// `base + interval`, or `base` when the addition overflows.
348fn next_instant(base: Instant, interval: Duration) -> Instant {
349 base.checked_add(interval).unwrap_or(base)
350}