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(1))
311 /// .with_completed_count(loop_completed.load(Ordering::Acquire))
312 /// });
313 /// let point = running.point_handle();
314 ///
315 /// completed.store(1, Ordering::Release);
316 /// assert!(point.report());
317 ///
318 /// running.stop_and_join();
319 /// });
320 /// ```
321 pub fn spawn_running_reporter<'scope, 'env, F>(
322 &self,
323 scope: &'scope thread::Scope<'scope, 'env>,
324 snapshot: F,
325 ) -> RunningProgressGuard<'scope>
326 where
327 'a: 'scope,
328 F: FnMut() -> ProgressCounters + Send + 'scope,
329 {
330 RunningProgressLoop::spawn_scoped(scope, self.fork_for_running(), snapshot)
331 }
332
333 /// Reports a lifecycle event with an explicit elapsed duration.
334 ///
335 /// # Parameters
336 ///
337 /// * `phase` - Lifecycle phase to report.
338 /// * `counters` - Counters carried by the event.
339 /// * `elapsed` - Elapsed duration carried by the event.
340 ///
341 /// # Returns
342 ///
343 /// The event sent to the configured reporter.
344 ///
345 /// # Panics
346 ///
347 /// Propagates panics from the configured reporter.
348 fn report_with_elapsed(
349 &self,
350 phase: ProgressPhase,
351 counters: ProgressCounters,
352 elapsed: Duration,
353 ) -> ProgressEvent {
354 let event = self.event_with_elapsed(phase, counters, elapsed);
355 self.reporter.report(&event);
356 event
357 }
358
359 /// Returns the elapsed duration since this run started.
360 ///
361 /// # Returns
362 ///
363 /// The monotonic elapsed duration for this progress run.
364 #[inline]
365 pub fn elapsed(&self) -> Duration {
366 self.started_at.elapsed()
367 }
368
369 /// Returns the start instant for this run.
370 ///
371 /// # Returns
372 ///
373 /// The monotonic instant used as this run's start time.
374 #[inline]
375 pub const fn started_at(&self) -> Instant {
376 self.started_at
377 }
378
379 /// Returns the configured running-event interval.
380 ///
381 /// # Returns
382 ///
383 /// The minimum delay between due-based running events.
384 #[inline]
385 pub const fn report_interval(&self) -> Duration {
386 self.report_interval
387 }
388
389 /// Returns the optional stage metadata attached to events.
390 ///
391 /// # Returns
392 ///
393 /// `Some(stage)` when stage metadata is configured, otherwise `None`.
394 #[inline]
395 pub const fn stage(&self) -> Option<&ProgressStage> {
396 self.stage.as_ref()
397 }
398
399 /// Creates a background-thread copy that reports running events for this run.
400 ///
401 /// # Returns
402 ///
403 /// A progress run with the same reporter, start time, interval, stage, and
404 /// next running deadline as this run.
405 fn fork_for_running(&self) -> Self {
406 Self {
407 reporter: self.reporter,
408 started_at: self.started_at,
409 report_interval: self.report_interval,
410 next_running_at: self.next_running_at,
411 stage: self.stage.clone(),
412 }
413 }
414
415 /// Builds a progress event with optional stage metadata.
416 ///
417 /// # Parameters
418 ///
419 /// * `phase` - Lifecycle phase for the event.
420 /// * `counters` - Counters carried by the event.
421 /// * `elapsed` - Elapsed duration carried by the event.
422 ///
423 /// # Returns
424 ///
425 /// A progress event ready to be sent to the reporter.
426 fn event_with_elapsed(
427 &self,
428 phase: ProgressPhase,
429 counters: ProgressCounters,
430 elapsed: Duration,
431 ) -> ProgressEvent {
432 let event = ProgressEvent::from_phase(phase, counters, elapsed);
433 match self.stage.clone() {
434 Some(stage) => event.with_stage(stage),
435 None => event,
436 }
437 }
438}
439
440/// Computes the next reporting instant while avoiding overflow panics.
441///
442/// # Parameters
443///
444/// * `base` - Base instant for the deadline.
445/// * `interval` - Duration added to `base`.
446///
447/// # Returns
448///
449/// `base + interval`, or `base` when the addition overflows.
450fn next_instant(base: Instant, interval: Duration) -> Instant {
451 base.checked_add(interval).unwrap_or(base)
452}