Skip to main content

gam_runtime/
loop_progress.rs

1//! Low-overhead progress ticker for long parallel loops.
2//!
3//! `LoopProgress::tick` advances a shared counter and lets exactly one
4//! worker emit after each wall-clock interval. Callers own the log message
5//! so units and totals stay local to the loop.
6
7use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
8use std::time::Instant;
9
10pub const DEFAULT_LOOP_PROGRESS_INTERVAL_SECS: u64 = 25;
11
12pub struct LoopProgress {
13    started: Instant,
14    last_emit_nanos: AtomicU64,
15    interval_nanos: u64,
16    progress: AtomicUsize,
17}
18
19impl LoopProgress {
20    pub fn new(interval_secs: u64) -> Self {
21        Self {
22            started: Instant::now(),
23            last_emit_nanos: AtomicU64::new(0),
24            interval_nanos: interval_secs.saturating_mul(1_000_000_000),
25            progress: AtomicUsize::new(0),
26        }
27    }
28
29    pub fn default_interval() -> Self {
30        Self::new(DEFAULT_LOOP_PROGRESS_INTERVAL_SECS)
31    }
32
33    /// Advance the progress counter by `delta` and, if at least
34    /// `interval` of wall time has passed since the last claimed print,
35    /// invoke `emit(progress, elapsed_secs)` exactly once across all
36    /// threads. The closure typically issues a `log::info!`.
37    pub fn tick(&self, delta: usize, emit: impl FnOnce(usize, f64)) {
38        let progress = self
39            .progress
40            .fetch_add(delta, Ordering::Relaxed)
41            .saturating_add(delta);
42        let elapsed = self.started.elapsed().as_nanos() as u64;
43        let last = self.last_emit_nanos.load(Ordering::Relaxed);
44        if elapsed < last.saturating_add(self.interval_nanos) {
45            return;
46        }
47        if self
48            .last_emit_nanos
49            .compare_exchange(last, elapsed, Ordering::Relaxed, Ordering::Relaxed)
50            .is_ok()
51        {
52            emit(progress, elapsed as f64 / 1.0e9);
53        }
54    }
55}