windowed_stats/experimental/
clock.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// This module provides a thin abstraction over chronological types using type definitions. This
6// can be further abstracted through dedicated types and more sophisticated APIs that support
7// different clock implementations. At time of writing, it is tightly integrated with
8// `fuchsia_async` and quantizes time to 1s.
9
10//! Monotonic clock and chronological APIs.
11
12use num::Integer;
13use std::fmt::Display;
14use std::mem;
15use thiserror::Error;
16
17/// Monotonic time error.
18///
19/// Describes temporal errors that occur when points in time are unexpectedly less (earlier) than
20/// some reference time.
21#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
22#[error("non-monotonic tick or timestamp")]
23pub struct MonotonicityError;
24
25/// Unit of time.
26///
27/// Time is expressed and quantized as integer multiples of this basis unit.
28pub type Quanta = i64;
29
30pub trait QuantaExt {
31    /// Converts the quanta into a `Display` that describes a duration in the nearest (largest)
32    /// units.
33    fn into_nearest_unit_display(self) -> impl Display;
34}
35
36impl QuantaExt for Quanta {
37    fn into_nearest_unit_display(self) -> impl Display {
38        const SECONDS_PER_MINUTE: i64 = 60;
39        const SECONDS_PER_HOUR: i64 = SECONDS_PER_MINUTE * 60;
40        const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * 24;
41
42        match (
43            self.div_rem(&SECONDS_PER_DAY),
44            self.div_rem(&SECONDS_PER_HOUR),
45            self.div_rem(&SECONDS_PER_MINUTE),
46        ) {
47            ((days, 0), _, _) => format!("{}d", days),
48            (_, (hours, 0), _) => format!("{}h", hours),
49            (_, _, (minutes, 0)) => format!("{}m", minutes),
50            _ => format!("{}s", self),
51        }
52    }
53}
54
55/// A point in time.
56pub type Timestamp = fuchsia_async::BootInstant;
57
58pub trait TimestampExt {
59    /// Calculates the number of quanta between zero and the current timestamp.
60    fn quantize(self) -> Quanta;
61}
62
63impl TimestampExt for Timestamp {
64    fn quantize(self) -> Quanta {
65        (self - Timestamp::from_nanos(0)).into_quanta()
66    }
67}
68
69/// A vector in time.
70pub type Duration = zx::BootDuration;
71
72pub trait DurationExt {
73    /// The unit duration.
74    ///
75    /// Durations are expressed in terms of this unit and so cannot express periods beyond this
76    /// resolution.
77    const QUANTUM: Self;
78
79    /// Constructs a `Duration` from quanta.
80    fn from_quanta(quanta: Quanta) -> Self;
81
82    /// Converts the duration into quanta.
83    fn into_quanta(self) -> Quanta;
84}
85
86impl DurationExt for Duration {
87    const QUANTUM: Self = Duration::from_seconds(1);
88
89    fn from_quanta(quanta: Quanta) -> Self {
90        Duration::from_seconds(quanta)
91    }
92
93    fn into_quanta(self) -> Quanta {
94        self.into_seconds()
95    }
96}
97
98/// An arrow in time.
99///
100/// A `Tick` represents a directed point or relative displacement in time with respect to some
101/// reference time.
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub struct Tick {
104    start: Timestamp,
105    end: Timestamp,
106    last_sample_timestamp: Option<Timestamp>,
107}
108
109impl Tick {
110    /// Quantizes the tick into start and end points in time, in that order.
111    pub fn quantize(self) -> (Quanta, Quanta) {
112        let start = self.start.quantize();
113        let end = self.end.quantize();
114        (start, end)
115    }
116
117    /// Return true if the sampling period at the start time of the tick has a sample.
118    /// Otherwise, return false.
119    pub fn start_has_sample(self, max_sampling_period: Quanta) -> bool {
120        let start = self.start.quantize();
121        match self.last_sample_timestamp {
122            Some(last_sample_timestamp) => {
123                let sample_time = last_sample_timestamp.quantize();
124                (start / max_sampling_period) == (sample_time / max_sampling_period)
125            }
126            None => false,
127        }
128    }
129}
130
131/// A monotonic reference point in time that advances when a sample is observed or
132/// an interpolation occurs.
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub struct ObservationTime {
135    pub(crate) last_update_timestamp: Timestamp,
136    pub(crate) last_sample_timestamp: Option<Timestamp>,
137}
138
139impl ObservationTime {
140    /// Advances the observation time to the given point in time.
141    ///
142    /// Returns a [`Tick`] that describes the displacement in time.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if the given point in time is not monotonic with respect to the current
147    /// observation time.
148    ///
149    /// [`Tick`]: crate::experimental::clock::Tick
150    pub fn tick(
151        &mut self,
152        timestamp: Timestamp,
153        is_sample: bool,
154    ) -> Result<Tick, MonotonicityError> {
155        if timestamp < self.last_update_timestamp {
156            Err(MonotonicityError)
157        } else {
158            let new_observation_time = ObservationTime {
159                last_update_timestamp: timestamp,
160                last_sample_timestamp: if is_sample {
161                    Some(timestamp)
162                } else {
163                    self.last_sample_timestamp
164                },
165            };
166            let prev = mem::replace(self, new_observation_time);
167            Ok(Tick {
168                start: prev.last_update_timestamp,
169                end: timestamp,
170                last_sample_timestamp: prev.last_sample_timestamp,
171            })
172        }
173    }
174}
175
176impl Default for ObservationTime {
177    fn default() -> Self {
178        ObservationTime { last_update_timestamp: Timestamp::now(), last_sample_timestamp: None }
179    }
180}
181
182/// Data associated with a [point in time][`Timestamp`], such as a sample or event.
183///
184/// [`Timestamp`]: crate::experimental::clock::Timestamp
185#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
186pub struct Timed<T> {
187    timestamp: Timestamp,
188    inner: T,
189}
190
191impl<T> Timed<T> {
192    /// Constructs a `Timed` from data at the [current time][`Timestamp::now`].
193    ///
194    /// [`Timestamp::now`]: crate::experimental::clock::Timestamp::now
195    pub fn now(inner: T) -> Self {
196        Timed { timestamp: Timestamp::now(), inner }
197    }
198
199    /// Constructs a `Timed` from data at the given point in time.
200    pub fn at(inner: T, timestamp: impl Into<Timestamp>) -> Self {
201        Timed { timestamp: timestamp.into(), inner }
202    }
203
204    /// Maps a `Timed<T>` to a `Timed<T>` by applying the given function to the inner data.
205    pub fn map<U, F>(self, f: F) -> Timed<U>
206    where
207        F: FnOnce(T) -> U,
208    {
209        let Timed { timestamp, inner } = self;
210        Timed { timestamp, inner: f(inner) }
211    }
212
213    /// Gets the point in time associated with the data.
214    pub fn timestamp(&self) -> &Timestamp {
215        &self.timestamp
216    }
217
218    /// Gets the inner data.
219    pub fn inner(&self) -> &T {
220        &self.inner
221    }
222}
223
224impl<T> Timed<Option<T>> {
225    pub fn transpose(self) -> Option<Timed<T>> {
226        let Timed { timestamp, inner } = self;
227        inner.map(|inner| Timed { timestamp, inner })
228    }
229}
230
231impl<T, E> Timed<Result<T, E>> {
232    pub fn transpose(self) -> Result<Timed<T>, E> {
233        let Timed { timestamp, inner } = self;
234        inner.map(|inner| Timed { timestamp, inner })
235    }
236}
237
238impl<T> From<Timed<T>> for (Timestamp, T) {
239    fn from(timed: Timed<T>) -> Self {
240        let Timed { timestamp, inner: sample } = timed;
241        (timestamp, sample)
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use crate::experimental::clock::{
248        Duration, DurationExt as _, MonotonicityError, ObservationTime, QuantaExt as _, Tick,
249        Timed, Timestamp, TimestampExt as _,
250    };
251    use fuchsia_async as fasync;
252
253    #[test]
254    fn quantize() {
255        let timestamp = Timestamp::from_nanos(0) + Duration::QUANTUM;
256        assert_eq!(timestamp.quantize(), 1);
257
258        let tick = Tick {
259            start: timestamp,
260            end: timestamp + Duration::from_quanta(3),
261            last_sample_timestamp: Some(Timestamp::from_nanos(-999)),
262        };
263        assert_eq!(tick.quantize(), (1, 4));
264    }
265
266    #[test]
267    fn start_has_sample() {
268        let tick = Tick {
269            start: Timestamp::from_nanos(9_000_000_000),
270            end: Timestamp::from_nanos(12_000_000_000),
271            last_sample_timestamp: Some(Timestamp::from_nanos(8_000_000_000)),
272        };
273        assert!(tick.start_has_sample(10));
274
275        let tick = Tick {
276            start: Timestamp::from_nanos(10_000_000_000),
277            end: Timestamp::from_nanos(13_000_000_000),
278            last_sample_timestamp: Some(Timestamp::from_nanos(8_000_000_000)),
279        };
280        assert!(!tick.start_has_sample(10));
281    }
282
283    #[test]
284    fn tick() {
285        const ZERO: Timestamp = Timestamp::from_nanos(0);
286        const MINUTE_ONE: Timestamp = Timestamp::from_nanos(60_000_000_000);
287        const MINUTE_THREE: Timestamp = Timestamp::from_nanos(180_000_000_000);
288        let mut last = ObservationTime { last_update_timestamp: ZERO, last_sample_timestamp: None };
289
290        let tick = last.tick(MINUTE_ONE, true).unwrap();
291        let expected_tick = Tick { start: ZERO, end: MINUTE_ONE, last_sample_timestamp: None };
292        let expected_last = ObservationTime {
293            last_update_timestamp: MINUTE_ONE,
294            last_sample_timestamp: Some(MINUTE_ONE),
295        };
296        assert_eq!(tick, expected_tick);
297        assert_eq!(last, expected_last);
298
299        let tick = last.tick(MINUTE_THREE, false).unwrap();
300        let expected_tick =
301            Tick { start: MINUTE_ONE, end: MINUTE_THREE, last_sample_timestamp: Some(MINUTE_ONE) };
302        let expected_last = ObservationTime {
303            last_update_timestamp: MINUTE_THREE,
304            last_sample_timestamp: Some(MINUTE_ONE),
305        };
306        assert_eq!(tick, expected_tick);
307        assert_eq!(last, expected_last);
308
309        let result = last.tick(MINUTE_ONE, false);
310        assert_eq!(result, Err(MonotonicityError));
311    }
312
313    #[test]
314    fn fmt_display_quanta() {
315        assert_eq!(0i64.into_nearest_unit_display().to_string(), "0d");
316        assert_eq!(1i64.into_nearest_unit_display().to_string(), "1s");
317        assert_eq!(5i64.into_nearest_unit_display().to_string(), "5s");
318        assert_eq!(60i64.into_nearest_unit_display().to_string(), "1m");
319        assert_eq!(65i64.into_nearest_unit_display().to_string(), "65s");
320        assert_eq!(120i64.into_nearest_unit_display().to_string(), "2m");
321        assert_eq!(3600i64.into_nearest_unit_display().to_string(), "1h");
322        assert_eq!(3605i64.into_nearest_unit_display().to_string(), "3605s");
323        assert_eq!(86400i64.into_nearest_unit_display().to_string(), "1d");
324    }
325
326    #[test]
327    fn timed_now_then_timestamp_eq_executor_now() {
328        const NOW: i64 = 3_000_000_000;
329
330        let executor = fasync::TestExecutor::new_with_fake_time();
331        executor.set_fake_time(fasync::MonotonicInstant::from_nanos(NOW));
332
333        let timed = Timed::now(());
334        let (timestamp, _) = timed.into();
335        assert_eq!(timestamp, Timestamp::from_nanos(NOW));
336    }
337
338    #[test]
339    fn timed_at_point_then_timestamp_eq_point() {
340        const AT: Timestamp = Timestamp::from_nanos(1);
341
342        let timed = Timed::at((), AT);
343        let (timestamp, _) = timed.into();
344        assert_eq!(timestamp, AT);
345    }
346
347    #[test]
348    fn map_timed_then_data_eq_output() {
349        let timed = Timed::at(9i64, Timestamp::from_nanos(1)).map(|n| n * 7);
350        let (_, n) = timed.into();
351        assert_eq!(n, 63);
352    }
353
354    #[test]
355    fn transpose_timed_then_output_is_congruent() {
356        const AT: Timestamp = Timestamp::from_nanos(1);
357
358        assert!(Timed::at(None::<()>, AT).transpose().is_none());
359        assert!(Timed::at(Some(()), AT).transpose().is_some());
360    }
361}