| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #![allow(unused)] |
| |
| use { |
| crate::{enums::FrequencyUpdateError, time_source::Sample}, |
| fuchsia_zircon as zx, |
| std::mem, |
| }; |
| |
| /// The time period over which a set of time samples are collected to update the frequency estimate. |
| const FREQUENCY_ESTIMATION_WINDOW: zx::Duration = zx::Duration::from_hours(24); |
| |
| /// The minimum number of samples that must be received in a frequency estimation window for it to |
| /// be eligible for a frequency estimate. |
| const FREQUENCY_ESTIMATION_MIN_SAMPLES: u32 = 12; |
| |
| /// The factor applied to the current period during the exponentially weighted moving average |
| /// calculation of frequency. |
| const FREQUENCY_ESTIMATION_SMOOTHING: f64 = 0.25; |
| |
| /// Failure modes that may occur while calling `EstimationWindow.add_sample()`. |
| #[derive(Clone, Debug, Eq, PartialEq)] |
| enum AddSampleError { |
| BeforeWindow, |
| AfterWindow, |
| } |
| |
| /// Failure modes that may occur while calling `EstimationWindow.frequency()`. |
| #[derive(Clone, Debug, Eq, PartialEq)] |
| enum GetFrequencyError { |
| InsufficientSamples, |
| PotentialLeapSecond, |
| } |
| |
| impl Into<FrequencyUpdateError> for GetFrequencyError { |
| fn into(self) -> FrequencyUpdateError { |
| match self { |
| Self::InsufficientSamples => FrequencyUpdateError::InsufficientSamples, |
| Self::PotentialLeapSecond => FrequencyUpdateError::PotentialLeapSecond, |
| } |
| } |
| } |
| |
| /// An accumulation of frequency data from a set of `Sample`s within a time window. |
| #[derive(Debug, PartialEq)] |
| struct EstimationWindow { |
| /// The UTC time of the first sample in the window. |
| initial_utc: zx::Time, |
| /// The monotonic time of the first sample in the window. |
| initial_monotonic: zx::Time, |
| /// The number of samples accepted into this window. |
| sample_count: u32, |
| /// The sum of (UTC - initial UTC), in nanoseconds, over all samples. |
| sum_utc: f64, |
| /// The sum of (monotonic - initial monotonic), in nanoseconds, over all samples. |
| sum_monotonic: f64, |
| /// The sum of (monotonic - initial monotonic)^2, in nanoseconds squared, over all samples. |
| sum_monotonic_squared: f64, |
| /// The sum of (UTC - initial UTC)*(monotonic - initial monotonic), in nanoseconds squared, |
| /// over all samples. |
| sum_utc_monotonic: f64, |
| } |
| |
| impl EstimationWindow { |
| /// Construct a new `EstimationWindow` initialized to the supplied sample. |
| fn new(sample: &Sample) -> Self { |
| EstimationWindow { |
| initial_utc: sample.utc, |
| initial_monotonic: sample.monotonic, |
| sample_count: 1, |
| sum_utc: 0.0, |
| sum_monotonic: 0.0, |
| sum_monotonic_squared: 0.0, |
| sum_utc_monotonic: 0.0, |
| } |
| } |
| |
| /// Attempts to add a new sample into this `EstimationWindow`. Returns an error if the sample |
| /// is outside the allowable window defined by the first sample. |
| fn add_sample(&mut self, sample: &Sample) -> Result<(), AddSampleError> { |
| if sample.utc < self.initial_utc { |
| return Err(AddSampleError::BeforeWindow); |
| } else if sample.utc > self.initial_utc + FREQUENCY_ESTIMATION_WINDOW { |
| return Err(AddSampleError::AfterWindow); |
| } |
| |
| let utc = (sample.utc - self.initial_utc).into_nanos() as f64; |
| let monotonic = (sample.monotonic - self.initial_monotonic).into_nanos() as f64; |
| self.sample_count += 1; |
| self.sum_utc += utc; |
| self.sum_monotonic += monotonic; |
| self.sum_monotonic_squared += monotonic * monotonic; |
| self.sum_utc_monotonic += utc * monotonic; |
| Ok(()) |
| } |
| |
| /// Returns the average frequency over the time window, or an error if the window is not |
| /// eligible for some reason. |
| fn frequency(&self) -> Result<f64, GetFrequencyError> { |
| if self.sample_count < FREQUENCY_ESTIMATION_MIN_SAMPLES { |
| return Err(GetFrequencyError::InsufficientSamples); |
| } else if self.overlaps_leap_second() { |
| return Err(GetFrequencyError::PotentialLeapSecond); |
| } |
| |
| let sample_count = self.sample_count as f64; |
| let denominator = |
| self.sum_monotonic_squared - self.sum_monotonic * self.sum_monotonic / sample_count; |
| let numerator = self.sum_utc_monotonic - self.sum_utc * self.sum_monotonic / sample_count; |
| Ok(numerator / denominator) |
| } |
| |
| /// Returns true if this `EstimationWindow` overlaps the 24hour smearing window centered on a |
| /// potential leap second. |
| fn overlaps_leap_second(&self) -> bool { |
| // TODO(jsankey): Implement this method before we encounter a leap second. This will be |
| // Dec 2021 at the earliest. |
| false |
| } |
| } |
| |
| /// Maintains an estimate of the frequency at which UTC time moves with respect to monotonic time |
| /// on this device. |
| /// |
| /// Note that this is the inverse of the local oscillator frequency: Local oscillator frequency |
| /// expresses the length of a monotonic nanosecond with respect to an actual nanosecond in the |
| /// physical universe and we assume that over long periods and excluding leap seconds UTC accurately |
| /// tracks the physical universe. |
| /// |
| /// The frequency estimate is implemented as an exponentially weighted moving average of window |
| /// frequency estimates, each calculated using least squares regression over a 24 hour window. |
| pub struct FrequencyEstimator { |
| /// The estimated frequency. |
| estimate: f64, |
| /// The number of time windows that have been included in this estimate of frequency. |
| window_count: u32, |
| /// The currently active `EstimationWindow`. |
| current_window: EstimationWindow, |
| } |
| |
| impl FrequencyEstimator { |
| /// Construct a new FrequencyEstimator initialized with the supplied sample. |
| pub fn new(sample: &Sample) -> Self { |
| FrequencyEstimator { |
| estimate: 1.0, |
| window_count: 0, |
| current_window: EstimationWindow::new(sample), |
| } |
| } |
| |
| /// Update the estimate to include the supplied sample. Very occasionally this will lead to a |
| /// change in the estimated frequency, in which case the new frequency and the total number of |
| /// windows used in producing this new frequency are returned. |
| pub fn update(&mut self, sample: &Sample) -> Result<Option<(f64, u32)>, FrequencyUpdateError> { |
| match self.current_window.add_sample(sample) { |
| Ok(()) => { |
| // This sample was accepted into the current window so didn't lead to a |
| // frequency change. |
| Ok(None) |
| } |
| Err(AddSampleError::BeforeWindow) => { |
| // If the server had a large step back in time theoretically we might see a UTC |
| // before the window we were accumulating into, in that case just start over. |
| self.current_window = EstimationWindow::new(sample); |
| Err(FrequencyUpdateError::UtcBeforeWindow) |
| } |
| Err(AddSampleError::AfterWindow) => { |
| // This sample was after the current window. Start a new window and if the previous |
| // window was viable use its data. |
| let previous_window = |
| mem::replace(&mut self.current_window, EstimationWindow::new(sample)); |
| match previous_window.frequency() { |
| Ok(window_estimate) => { |
| if self.window_count == 0 { |
| // If this is the first window use the estimated frequency directly... |
| self.estimate = window_estimate; |
| } else { |
| // ... otherwise combine it with the previous estimate. |
| self.estimate = window_estimate * FREQUENCY_ESTIMATION_SMOOTHING |
| + self.estimate * (1f64 - FREQUENCY_ESTIMATION_SMOOTHING); |
| } |
| self.window_count += 1; |
| Ok(Some((self.estimate, self.window_count))) |
| } |
| Err(err) => Err(err.into()), |
| } |
| } |
| } |
| } |
| |
| /// Update the estimate to include the supplied sample that is disjoint from previous samples. |
| /// This will discard any current estimation window so never leads to a new frequency estimate. |
| pub fn update_disjoint(&mut self, sample: &Sample) { |
| self.current_window = EstimationWindow::new(sample); |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use {super::*, chrono::DateTime, test_util::assert_near, zx::DurationNum}; |
| |
| const INITIAL_MONO: zx::Time = zx::Time::from_nanos(7_000_000_000); |
| const STD_DEV: zx::Duration = zx::Duration::from_millis(88); |
| |
| // This time is nowhere near a leap second. |
| const TEST_UTC_STR: &str = "2021-03-25T13:22:52-08:00"; |
| |
| /// Creates a single sample with the supplied times and the standard standard deviation |
| /// Initial UTC is specified as an RFC3339 string. |
| fn create_sample(utc_string: &str, monotonic: zx::Time) -> Sample { |
| let chrono_utc = DateTime::parse_from_rfc3339(utc_string).expect("Invalid UTC string"); |
| Sample::new(zx::Time::from_nanos(chrono_utc.timestamp_nanos()), monotonic, STD_DEV) |
| } |
| |
| /// Create a vector of evenly spaced samples following the supplied reference sample with a |
| /// frequency specified as (utc ticks)/(monotonic ticks). |
| fn create_sample_set( |
| reference_sample: &Sample, |
| quantity: u32, |
| utc_spacing: zx::Duration, |
| frequency: f64, |
| ) -> Vec<Sample> { |
| let monotonic_spacing = |
| zx::Duration::from_nanos((utc_spacing.into_nanos() as f64 / frequency) as i64); |
| |
| let mut vec = Vec::<Sample>::new(); |
| let mut utc = reference_sample.utc; |
| let mut monotonic = reference_sample.monotonic; |
| |
| for _ in 0..quantity { |
| utc += utc_spacing; |
| monotonic += monotonic_spacing; |
| vec.push(Sample::new(utc, monotonic, STD_DEV)); |
| } |
| vec |
| } |
| |
| /// Append new evenly spaced samples to the supplied vector with a frequency specified as |
| /// (utc ticks)/(monotonic ticks). |
| fn extend_sample_set( |
| samples: &mut Vec<Sample>, |
| quantity: u32, |
| utc_spacing: zx::Duration, |
| frequency: f64, |
| ) { |
| let previous_sample = samples.last().expect("No existing samples to extend"); |
| let mut new_samples = create_sample_set(previous_sample, quantity, utc_spacing, frequency); |
| samples.append(&mut new_samples); |
| } |
| |
| fn assert_estimator_update( |
| estimator: &mut FrequencyEstimator, |
| sample: &Sample, |
| expected_frequency: f64, |
| expected_window_count: u32, |
| ) { |
| let (frequency, window_count) = estimator |
| .update(sample) |
| .expect("update returned an error") |
| .expect("update did not lead to an updated frequency"); |
| assert_near!(frequency, expected_frequency, 0.0000001); |
| assert_eq!(window_count, expected_window_count); |
| } |
| |
| #[fuchsia::test] |
| fn estimation_window_valid() { |
| for freq in &[0.999, 1.0, 1.001] { |
| let initial_sample = create_sample(TEST_UTC_STR, INITIAL_MONO); |
| let mut window = EstimationWindow::new(&initial_sample); |
| for sample in create_sample_set(&initial_sample, 20, 1.hour(), *freq) { |
| window.add_sample(&sample).unwrap(); |
| } |
| assert_near!(window.frequency().unwrap(), *freq, 0.0000001); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn estimation_window_outside_window() { |
| let initial = create_sample(TEST_UTC_STR, INITIAL_MONO); |
| let earlier = Sample::new(initial.utc - 1.hour(), initial.monotonic - 1.hour(), STD_DEV); |
| let later = Sample::new(initial.utc + 36.hours(), initial.monotonic + 36.hours(), STD_DEV); |
| |
| let mut window = EstimationWindow::new(&initial); |
| assert_eq!(window.add_sample(&earlier), Err(AddSampleError::BeforeWindow)); |
| assert_eq!(window.add_sample(&later), Err(AddSampleError::AfterWindow)); |
| } |
| #[fuchsia::test] |
| fn estimation_window_insufficient_samples() { |
| let initial_sample = create_sample(TEST_UTC_STR, INITIAL_MONO); |
| let mut window = EstimationWindow::new(&initial_sample); |
| for sample in create_sample_set(&initial_sample, 10, 1.hour(), 0.9876) { |
| window.add_sample(&sample).unwrap(); |
| } |
| assert_eq!(window.frequency(), Err(GetFrequencyError::InsufficientSamples)); |
| } |
| |
| #[fuchsia::test] |
| fn frequency_estimator_valid() { |
| // Two days of data at an initial frequency. |
| let mut samples = vec![create_sample(TEST_UTC_STR, INITIAL_MONO)]; |
| extend_sample_set(&mut samples, 47, 1.hour() + 1.second(), 0.999); |
| // Two more days of data at an different frequency (plus one extra sample so we can close |
| // the fourth day) |
| extend_sample_set(&mut samples, 49, 1.hour() + 1.second(), 0.998); |
| |
| let mut estimator = FrequencyEstimator::new(&mut samples.remove(0)); |
| |
| // We should receive a frequency of exactly 0.099 after each of the first two days. |
| // In the second two days the frequency should converge towards the updated value. |
| for (day, expected_freq) in vec![(1, 0.999), (2, 0.999), (3, 0.99875), (4, 0.9985625)] { |
| for _ in 0..23 { |
| assert_eq!(estimator.update(&mut samples.remove(0)), Ok(None)); |
| } |
| assert_estimator_update(&mut estimator, &mut samples.remove(0), expected_freq, day); |
| } |
| } |
| |
| #[fuchsia::test] |
| fn frequency_estimator_disjoint_update() { |
| let mut samples = vec![create_sample(TEST_UTC_STR, INITIAL_MONO)]; |
| extend_sample_set(&mut samples, 11, 1.hour() + 1.second(), 1.1); |
| extend_sample_set(&mut samples, 25, 1.hour() + 1.second(), 0.999); |
| |
| let mut estimator = FrequencyEstimator::new(&mut samples.remove(0)); |
| |
| // Feed the first 12 samples then mark the 13th as disjoint, causing the first samples to |
| // be ignored. |
| for _ in 0..11 { |
| assert_eq!(estimator.update(&mut samples.remove(0)), Ok(None)); |
| } |
| estimator.update_disjoint(&mut samples.remove(0)); |
| for _ in 0..23 { |
| assert_eq!(estimator.update(&mut samples.remove(0)), Ok(None)); |
| } |
| assert_estimator_update(&mut estimator, &mut samples.remove(0), 0.999, 1); |
| } |
| |
| #[fuchsia::test] |
| fn frequency_estimator_insufficient_samples() { |
| let mut samples = vec![create_sample(TEST_UTC_STR, INITIAL_MONO)]; |
| // Note the first day only has 6 samples so should be ignored. |
| extend_sample_set(&mut samples, 6, 4.hour() + 1.second(), 1.1); |
| extend_sample_set(&mut samples, 25, 1.hour() + 1.second(), 0.999); |
| |
| let mut estimator = FrequencyEstimator::new(&mut samples.remove(0)); |
| |
| for _ in 0..5 { |
| assert_eq!(estimator.update(&mut samples.remove(0)), Ok(None)); |
| } |
| assert_eq!( |
| estimator.update(&mut samples.remove(0)), |
| Err(FrequencyUpdateError::InsufficientSamples) |
| ); |
| for _ in 0..23 { |
| assert_eq!(estimator.update(&mut samples.remove(0)), Ok(None)); |
| } |
| assert_estimator_update(&mut estimator, &mut samples.remove(0), 0.999, 1); |
| } |
| } |