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    /// Creates a timestamp from elapsed Quanta from the 0 timestamp.
63    fn from_quanta(quanta: Quanta) -> Self;
64}
65
66impl TimestampExt for Timestamp {
67    fn quantize(self) -> Quanta {
68        (self - Timestamp::from_nanos(0)).into_quanta()
69    }
70
71    fn from_quanta(quanta: Quanta) -> Self {
72        Timestamp::from_nanos(0) + Duration::from_quanta(quanta)
73    }
74}
75
76/// A vector in time.
77pub type Duration = zx::BootDuration;
78
79pub trait DurationExt {
80    /// The unit duration.
81    ///
82    /// Durations are expressed in terms of this unit and so cannot express periods beyond this
83    /// resolution.
84    const QUANTUM: Self;
85
86    /// Constructs a `Duration` from quanta.
87    fn from_quanta(quanta: Quanta) -> Self;
88
89    /// Converts the duration into quanta.
90    fn into_quanta(self) -> Quanta;
91}
92
93impl DurationExt for Duration {
94    const QUANTUM: Self = Duration::from_seconds(1);
95
96    fn from_quanta(quanta: Quanta) -> Self {
97        Duration::from_seconds(quanta)
98    }
99
100    fn into_quanta(self) -> Quanta {
101        self.into_seconds()
102    }
103}
104
105/// An arrow in time.
106///
107/// A `Tick` represents a directed point or relative displacement in time with respect to some
108/// reference time.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub struct Tick {
111    start: Timestamp,
112    end: Timestamp,
113    last_sample_timestamp: Option<Timestamp>,
114}
115
116impl Tick {
117    /// Quantizes the tick into start and end points in time, in that order.
118    pub fn quantize(self) -> (Quanta, Quanta) {
119        let start = self.start.quantize();
120        let end = self.end.quantize();
121        (start, end)
122    }
123
124    /// Return true if the sampling period at the start time of the tick has a sample.
125    /// Otherwise, return false.
126    pub fn start_has_sample(self, max_sampling_period: Quanta) -> bool {
127        let start = self.start.quantize();
128        match self.last_sample_timestamp {
129            Some(last_sample_timestamp) => {
130                let sample_time = last_sample_timestamp.quantize();
131                (start / max_sampling_period) == (sample_time / max_sampling_period)
132            }
133            None => false,
134        }
135    }
136}
137
138/// A monotonic reference point in time that advances when a sample is observed or
139/// an interpolation occurs.
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub struct ObservationTime {
142    pub(crate) last_update_timestamp: Timestamp,
143    pub(crate) last_sample_timestamp: Option<Timestamp>,
144}
145
146impl ObservationTime {
147    /// Advances the observation time to the given point in time.
148    ///
149    /// Returns a [`Tick`] that describes the displacement in time.
150    ///
151    /// # Errors
152    ///
153    /// Returns an error if the given point in time is not monotonic with respect to the current
154    /// observation time.
155    ///
156    /// [`Tick`]: crate::experimental::clock::Tick
157    pub fn tick(
158        &mut self,
159        timestamp: Timestamp,
160        is_sample: bool,
161    ) -> Result<Tick, MonotonicityError> {
162        if timestamp < self.last_update_timestamp {
163            Err(MonotonicityError)
164        } else {
165            let new_observation_time = ObservationTime {
166                last_update_timestamp: timestamp,
167                last_sample_timestamp: if is_sample {
168                    Some(timestamp)
169                } else {
170                    self.last_sample_timestamp
171                },
172            };
173            let prev = mem::replace(self, new_observation_time);
174            Ok(Tick {
175                start: prev.last_update_timestamp,
176                end: timestamp,
177                last_sample_timestamp: prev.last_sample_timestamp,
178            })
179        }
180    }
181
182    pub(crate) fn at(last_update_timestamp: Timestamp) -> Self {
183        Self { last_update_timestamp, last_sample_timestamp: None }
184    }
185}
186
187impl Default for ObservationTime {
188    fn default() -> Self {
189        ObservationTime { last_update_timestamp: Timestamp::now(), last_sample_timestamp: None }
190    }
191}
192
193/// Data associated with a [point in time][`Timestamp`], such as a sample or event.
194///
195/// [`Timestamp`]: crate::experimental::clock::Timestamp
196#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
197pub struct Timed<T> {
198    timestamp: Timestamp,
199    inner: T,
200}
201
202impl<T> Timed<T> {
203    /// Constructs a `Timed` from data at the [current time][`Timestamp::now`].
204    ///
205    /// [`Timestamp::now`]: crate::experimental::clock::Timestamp::now
206    pub fn now(inner: T) -> Self {
207        Timed { timestamp: Timestamp::now(), inner }
208    }
209
210    /// Constructs a `Timed` from data at the given point in time.
211    pub fn at(inner: T, timestamp: impl Into<Timestamp>) -> Self {
212        Timed { timestamp: timestamp.into(), inner }
213    }
214
215    /// Maps a `Timed<T>` to a `Timed<T>` by applying the given function to the inner data.
216    pub fn map<U, F>(self, f: F) -> Timed<U>
217    where
218        F: FnOnce(T) -> U,
219    {
220        let Timed { timestamp, inner } = self;
221        Timed { timestamp, inner: f(inner) }
222    }
223
224    /// Gets the point in time associated with the data.
225    pub fn timestamp(&self) -> &Timestamp {
226        &self.timestamp
227    }
228
229    /// Gets the inner data.
230    pub fn inner(&self) -> &T {
231        &self.inner
232    }
233}
234
235impl<T> Timed<Option<T>> {
236    pub fn transpose(self) -> Option<Timed<T>> {
237        let Timed { timestamp, inner } = self;
238        inner.map(|inner| Timed { timestamp, inner })
239    }
240}
241
242impl<T, E> Timed<Result<T, E>> {
243    pub fn transpose(self) -> Result<Timed<T>, E> {
244        let Timed { timestamp, inner } = self;
245        inner.map(|inner| Timed { timestamp, inner })
246    }
247}
248
249impl<T> From<Timed<T>> for (Timestamp, T) {
250    fn from(timed: Timed<T>) -> Self {
251        let Timed { timestamp, inner: sample } = timed;
252        (timestamp, sample)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use crate::experimental::clock::{
259        Duration, DurationExt as _, MonotonicityError, ObservationTime, QuantaExt as _, Tick,
260        Timed, Timestamp, TimestampExt as _,
261    };
262    use fuchsia_async as fasync;
263
264    #[test]
265    fn quantize() {
266        let timestamp = Timestamp::from_nanos(0) + Duration::QUANTUM;
267        assert_eq!(timestamp.quantize(), 1);
268
269        let tick = Tick {
270            start: timestamp,
271            end: timestamp + Duration::from_quanta(3),
272            last_sample_timestamp: Some(Timestamp::from_nanos(-999)),
273        };
274        assert_eq!(tick.quantize(), (1, 4));
275    }
276
277    #[test]
278    fn start_has_sample() {
279        let tick = Tick {
280            start: Timestamp::from_nanos(9_000_000_000),
281            end: Timestamp::from_nanos(12_000_000_000),
282            last_sample_timestamp: Some(Timestamp::from_nanos(8_000_000_000)),
283        };
284        assert!(tick.start_has_sample(10));
285
286        let tick = Tick {
287            start: Timestamp::from_nanos(10_000_000_000),
288            end: Timestamp::from_nanos(13_000_000_000),
289            last_sample_timestamp: Some(Timestamp::from_nanos(8_000_000_000)),
290        };
291        assert!(!tick.start_has_sample(10));
292    }
293
294    #[test]
295    fn tick() {
296        const ZERO: Timestamp = Timestamp::from_nanos(0);
297        const MINUTE_ONE: Timestamp = Timestamp::from_nanos(60_000_000_000);
298        const MINUTE_THREE: Timestamp = Timestamp::from_nanos(180_000_000_000);
299        let mut last = ObservationTime { last_update_timestamp: ZERO, last_sample_timestamp: None };
300
301        let tick = last.tick(MINUTE_ONE, true).unwrap();
302        let expected_tick = Tick { start: ZERO, end: MINUTE_ONE, last_sample_timestamp: None };
303        let expected_last = ObservationTime {
304            last_update_timestamp: MINUTE_ONE,
305            last_sample_timestamp: Some(MINUTE_ONE),
306        };
307        assert_eq!(tick, expected_tick);
308        assert_eq!(last, expected_last);
309
310        let tick = last.tick(MINUTE_THREE, false).unwrap();
311        let expected_tick =
312            Tick { start: MINUTE_ONE, end: MINUTE_THREE, last_sample_timestamp: Some(MINUTE_ONE) };
313        let expected_last = ObservationTime {
314            last_update_timestamp: MINUTE_THREE,
315            last_sample_timestamp: Some(MINUTE_ONE),
316        };
317        assert_eq!(tick, expected_tick);
318        assert_eq!(last, expected_last);
319
320        let result = last.tick(MINUTE_ONE, false);
321        assert_eq!(result, Err(MonotonicityError));
322    }
323
324    #[test]
325    fn fmt_display_quanta() {
326        assert_eq!(0i64.into_nearest_unit_display().to_string(), "0d");
327        assert_eq!(1i64.into_nearest_unit_display().to_string(), "1s");
328        assert_eq!(5i64.into_nearest_unit_display().to_string(), "5s");
329        assert_eq!(60i64.into_nearest_unit_display().to_string(), "1m");
330        assert_eq!(65i64.into_nearest_unit_display().to_string(), "65s");
331        assert_eq!(120i64.into_nearest_unit_display().to_string(), "2m");
332        assert_eq!(3600i64.into_nearest_unit_display().to_string(), "1h");
333        assert_eq!(3605i64.into_nearest_unit_display().to_string(), "3605s");
334        assert_eq!(86400i64.into_nearest_unit_display().to_string(), "1d");
335    }
336
337    #[test]
338    fn timed_now_then_timestamp_eq_executor_now() {
339        const NOW: i64 = 3_000_000_000;
340
341        let executor = fasync::TestExecutor::new_with_fake_time();
342        executor.set_fake_time(fasync::MonotonicInstant::from_nanos(NOW));
343
344        let timed = Timed::now(());
345        let (timestamp, _) = timed.into();
346        assert_eq!(timestamp, Timestamp::from_nanos(NOW));
347    }
348
349    #[test]
350    fn timed_at_point_then_timestamp_eq_point() {
351        const AT: Timestamp = Timestamp::from_nanos(1);
352
353        let timed = Timed::at((), AT);
354        let (timestamp, _) = timed.into();
355        assert_eq!(timestamp, AT);
356    }
357
358    #[test]
359    fn map_timed_then_data_eq_output() {
360        let timed = Timed::at(9i64, Timestamp::from_nanos(1)).map(|n| n * 7);
361        let (_, n) = timed.into();
362        assert_eq!(n, 63);
363    }
364
365    #[test]
366    fn transpose_timed_then_output_is_congruent() {
367        const AT: Timestamp = Timestamp::from_nanos(1);
368
369        assert!(Timed::at(None::<()>, AT).transpose().is_none());
370        assert!(Timed::at(Some(()), AT).transpose().is_some());
371    }
372}