veloren_common/
clock.rs

1use common_base::span;
2use ordered_float::NotNan;
3use std::{
4    collections::VecDeque,
5    time::{Duration, Instant},
6};
7
8/// This Clock tries to make this tick a constant time by sleeping the rest of
9/// the tick
10/// - if we actually took less time than we planned: sleep and return planned
11///   time
12/// - if we ran behind: don't sleep and return actual time
13///
14/// We DON'T do any fancy averaging of the deltas for tick for 2 reasons:
15///  - all Systems have to work based on `dt` and we cannot assume that this is
16///    const through all ticks
17///  - when we have a slow tick, a lag, it doesn't help that we have 10 fast
18///    ticks directly afterwards
19///
20/// We return a smoothed version for display only!
21pub struct Clock {
22    /// This is the dt that the Clock tries to archive with each call of tick.
23    target_dt: Duration,
24    /// Last time `tick` was called
25    last_sys_time: Instant,
26    /// Will be calculated in `tick` returns the dt used by the next iteration
27    /// of the main loop
28    last_dt: Duration,
29    /// Summed up `last_dt`
30    total_tick_time: Duration,
31    // Stats only
32    // uses f32 so we have enough precision to display fps values while saving space
33    // This is in seconds
34    last_dts: VecDeque<NotNan<f32>>,
35    last_dts_sorted: Vec<NotNan<f32>>,
36    last_busy_dts: VecDeque<NotNan<f32>>,
37    stats: ClockStats,
38}
39
40pub struct ClockStats {
41    /// Busy dt is the part of the tick that we didn't sleep.
42    /// e.g. the total tick is 33ms, including 25ms sleeping. then this returns
43    /// 8ms
44    /// This is in seconds
45    pub average_busy_dt: Duration,
46    /// avg over the last NUMBER_OF_OLD_DELTAS_KEPT ticks
47    pub average_tps: f64,
48    /// = 50% percentile
49    pub median_tps: f64,
50    /// lowest 10% of the frames
51    pub percentile_90_tps: f64,
52    /// lowest 5% of the frames
53    pub percentile_95_tps: f64,
54    /// lowest 1% of the frames
55    pub percentile_99_tps: f64,
56}
57
58const NUMBER_OF_OLD_DELTAS_KEPT: usize = 100;
59const NUMBER_OF_DELTAS_COMPARED: usize = 5;
60
61impl Clock {
62    pub fn new(target_dt: Duration) -> Self {
63        Self {
64            target_dt,
65            last_sys_time: Instant::now(),
66            last_dt: target_dt,
67            total_tick_time: Duration::default(),
68            last_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
69            last_dts_sorted: Vec::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
70            last_busy_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
71            stats: ClockStats::new(&[], &VecDeque::new()),
72        }
73    }
74
75    pub fn set_target_dt(&mut self, target_dt: Duration) { self.target_dt = target_dt; }
76
77    pub fn stats(&self) -> &ClockStats { &self.stats }
78
79    pub fn dt(&self) -> Duration { self.last_dt }
80
81    pub fn get_stable_dt(&self) -> Duration {
82        if self.last_dts.len() >= NUMBER_OF_DELTAS_COMPARED {
83            // Take the median of the last few tick times
84            let mut dts = [0.0; NUMBER_OF_DELTAS_COMPARED];
85            for (i, dt) in self
86                .last_dts
87                .iter()
88                .rev()
89                .take(NUMBER_OF_DELTAS_COMPARED)
90                .enumerate()
91            {
92                dts[i] = **dt;
93            }
94            dts.sort_by_key(|x| ordered_float::OrderedFloat(*x));
95            let stable_dt = Duration::from_secs_f32(dts[NUMBER_OF_DELTAS_COMPARED / 2]);
96
97            if self.last_dt > 2 * stable_dt {
98                tracing::trace!(?self.last_dt, ?self.total_tick_time, "lag spike detected, unusually slow tick");
99                stable_dt
100            } else {
101                self.last_dt
102            }
103        } else {
104            self.last_dt
105        }
106    }
107
108    /// Do not modify without asking @xMAC94x first!
109    pub fn tick(&mut self) {
110        span!(_guard, "tick", "Clock::tick");
111        span!(guard, "clock work");
112        let current_sys_time = Instant::now();
113        let busy_delta = current_sys_time.duration_since(self.last_sys_time);
114        // Maintain TPS
115        self.last_dts_sorted = self.last_dts.iter().copied().collect();
116        self.last_dts_sorted.sort_unstable();
117        self.stats = ClockStats::new(&self.last_dts_sorted, &self.last_busy_dts);
118        drop(guard);
119        // Attempt to sleep to fill the gap.
120        if let Some(sleep_dur) = self.target_dt.checked_sub(busy_delta) {
121            spin_sleep::sleep(sleep_dur);
122        }
123
124        let after_sleep_sys_time = Instant::now();
125        self.last_dt = after_sleep_sys_time.duration_since(self.last_sys_time);
126        if self.last_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
127            self.last_dts.pop_front();
128        }
129        if self.last_busy_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
130            self.last_busy_dts.pop_front();
131        }
132        self.last_dts.push_back(
133            NotNan::new(self.last_dt.as_secs_f32())
134                .expect("Duration::as_secs_f32 never returns NaN"),
135        );
136        self.last_busy_dts.push_back(
137            NotNan::new(busy_delta.as_secs_f32()).expect("Duration::as_secs_f32 never returns NaN"),
138        );
139        self.total_tick_time += self.last_dt;
140        self.last_sys_time = after_sleep_sys_time;
141    }
142}
143
144impl ClockStats {
145    fn new(sorted: &[NotNan<f32>], busy_dt_list: &VecDeque<NotNan<f32>>) -> Self {
146        let average_frame_time =
147            sorted.iter().sum::<NotNan<f32>>().into_inner() / sorted.len().max(1) as f32;
148
149        let average_busy_dt = busy_dt_list.iter().sum::<NotNan<f32>>().into_inner()
150            / busy_dt_list.len().max(1) as f32;
151
152        let average_tps = 1.0 / average_frame_time as f64;
153        let (median_tps, percentile_90_tps, percentile_95_tps, percentile_99_tps) =
154            if sorted.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
155                let median_frame_time = *sorted[sorted.len() / 2];
156                let percentile_90_frame_time =
157                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.1) as usize];
158                let percentile_95_frame_time =
159                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.05) as usize];
160                let percentile_99_frame_time =
161                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.01) as usize];
162
163                let median_tps = 1.0 / median_frame_time as f64;
164                let percentile_90_tps = 1.0 / percentile_90_frame_time as f64;
165                let percentile_95_tps = 1.0 / percentile_95_frame_time as f64;
166                let percentile_99_tps = 1.0 / percentile_99_frame_time as f64;
167                (
168                    median_tps,
169                    percentile_90_tps,
170                    percentile_95_tps,
171                    percentile_99_tps,
172                )
173            } else {
174                let avg_tps = 1.0 / average_busy_dt as f64;
175                (avg_tps, avg_tps, avg_tps, avg_tps)
176            };
177
178        Self {
179            average_busy_dt: Duration::from_secs_f32(average_busy_dt),
180            average_tps,
181            median_tps,
182            percentile_90_tps,
183            percentile_95_tps,
184            percentile_99_tps,
185        }
186    }
187}