blob: 219e1b93f361160e3d4d1a5e39e08385c011a350 [file] [log] [blame]
// 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::Track,
time_source::Sample,
},
chrono::prelude::*,
kalman_filter::KalmanFilter,
log::{info, warn},
std::sync::Arc,
time_util::Transform,
};
/// 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;
/// 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
}
/// 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,
/// 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 filter = KalmanFilter::new(&sample);
diagnostics.record(Event::KalmanFilterUpdated {
track,
monotonic: filter.monotonic(),
utc: filter.utc(),
sqrt_covariance: filter.sqrt_covariance(),
});
Estimator { filter, track, diagnostics }
}
/// Update the estimate to include the supplied sample.
pub fn update(&mut self, sample: Sample) {
let utc = sample.utc;
if let Err(err) = self.filter.update(&sample) {
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 {}. sqrt_covariance={}ns",
self.track,
Utc.timestamp_nanos(utc.into_nanos()),
sqrt_covariance.into_nanos()
);
}
/// 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},
};
const TIME_1: zx::Time = zx::Time::from_nanos(10_000_000_000);
const TIME_2: zx::Time = zx::Time::from_nanos(20_000_000_000);
const TIME_3: zx::Time = zx::Time::from_nanos(30_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),
}
}
#[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 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_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);
diagnostics.assert_events(&[
create_filter_event(TIME_1, OFFSET_1, SQRT_COV_1),
create_filter_event(TIME_2, expected_offset, 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)]);
}
}