1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use common_base::span;
use ordered_float::NotNan;
use std::{
    collections::VecDeque,
    time::{Duration, Instant},
};

/// This Clock tries to make this tick a constant time by sleeping the rest of
/// the tick
/// - if we actually took less time than we planned: sleep and return planned
///   time
/// - if we ran behind: don't sleep and return actual time
///
/// We DON'T do any fancy averaging of the deltas for tick for 2 reasons:
///  - all Systems have to work based on `dt` and we cannot assume that this is
///    const through all ticks
///  - when we have a slow tick, a lag, it doesn't help that we have 10 fast
///    ticks directly afterwards
///
/// We return a smoothed version for display only!
pub struct Clock {
    /// This is the dt that the Clock tries to archive with each call of tick.
    target_dt: Duration,
    /// Last time `tick` was called
    last_sys_time: Instant,
    /// Will be calculated in `tick` returns the dt used by the next iteration
    /// of the main loop
    last_dt: Duration,
    /// Summed up `last_dt`
    total_tick_time: Duration,
    // Stats only
    // uses f32 so we have enough precision to display fps values while saving space
    // This is in seconds
    last_dts: VecDeque<NotNan<f32>>,
    last_dts_sorted: Vec<NotNan<f32>>,
    last_busy_dts: VecDeque<NotNan<f32>>,
    stats: ClockStats,
}

pub struct ClockStats {
    /// Busy dt is the part of the tick that we didn't sleep.
    /// e.g. the total tick is 33ms, including 25ms sleeping. then this returns
    /// 8ms
    /// This is in seconds
    pub average_busy_dt: Duration,
    /// avg over the last NUMBER_OF_OLD_DELTAS_KEPT ticks
    pub average_tps: f64,
    /// = 50% percentile
    pub median_tps: f64,
    /// lowest 10% of the frames
    pub percentile_90_tps: f64,
    /// lowest 5% of the frames
    pub percentile_95_tps: f64,
    /// lowest 1% of the frames
    pub percentile_99_tps: f64,
}

const NUMBER_OF_OLD_DELTAS_KEPT: usize = 100;
const NUMBER_OF_DELTAS_COMPARED: usize = 5;

impl Clock {
    pub fn new(target_dt: Duration) -> Self {
        Self {
            target_dt,
            last_sys_time: Instant::now(),
            last_dt: target_dt,
            total_tick_time: Duration::default(),
            last_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
            last_dts_sorted: Vec::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
            last_busy_dts: VecDeque::with_capacity(NUMBER_OF_OLD_DELTAS_KEPT),
            stats: ClockStats::new(&[], &VecDeque::new()),
        }
    }

    pub fn set_target_dt(&mut self, target_dt: Duration) { self.target_dt = target_dt; }

    pub fn stats(&self) -> &ClockStats { &self.stats }

    pub fn dt(&self) -> Duration { self.last_dt }

    pub fn get_stable_dt(&self) -> Duration {
        if self.last_dts.len() >= NUMBER_OF_DELTAS_COMPARED {
            // Take the median of the last few tick times
            let mut dts = [0.0; NUMBER_OF_DELTAS_COMPARED];
            for (i, dt) in self
                .last_dts
                .iter()
                .rev()
                .take(NUMBER_OF_DELTAS_COMPARED)
                .enumerate()
            {
                dts[i] = **dt;
            }
            dts.sort_by_key(|x| ordered_float::OrderedFloat(*x));
            let stable_dt = Duration::from_secs_f32(dts[NUMBER_OF_DELTAS_COMPARED / 2]);

            if self.last_dt > 2 * stable_dt {
                tracing::trace!(?self.last_dt, ?self.total_tick_time, "lag spike detected, unusually slow tick");
                stable_dt
            } else {
                self.last_dt
            }
        } else {
            self.last_dt
        }
    }

    /// Do not modify without asking @xMAC94x first!
    pub fn tick(&mut self) {
        span!(_guard, "tick", "Clock::tick");
        span!(guard, "clock work");
        let current_sys_time = Instant::now();
        let busy_delta = current_sys_time.duration_since(self.last_sys_time);
        // Maintain TPS
        self.last_dts_sorted = self.last_dts.iter().copied().collect();
        self.last_dts_sorted.sort_unstable();
        self.stats = ClockStats::new(&self.last_dts_sorted, &self.last_busy_dts);
        drop(guard);
        // Attempt to sleep to fill the gap.
        if let Some(sleep_dur) = self.target_dt.checked_sub(busy_delta) {
            spin_sleep::sleep(sleep_dur);
        }

        let after_sleep_sys_time = Instant::now();
        self.last_dt = after_sleep_sys_time.duration_since(self.last_sys_time);
        if self.last_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
            self.last_dts.pop_front();
        }
        if self.last_busy_dts.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
            self.last_busy_dts.pop_front();
        }
        self.last_dts.push_back(
            NotNan::new(self.last_dt.as_secs_f32())
                .expect("Duration::as_secs_f32 never returns NaN"),
        );
        self.last_busy_dts.push_back(
            NotNan::new(busy_delta.as_secs_f32()).expect("Duration::as_secs_f32 never returns NaN"),
        );
        self.total_tick_time += self.last_dt;
        self.last_sys_time = after_sleep_sys_time;
    }
}

impl ClockStats {
    fn new(sorted: &[NotNan<f32>], busy_dt_list: &VecDeque<NotNan<f32>>) -> Self {
        let average_frame_time =
            sorted.iter().sum::<NotNan<f32>>().into_inner() / sorted.len().max(1) as f32;

        let average_busy_dt = busy_dt_list.iter().sum::<NotNan<f32>>().into_inner()
            / busy_dt_list.len().max(1) as f32;

        let average_tps = 1.0 / average_frame_time as f64;
        let (median_tps, percentile_90_tps, percentile_95_tps, percentile_99_tps) =
            if sorted.len() >= NUMBER_OF_OLD_DELTAS_KEPT {
                let median_frame_time = *sorted[sorted.len() / 2];
                let percentile_90_frame_time =
                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.1) as usize];
                let percentile_95_frame_time =
                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.05) as usize];
                let percentile_99_frame_time =
                    *sorted[(NUMBER_OF_OLD_DELTAS_KEPT as f32 * 0.01) as usize];

                let median_tps = 1.0 / median_frame_time as f64;
                let percentile_90_tps = 1.0 / percentile_90_frame_time as f64;
                let percentile_95_tps = 1.0 / percentile_95_frame_time as f64;
                let percentile_99_tps = 1.0 / percentile_99_frame_time as f64;
                (
                    median_tps,
                    percentile_90_tps,
                    percentile_95_tps,
                    percentile_99_tps,
                )
            } else {
                let avg_tps = 1.0 / average_busy_dt as f64;
                (avg_tps, avg_tps, avg_tps, avg_tps)
            };

        Self {
            average_busy_dt: Duration::from_secs_f32(average_busy_dt),
            average_tps,
            median_tps,
            percentile_90_tps,
            percentile_95_tps,
            percentile_99_tps,
        }
    }
}