gam_runtime/
loop_progress.rs1use 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 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}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use std::sync::atomic::{AtomicBool, AtomicUsize};
61
62 #[test]
63 fn default_interval_constant_matches_expectation() {
64 assert_eq!(DEFAULT_LOOP_PROGRESS_INTERVAL_SECS, 25);
65 }
66
67 #[test]
68 fn new_with_zero_interval_emits_on_first_tick() {
69 let lp = LoopProgress::new(0);
70 let called = AtomicBool::new(false);
71 lp.tick(1, |_progress, _elapsed| {
72 called.store(true, Ordering::Relaxed);
73 });
74 assert!(called.load(Ordering::Relaxed), "emit should be called with zero interval");
75 }
76
77 #[test]
78 fn tick_accumulates_progress_across_calls() {
79 let lp = LoopProgress::new(0);
80 let last_seen = AtomicUsize::new(0);
81 lp.tick(5, |progress, _| {
82 last_seen.store(progress, Ordering::Relaxed);
83 });
84 assert_eq!(last_seen.load(Ordering::Relaxed), 5);
85 }
86
87 #[test]
88 fn tick_with_large_interval_does_not_emit_on_first_call() {
89 let lp = LoopProgress::new(3600);
94 let called = AtomicBool::new(false);
95 lp.tick(1, |_, _| {
96 called.store(true, Ordering::Relaxed);
97 });
98 assert!(!called.load(Ordering::Relaxed), "emit should not fire with 1-hour interval");
101 }
102
103 #[test]
104 fn tick_delta_zero_still_works() {
105 let lp = LoopProgress::new(0);
106 let seen = AtomicUsize::new(usize::MAX);
107 lp.tick(0, |progress, _elapsed| {
108 seen.store(progress, Ordering::Relaxed);
109 });
110 assert_eq!(
113 seen.load(Ordering::Relaxed),
114 0,
115 "zero-delta tick must emit progress 0"
116 );
117 }
118}