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