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}