Skip to main content

nifty_download/
speed.rs

1//! How fast a download is going, averaged over its last moments so the number doesn't
2//! jump with every chunk.
3
4use std::collections::VecDeque;
5use std::time::{Duration, Instant};
6
7/// How far back the average reaches.
8const WINDOW: Duration = Duration::from_secs(2);
9const NANOS_PER_SECOND: u128 = 1_000_000_000;
10
11#[derive(Debug, Default)]
12pub(crate) struct SpeedMeter {
13    /// When each count was taken, oldest first, with the bytes received by then.
14    samples: VecDeque<(Instant, u64)>,
15}
16
17impl SpeedMeter {
18    /// Notes that `received` bytes have arrived by `now`, and returns the bytes per second
19    /// over the window up to it.
20    pub(crate) fn record(&mut self, now: Instant, received: u64) -> u64 {
21        self.samples.push_back((now, received));
22        // Keep the newest sample older than the window, so the average spans all of it.
23        while self
24            .samples
25            .get(1)
26            .is_some_and(|&(taken, _)| now.duration_since(taken) >= WINDOW)
27        {
28            self.samples.pop_front();
29        }
30        let Some(&(since, from)) = self.samples.front() else {
31            return 0;
32        };
33        let elapsed = now.duration_since(since).as_nanos();
34        if elapsed == 0 {
35            return 0;
36        }
37        let bytes = u128::from(received.saturating_sub(from));
38        u64::try_from(bytes * NANOS_PER_SECOND / elapsed).unwrap_or(u64::MAX)
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45
46    #[test]
47    fn averages_over_the_last_two_seconds() {
48        let start = Instant::now();
49        let at = |millis| start + Duration::from_millis(millis);
50        let mut meter = SpeedMeter::default();
51        assert_eq!(meter.record(at(0), 0), 0, "one sample has no speed yet");
52        assert_eq!(meter.record(at(500), 1_000), 2_000);
53        assert_eq!(meter.record(at(1_000), 2_000), 2_000);
54        assert_eq!(
55            meter.record(at(3_000), 2_000),
56            0,
57            "nothing in the last two seconds"
58        );
59        assert_eq!(
60            meter.record(at(3_250), 2_500),
61            222,
62            "500 bytes over 2.25 seconds"
63        );
64    }
65}