| // 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. |
| |
| mod frequency; |
| mod kalman_filter; |
| |
| use { |
| crate::{ |
| diagnostics::{Diagnostics, Event}, |
| enums::{FrequencyDiscardReason, Track}, |
| time_source::Sample, |
| }, |
| chrono::prelude::*, |
| frequency::FrequencyEstimator, |
| fuchsia_zircon as zx, |
| kalman_filter::KalmanFilter, |
| std::sync::Arc, |
| time_util::Transform, |
| tracing::{info, warn}, |
| }; |
| |
| /// The standard deviation of the system oscillator frequency error in parts per million, used to |
| /// control the growth in error bound and bound the allowed frequency estimates. |
| const OSCILLATOR_ERROR_STD_DEV_PPM: u64 = 15; |
| |
| /// The maximum allowed frequency error away from the nominal 1ns UTC == 1ns monotonic. |
| // TODO(jsankey): This should shortly be increased to a ppm of 2*OSCILLATOR_ERROR, but we start off |
| // more conservative to limit potential impact of any frequency algorithm problems. |
| const MAX_FREQUENCY_ERROR: f64 = 10f64 /*value in ppm*/ / 1_000_000f64; |
| |
| /// The maximum change in Kalman filter estimate that can occur before we discard any previous |
| /// samples being used as part of a long term frequency assessment. This is similar to the value |
| /// used by the ClockManager to determine when to step the externally visible clock but it need |
| /// not be identical. Since the steps are measured at different monotonic times there would always |
| /// be the possibility of an inconsistency. |
| const MAX_STEP_FOR_FREQUENCY_CONTINUITY: zx::Duration = zx::Duration::from_seconds(1); |
| |
| /// Converts a floating point frequency to a rate adjustment in PPM. |
| fn frequency_to_adjust_ppm(frequency: f64) -> i32 { |
| ((frequency - 1.0f64) * 1_000_000f64).round() as i32 |
| } |
| |
| /// Limits the supplied frequency to within +/- MAX_FREQUENCY_ERROR. |
| fn clamp_frequency(input: f64) -> f64 { |
| input.clamp(1.0f64 - MAX_FREQUENCY_ERROR, 1.0f64 + MAX_FREQUENCY_ERROR) |
| } |
| |
| /// Maintains an estimate of the relationship between true UTC time and monotonic time on this |
| /// device, based on time samples received from one or more time sources. |
| #[derive(Debug)] |
| pub struct Estimator<D: Diagnostics> { |
| /// A Kalman Filter to track the UTC offset and uncertainty using a supplied frequency. |
| filter: KalmanFilter, |
| /// An estimator to produce long term estimates of the oscillator frequency. |
| frequency_estimator: FrequencyEstimator, |
| /// The track of the estimate being managed. |
| track: Track, |
| /// A diagnostics implementation for recording events of note. |
| diagnostics: Arc<D>, |
| } |
| |
| impl<D: Diagnostics> Estimator<D> { |
| /// Construct a new estimator initialized to the supplied sample. |
| pub fn new(track: Track, sample: Sample, diagnostics: Arc<D>) -> Self { |
| let frequency_estimator = FrequencyEstimator::new(&sample); |
| let filter = KalmanFilter::new(&sample); |
| diagnostics.record(Event::KalmanFilterUpdated { |
| track, |
| monotonic: filter.monotonic(), |
| utc: filter.utc(), |
| sqrt_covariance: filter.sqrt_covariance(), |
| }); |
| Estimator { filter, frequency_estimator, track, diagnostics } |
| } |
| |
| /// Update the estimate to include the supplied sample. |
| pub fn update(&mut self, sample: Sample) { |
| // Begin by letting the Kalman filter consume the sample at its existing frequency... |
| let utc = sample.utc; |
| let change = match self.filter.update(&sample) { |
| Ok(change) => change, |
| Err(err) => { |
| warn!("Rejected update: {}", err); |
| return; |
| } |
| }; |
| let sqrt_covariance = self.filter.sqrt_covariance(); |
| self.diagnostics.record(Event::KalmanFilterUpdated { |
| track: self.track, |
| monotonic: self.filter.monotonic(), |
| utc: self.filter.utc(), |
| sqrt_covariance, |
| }); |
| info!( |
| "Received {:?} update to {}. estimate_change={}ns, sqrt_covariance={}ns", |
| self.track, |
| Utc.timestamp_nanos(utc.into_nanos()), |
| change.into_nanos(), |
| sqrt_covariance.into_nanos() |
| ); |
| |
| // If this was a big change just flush any long term frequency estimate ... |
| if change.into_nanos().abs() > MAX_STEP_FOR_FREQUENCY_CONTINUITY.into_nanos() { |
| self.frequency_estimator.update_disjoint(&sample); |
| self.diagnostics.record(Event::FrequencyWindowDiscarded { |
| track: self.track, |
| reason: FrequencyDiscardReason::TimeStep, |
| }); |
| info!("Discarding {:?} frequency window due to time step", self.track); |
| return; |
| } |
| |
| // ... otherwise see if the sample lets us calculate a new frequency we can give to |
| // the Kalman filter for next time. |
| match self.frequency_estimator.update(&sample) { |
| Ok(Some((raw_frequency, window_count))) => { |
| let clamped_frequency = clamp_frequency(raw_frequency); |
| self.filter.update_frequency(clamped_frequency); |
| self.diagnostics.record(Event::FrequencyUpdated { |
| track: self.track, |
| monotonic: sample.monotonic, |
| rate_adjust_ppm: frequency_to_adjust_ppm(clamped_frequency), |
| window_count, |
| }); |
| info!( |
| "Received {:?} frequency update: raw={:.9}, clamped={:.9}", |
| self.track, raw_frequency, clamped_frequency |
| ); |
| } |
| Ok(None) => {} |
| Err(reason) => { |
| self.diagnostics |
| .record(Event::FrequencyWindowDiscarded { track: self.track, reason }); |
| warn!("Discarding {:?} frequency window: {:?}", self.track, reason); |
| } |
| } |
| } |
| |
| /// Returns a `Transform` describing the estimated synthetic time and error as a function |
| /// of the monotonic time. |
| pub fn transform(&self) -> Transform { |
| self.filter.transform() |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use { |
| super::*, |
| crate::diagnostics::FakeDiagnostics, |
| fuchsia_zircon::{self as zx, DurationNum}, |
| test_util::assert_near, |
| }; |
| |
| // Note: we need to ensure the absolute times are not near the January 1st leap second. |
| const TIME_1: zx::Time = zx::Time::from_nanos(100_010_000_000_000); |
| const TIME_2: zx::Time = zx::Time::from_nanos(100_020_000_000_000); |
| const TIME_3: zx::Time = zx::Time::from_nanos(100_030_000_000_000); |
| const OFFSET_1: zx::Duration = zx::Duration::from_seconds(777); |
| const OFFSET_2: zx::Duration = zx::Duration::from_seconds(999); |
| const STD_DEV_1: zx::Duration = zx::Duration::from_millis(22); |
| const TEST_TRACK: Track = Track::Primary; |
| const SQRT_COV_1: u64 = STD_DEV_1.into_nanos() as u64; |
| |
| fn create_filter_event( |
| monotonic: zx::Time, |
| offset: zx::Duration, |
| sqrt_covariance: u64, |
| ) -> Event { |
| Event::KalmanFilterUpdated { |
| track: TEST_TRACK, |
| monotonic: monotonic, |
| utc: monotonic + offset, |
| sqrt_covariance: zx::Duration::from_nanos(sqrt_covariance as i64), |
| } |
| } |
| |
| fn create_window_discard_event(reason: FrequencyDiscardReason) -> Event { |
| Event::FrequencyWindowDiscarded { track: TEST_TRACK, reason } |
| } |
| |
| #[fuchsia::test] |
| fn frequency_to_adjust_ppm_test() { |
| assert_eq!(frequency_to_adjust_ppm(0.999), -1000); |
| assert_eq!(frequency_to_adjust_ppm(0.999999), -1); |
| assert_eq!(frequency_to_adjust_ppm(1.0), 0); |
| assert_eq!(frequency_to_adjust_ppm(1.000001), 1); |
| assert_eq!(frequency_to_adjust_ppm(1.001), 1000); |
| } |
| |
| #[fuchsia::test] |
| fn clamp_frequency_test() { |
| assert_near!(clamp_frequency(-452.0), 0.99999, 1e-9); |
| assert_near!(clamp_frequency(0.99), 0.99999, 1e-9); |
| assert_near!(clamp_frequency(1.0000001), 1.0000001, 1e-9); |
| assert_near!(clamp_frequency(1.01), 1.00001, 1e-9); |
| } |
| |
| #[fuchsia::test] |
| fn initialize() { |
| let diagnostics = Arc::new(FakeDiagnostics::new()); |
| let estimator = Estimator::new( |
| TEST_TRACK, |
| Sample::new(TIME_1 + OFFSET_1, TIME_1, STD_DEV_1), |
| Arc::clone(&diagnostics), |
| ); |
| diagnostics.assert_events(&[create_filter_event(TIME_1, OFFSET_1, SQRT_COV_1)]); |
| let transform = estimator.transform(); |
| assert_eq!(transform.synthetic(TIME_1), TIME_1 + OFFSET_1); |
| assert_eq!(transform.synthetic(TIME_2), TIME_2 + OFFSET_1); |
| assert_eq!(transform.error_bound(TIME_1), 2 * SQRT_COV_1); |
| // Earlier time should return same error bound. |
| assert_eq!(transform.error_bound(TIME_1 - 1.second()), 2 * SQRT_COV_1); |
| // Later time should have a higher bound. |
| assert_eq!( |
| transform.error_bound(TIME_1 + 1.second()), |
| 2 * SQRT_COV_1 + 2000 * OSCILLATOR_ERROR_STD_DEV_PPM |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn apply_inconsistent_update() { |
| let diagnostics = Arc::new(FakeDiagnostics::new()); |
| let mut estimator = Estimator::new( |
| TEST_TRACK, |
| Sample::new(TIME_1 + OFFSET_1, TIME_1, STD_DEV_1), |
| Arc::clone(&diagnostics), |
| ); |
| estimator.update(Sample::new(TIME_2 + OFFSET_2, TIME_2, STD_DEV_1)); |
| |
| // Expected offset is biased slightly towards the second estimate. |
| let expected_offset = 88_8002_580_002.nanos(); |
| let expected_sqrt_cov = 15_556_529u64; |
| assert_eq!(estimator.transform().synthetic(TIME_3), TIME_3 + expected_offset); |
| |
| // The frequency estimator should have discarded the first update. |
| assert_eq!(estimator.frequency_estimator.window_count(), 0); |
| assert_eq!(estimator.frequency_estimator.current_sample_count(), 1); |
| |
| diagnostics.assert_events(&[ |
| create_filter_event(TIME_1, OFFSET_1, SQRT_COV_1), |
| create_filter_event(TIME_2, expected_offset, expected_sqrt_cov), |
| create_window_discard_event(FrequencyDiscardReason::TimeStep), |
| ]); |
| } |
| |
| #[fuchsia::test] |
| fn apply_consistent_update() { |
| let diagnostics = Arc::new(FakeDiagnostics::new()); |
| let mut estimator = Estimator::new( |
| TEST_TRACK, |
| Sample::new(TIME_1 + OFFSET_1, TIME_1, STD_DEV_1), |
| Arc::clone(&diagnostics), |
| ); |
| estimator.update(Sample::new(TIME_2 + OFFSET_1, TIME_2, STD_DEV_1)); |
| |
| let expected_sqrt_cov = 15_556_529u64; |
| assert_eq!(estimator.transform().synthetic(TIME_3), TIME_3 + OFFSET_1); |
| |
| // The frequency estimator should have retained both samples. |
| assert_eq!(estimator.frequency_estimator.window_count(), 0); |
| assert_eq!(estimator.frequency_estimator.current_sample_count(), 2); |
| |
| diagnostics.assert_events(&[ |
| create_filter_event(TIME_1, OFFSET_1, SQRT_COV_1), |
| create_filter_event(TIME_2, OFFSET_1, expected_sqrt_cov), |
| ]); |
| } |
| |
| #[fuchsia::test] |
| fn earlier_monotonic_ignored() { |
| let diagnostics = Arc::new(FakeDiagnostics::new()); |
| let mut estimator = Estimator::new( |
| TEST_TRACK, |
| Sample::new(TIME_2 + OFFSET_1, TIME_2, STD_DEV_1), |
| Arc::clone(&diagnostics), |
| ); |
| assert_eq!(estimator.transform().synthetic(TIME_3), TIME_3 + OFFSET_1); |
| estimator.update(Sample::new(TIME_1 + OFFSET_2, TIME_1, STD_DEV_1)); |
| assert_eq!(estimator.transform().synthetic(TIME_3), TIME_3 + OFFSET_1); |
| // Ignored event should not be logged. |
| diagnostics.assert_events(&[create_filter_event(TIME_2, OFFSET_1, SQRT_COV_1)]); |
| // Nor included in the frequency estimator. |
| assert_eq!(estimator.frequency_estimator.current_sample_count(), 1); |
| } |
| |
| #[fuchsia::test] |
| fn frequency_convergence() { |
| // Generate two days of samples at a fixed, slightly erroneous, frequency. |
| let reference_sample = Sample::new(TIME_1 + OFFSET_1, TIME_2, STD_DEV_1); |
| let mut samples = Vec::<Sample>::new(); |
| { |
| let test_frequency = 1.000003; |
| let utc_spacing = 1.hour() + 1.millis(); |
| let monotonic_spacing = |
| zx::Duration::from_nanos((utc_spacing.into_nanos() as f64 / test_frequency) as i64); |
| for i in 1..48 { |
| samples.push(Sample::new( |
| reference_sample.utc + utc_spacing * i, |
| reference_sample.monotonic + monotonic_spacing * i, |
| reference_sample.std_dev, |
| )); |
| } |
| } |
| |
| let diagnostics = Arc::new(FakeDiagnostics::new()); |
| let mut estimator = Estimator::new(TEST_TRACK, reference_sample, Arc::clone(&diagnostics)); |
| |
| // Run through these samples, asking the estimator to predict the utc of each sample based |
| // on the sample's monotonic value, before feeding that sample into the estimator. |
| for (i, sample) in samples.into_iter().enumerate() { |
| let estimate = estimator.transform().synthetic(sample.monotonic); |
| let error = zx::Duration::from_nanos((sample.utc - estimate).into_nanos().abs()); |
| |
| // For the first day of samples the estimator will perform imperfectly until its been |
| // able to estimate the long term frequency. After this it should get much better. |
| // We calculated the new frequency after consuming the first window outside the first |
| // day (i=23) and it begins to help on the next sample (i=24). |
| let expected_error = match i { |
| 0 => 11.millis(), |
| _ if i <= 23 => 12.millis(), |
| 24 => 2.millis(), |
| _ => 0.millis(), |
| }; |
| |
| assert_near!(error, expected_error, 1.millis()); |
| estimator.update(sample); |
| } |
| } |
| } |