twilight_gateway/
latency.rs

1//! Statistics about the latency of a shard, useful for debugging.
2
3use std::time::{Duration, Instant};
4
5/// [`Shard`]'s gateway connection latency.
6///
7/// Measures the difference between sending a heartbeat and receiving an
8/// acknowledgement, also known as a heartbeat period. Spurious heartbeat
9/// acknowledgements are ignored.
10///
11/// May be obtained via [`Shard::latency`].
12///
13/// [`Shard`]: crate::Shard
14/// [`Shard::latency`]: crate::Shard::latency
15#[derive(Clone, Debug)]
16pub struct Latency {
17    /// Sum of recorded latencies.
18    #[allow(clippy::struct_field_names)]
19    latency_sum: Duration,
20    /// Number of recorded heartbeat periods.
21    periods: u32,
22    /// When the last heartbeat received an acknowledgement.
23    received: Option<Instant>,
24    /// List of most recent latencies.
25    recent: [Duration; Self::RECENT_LEN],
26    /// When the last heartbeat was sent.
27    sent: Option<Instant>,
28}
29
30impl Latency {
31    /// Number of recent latencies to store.
32    const RECENT_LEN: usize = 5;
33
34    /// Create a new instance for tracking shard latency.
35    pub(crate) const fn new() -> Self {
36        Self {
37            latency_sum: Duration::ZERO,
38            periods: 0,
39            received: None,
40            recent: [Duration::MAX; Self::RECENT_LEN],
41            sent: None,
42        }
43    }
44
45    /// Average latency.
46    ///
47    /// For example, a reasonable value for this may be between 10 to 100
48    /// milliseconds depending on the network connection and physical location.
49    ///
50    /// Returns [`None`] if no heartbeat periods have been recorded.
51    pub const fn average(&self) -> Option<Duration> {
52        self.latency_sum.checked_div(self.periods)
53    }
54
55    /// Number of recorded heartbeat periods.
56    pub const fn periods(&self) -> u32 {
57        self.periods
58    }
59
60    /// Most recent latencies from newest to oldest.
61    pub fn recent(&self) -> &[Duration] {
62        // We use the sentinel value of Duration::MAX since using
63        // `Duration::ZERO` would cause tests depending on elapsed time on fast
64        // CPUs to flake. See issue #2114.
65        let maybe_zero_idx = self
66            .recent
67            .iter()
68            .position(|duration| *duration == Duration::MAX);
69
70        &self.recent[0..maybe_zero_idx.unwrap_or(Self::RECENT_LEN)]
71    }
72
73    /// When the last heartbeat received an acknowledgement.
74    pub const fn received(&self) -> Option<Instant> {
75        self.received
76    }
77
78    /// When the last heartbeat was sent.
79    pub const fn sent(&self) -> Option<Instant> {
80        self.sent
81    }
82
83    /// Record that a heartbeat acknowledgement was received, completing the
84    /// period.
85    ///
86    /// The current time is subtracted against when the last heartbeat
87    /// [was sent] to calculate the heartbeat period's latency.
88    ///
89    /// # Panics
90    ///
91    /// Panics if the period is already complete or has not begun.
92    ///
93    /// [was sent]: Self::record_sent
94    #[track_caller]
95    pub(crate) fn record_received(&mut self) {
96        debug_assert!(self.received.is_none(), "period completed multiple times");
97
98        let now = Instant::now();
99        let period_latency = now - self.sent.expect("period has not begun");
100        self.received = Some(now);
101        self.periods += 1;
102
103        self.latency_sum += period_latency;
104        self.recent.copy_within(..Self::RECENT_LEN - 1, 1);
105        self.recent[0] = period_latency;
106    }
107
108    /// Record that a heartbeat was sent, beginning a new period.
109    ///
110    /// The current time is stored to be used in [`record_received`].
111    ///
112    /// [`record_received`]: Self::record_received
113    pub(crate) fn record_sent(&mut self) {
114        self.received = None;
115        self.sent = Some(Instant::now());
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::Latency;
122    use static_assertions::assert_impl_all;
123    use std::{fmt::Debug, time::Duration};
124
125    assert_impl_all!(Latency: Clone, Debug, Send, Sync);
126
127    const fn default_latency() -> Latency {
128        Latency {
129            latency_sum: Duration::from_millis(510),
130            periods: 17,
131            received: None,
132            recent: [
133                Duration::from_millis(20),
134                Duration::from_millis(25),
135                Duration::from_millis(30),
136                Duration::from_millis(35),
137                Duration::from_millis(40),
138            ],
139            sent: None,
140        }
141    }
142
143    #[test]
144    fn public_api() {
145        let latency = default_latency();
146        assert_eq!(latency.average(), Some(Duration::from_millis(30)));
147        assert_eq!(latency.periods(), 17);
148        assert!(latency.received().is_none());
149        assert!(latency.sent().is_none());
150
151        assert_eq!(latency.recent.len(), Latency::RECENT_LEN);
152        let mut iter = latency.recent().iter();
153        assert_eq!(iter.next(), Some(&Duration::from_millis(20)));
154        assert_eq!(iter.next_back(), Some(&Duration::from_millis(40)));
155        assert_eq!(iter.next(), Some(&Duration::from_millis(25)));
156        assert_eq!(iter.next(), Some(&Duration::from_millis(30)));
157        assert_eq!(iter.next_back(), Some(&Duration::from_millis(35)));
158        assert!(iter.next().is_none());
159        assert!(iter.next_back().is_none());
160    }
161
162    /// Test that only recent values up to and not including the sentinel value
163    /// are returned.
164    #[test]
165    fn recent() {
166        // Assert that when all recent latencies are the sentinel value then an
167        // empty slice is returned.
168        let no_recents = Latency {
169            latency_sum: Duration::ZERO,
170            periods: 0,
171            received: None,
172            recent: [Duration::MAX; Latency::RECENT_LEN],
173            sent: None,
174        };
175        assert!(no_recents.recent().is_empty());
176
177        // Assert that when only some recent latencies aren't the sentinel value
178        // then a partial slice is returned.
179        let partial = Latency {
180            recent: [
181                Duration::from_millis(40),
182                Duration::from_millis(50),
183                Duration::MAX,
184                Duration::MAX,
185                Duration::MAX,
186            ],
187            ..no_recents
188        };
189        assert_eq!(
190            [Duration::from_millis(40), Duration::from_millis(50)],
191            partial.recent()
192        );
193
194        // Assert that when all recent latencies aren't the sentinel value then
195        // the full slice is returned.
196        let full = Latency {
197            recent: [
198                Duration::from_millis(40),
199                Duration::from_millis(50),
200                Duration::from_millis(60),
201                Duration::from_millis(70),
202                Duration::from_millis(60),
203            ],
204            ..no_recents
205        };
206        assert_eq!(
207            [
208                Duration::from_millis(40),
209                Duration::from_millis(50),
210                Duration::from_millis(60),
211                Duration::from_millis(70),
212                Duration::from_millis(60),
213            ],
214            full.recent()
215        );
216    }
217
218    #[test]
219    fn record_period() {
220        let mut latency = Latency::new();
221        assert_eq!(latency.periods(), 0);
222        assert!(latency.received().is_none());
223        assert!(latency.sent().is_none());
224        assert!(latency.recent().is_empty());
225
226        latency.record_sent();
227        assert_eq!(latency.periods(), 0);
228        assert!(latency.received().is_none());
229        assert!(latency.sent().is_some());
230
231        latency.record_received();
232        assert_eq!(latency.periods(), 1);
233        assert!(latency.received().is_some());
234        assert!(latency.sent().is_some());
235        assert_eq!(latency.recent().len(), 1);
236
237        latency.record_sent();
238        assert_eq!(latency.periods(), 1);
239        assert!(latency.received().is_none());
240        assert!(latency.sent().is_some());
241        assert_eq!(latency.recent().len(), 1);
242    }
243
244    #[test]
245    #[should_panic(expected = "period completed multiple times")]
246    fn record_completed_period() {
247        let mut latency = Latency::new();
248        latency.record_sent();
249        latency.record_received();
250        latency.record_received();
251    }
252
253    #[test]
254    #[should_panic(expected = "period has not begun")]
255    fn record_not_begun_period() {
256        let mut latency = Latency::new();
257        latency.record_received();
258    }
259}