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!(
75 called.load(Ordering::Relaxed),
76 "emit should be called with zero interval"
77 );
78 }
79
80 #[test]
81 fn tick_accumulates_progress_across_calls() {
82 let lp = LoopProgress::new(0);
83 let last_seen = AtomicUsize::new(0);
84 lp.tick(5, |progress, _| {
85 last_seen.store(progress, Ordering::Relaxed);
86 });
87 assert_eq!(last_seen.load(Ordering::Relaxed), 5);
88 }
89
90 #[test]
91 fn tick_with_large_interval_does_not_emit_on_first_call() {
92 let lp = LoopProgress::new(3600);
97 let called = AtomicBool::new(false);
98 lp.tick(1, |_, _| {
99 called.store(true, Ordering::Relaxed);
100 });
101 assert!(
104 !called.load(Ordering::Relaxed),
105 "emit should not fire with 1-hour interval"
106 );
107 }
108
109 #[test]
110 fn tick_delta_zero_still_works() {
111 let lp = LoopProgress::new(0);
112 let seen = AtomicUsize::new(usize::MAX);
113 lp.tick(0, |progress, _elapsed| {
114 seen.store(progress, Ordering::Relaxed);
115 });
116 assert_eq!(
119 seen.load(Ordering::Relaxed),
120 0,
121 "zero-delta tick must emit progress 0"
122 );
123 }
124}