forked from ROMEO/nexosim
Merge pull request #9 from asynchronics/feature/clock
Add support for custom/real-time clocks
This commit is contained in:
commit
9d78e4f72a
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -73,7 +73,7 @@ jobs:
|
||||
- name: Run cargo miri tests
|
||||
run: cargo miri test --tests --lib
|
||||
env:
|
||||
MIRIFLAGS: -Zmiri-strict-provenance
|
||||
MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-disable-isolation -Zmiri-num-cpus=4
|
||||
|
||||
- name: Run cargo miri example1
|
||||
run: cargo miri run --example espresso_machine
|
||||
|
@ -35,6 +35,7 @@ num_cpus = "1.13"
|
||||
pin-project-lite = "0.2"
|
||||
recycle-box = "0.2"
|
||||
slab = "0.4"
|
||||
spin_sleep = "1"
|
||||
st3 = "0.4"
|
||||
|
||||
[target.'cfg(asynchronix_loom)'.dependencies]
|
||||
|
@ -235,8 +235,8 @@
|
||||
//! The simulation can be controlled in several ways:
|
||||
//!
|
||||
//! 1. by advancing time, either until the next scheduled event with
|
||||
//! [`Simulation::step()`](simulation::Simulation::step), or by a specific
|
||||
//! duration using for instance
|
||||
//! [`Simulation::step()`](simulation::Simulation::step), or until a specific
|
||||
//! deadline using for instance
|
||||
//! [`Simulation::step_by()`](simulation::Simulation::step_by).
|
||||
//! 2. by sending events or queries without advancing simulation time, using
|
||||
//! [`Simulation::send_event()`](simulation::Simulation::send_event) or
|
||||
@ -244,6 +244,15 @@
|
||||
//! 3. by scheduling events, using for instance
|
||||
//! [`Simulation::schedule_event()`](simulation::Simulation::schedule_event).
|
||||
//!
|
||||
//! When a simulation is initialized via
|
||||
//! [`SimInit::init()`](simulation::SimInit::init) then the simulation will run
|
||||
//! as fast as possible, without regard for the actual wall clock time.
|
||||
//! Alternatively, it is possible to initialize a simulation via
|
||||
//! [`SimInit::init_with_clock()`](simulation::SimInit::init_with_clock) to bind
|
||||
//! the simulation time to the wall clock time using a custom
|
||||
//! [`Clock`](time::Clock) type or a readily-available real-time clock such as
|
||||
//! [`AutoSystemClock`](time::AutoSystemClock).
|
||||
//!
|
||||
//! Simulation outputs can be monitored using
|
||||
//! [`EventSlot`](simulation::EventSlot)s and
|
||||
//! [`EventStream`](simulation::EventStream)s, which can be connected to any
|
||||
|
@ -9,13 +9,14 @@
|
||||
//! The lifecycle of a simulation bench typically comprises the following
|
||||
//! stages:
|
||||
//!
|
||||
//! 1) instantiation of models and their [`Mailbox`]es,
|
||||
//! 2) connection of the models' output/requestor ports to input/replier ports
|
||||
//! 1. instantiation of models and their [`Mailbox`]es,
|
||||
//! 2. connection of the models' output/requestor ports to input/replier ports
|
||||
//! using the [`Address`]es of the target models,
|
||||
//! 3) instantiation of a [`SimInit`] simulation builder and migration of all
|
||||
//! 3. instantiation of a [`SimInit`] simulation builder and migration of all
|
||||
//! models and mailboxes to the builder with [`SimInit::add_model()`],
|
||||
//! 4) initialization of a [`Simulation`] instance with [`SimInit::init()`],
|
||||
//! 5) discrete-time simulation, which typically involves scheduling events and
|
||||
//! 4. initialization of a [`Simulation`] instance with [`SimInit::init()`] or
|
||||
//! [`SimInit::init_with_clock()`],
|
||||
//! 5. discrete-time simulation, which typically involves scheduling events and
|
||||
//! incrementing simulation time while observing the models outputs.
|
||||
//!
|
||||
//! Most information necessary to run a simulation is available in the root
|
||||
@ -137,8 +138,8 @@ use recycle_box::{coerce_box, RecycleBox};
|
||||
use crate::executor::Executor;
|
||||
use crate::model::{InputFn, Model, ReplierFn};
|
||||
use crate::time::{
|
||||
self, Deadline, EventKey, MonotonicTime, ScheduledEvent, SchedulerQueue, SchedulingError,
|
||||
TearableAtomicTime,
|
||||
self, Clock, Deadline, EventKey, MonotonicTime, NoClock, ScheduledEvent, SchedulerQueue,
|
||||
SchedulingError, TearableAtomicTime,
|
||||
};
|
||||
use crate::util::futures::SeqFuture;
|
||||
use crate::util::slot;
|
||||
@ -146,15 +147,17 @@ use crate::util::sync_cell::SyncCell;
|
||||
|
||||
/// Simulation environment.
|
||||
///
|
||||
/// A `Simulation` is created by calling the
|
||||
/// [`SimInit::init()`](crate::simulation::SimInit::init) method on a simulation
|
||||
/// initializer. It contains an asynchronous executor that runs all simulation
|
||||
/// models added beforehand to [`SimInit`](crate::simulation::SimInit).
|
||||
/// A `Simulation` is created by calling
|
||||
/// [`SimInit::init()`](crate::simulation::SimInit::init) or
|
||||
/// [`SimInit::init_with_clock()`](crate::simulation::SimInit::init_with_clock)
|
||||
/// method on a simulation initializer. It contains an asynchronous executor
|
||||
/// that runs all simulation models added beforehand to
|
||||
/// [`SimInit`](crate::simulation::SimInit).
|
||||
///
|
||||
/// A [`Simulation`] object also manages an event scheduling queue and
|
||||
/// simulation time. The scheduling queue can be accessed from the simulation
|
||||
/// itself, but also from models via the optional
|
||||
/// [`&Scheduler`][time::Scheduler] argument of input and replier port methods.
|
||||
/// [`&Scheduler`](time::Scheduler) argument of input and replier port methods.
|
||||
/// Likewise, simulation time can be accessed with the [`Simulation::time()`]
|
||||
/// method, or from models with the [`Scheduler::time()`](time::Scheduler::time)
|
||||
/// method.
|
||||
@ -169,18 +172,24 @@ use crate::util::sync_cell::SyncCell;
|
||||
/// [`schedule_*()`](Simulation::schedule_event) method. These methods queue an
|
||||
/// event without blocking.
|
||||
///
|
||||
/// Finally, the [`Simulation`] instance manages simulation time. Calling
|
||||
/// [`step()`](Simulation::step) will increment simulation time until that of
|
||||
/// the next scheduled event in chronological order, whereas
|
||||
/// [`step_by()`](Simulation::step_by) and
|
||||
/// [`step_until()`](Simulation::step_until) can increment time by an arbitrary
|
||||
/// duration, running the computations for all intermediate time slices
|
||||
/// sequentially. These methods will block until all computations for the
|
||||
/// relevant time slice(s) have completed.
|
||||
/// Finally, the [`Simulation`] instance manages simulation time. A call to
|
||||
/// [`step()`](Simulation::step) will:
|
||||
///
|
||||
/// 1. increment simulation time until that of the next scheduled event in
|
||||
/// chronological order, then
|
||||
/// 2. call [`Clock::synchronize()`](time::Clock::synchronize) which, unless the
|
||||
/// simulation is configured to run as fast as possible, blocks until the
|
||||
/// desired wall clock time, and finally
|
||||
/// 3. run all computations scheduled for the new simulation time.
|
||||
///
|
||||
/// The [`step_by()`](Simulation::step_by) and
|
||||
/// [`step_until()`](Simulation::step_until) methods operate similarly but
|
||||
/// iterate until the target simulation time has been reached.
|
||||
pub struct Simulation {
|
||||
executor: Executor,
|
||||
scheduler_queue: Arc<Mutex<SchedulerQueue>>,
|
||||
time: SyncCell<TearableAtomicTime>,
|
||||
clock: Box<dyn Clock>,
|
||||
}
|
||||
|
||||
impl Simulation {
|
||||
@ -194,6 +203,22 @@ impl Simulation {
|
||||
executor,
|
||||
scheduler_queue,
|
||||
time,
|
||||
clock: Box::new(NoClock::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `Simulation` with the specified clock.
|
||||
pub(crate) fn with_clock(
|
||||
executor: Executor,
|
||||
scheduler_queue: Arc<Mutex<SchedulerQueue>>,
|
||||
time: SyncCell<TearableAtomicTime>,
|
||||
clock: impl Clock + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
executor,
|
||||
scheduler_queue,
|
||||
time,
|
||||
clock: Box::new(clock),
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,31 +230,34 @@ impl Simulation {
|
||||
/// Advances simulation time to that of the next scheduled event, processing
|
||||
/// that event as well as all other event scheduled for the same time.
|
||||
///
|
||||
/// This method may block. Once it returns, it is guaranteed that all newly
|
||||
/// processed event (if any) have completed.
|
||||
/// Processing is gated by a (possibly blocking) call to
|
||||
/// [`Clock::synchronize()`](time::Clock::synchronize) on the configured
|
||||
/// simulation clock. This method blocks until all newly processed events
|
||||
/// have completed.
|
||||
pub fn step(&mut self) {
|
||||
self.step_to_next_bounded(MonotonicTime::MAX);
|
||||
}
|
||||
|
||||
/// Iteratively advances the simulation time by the specified duration and
|
||||
/// processes all events scheduled up to the target time.
|
||||
/// Iteratively advances the simulation time by the specified duration, as
|
||||
/// if by calling [`Simulation::step()`] repeatedly.
|
||||
///
|
||||
/// This method may block. Once it returns, it is guaranteed that (i) all
|
||||
/// events scheduled up to the specified target time have completed and (ii)
|
||||
/// the final simulation time has been incremented by the specified
|
||||
/// duration.
|
||||
/// This method blocks until all events scheduled up to the specified target
|
||||
/// time have completed. The simulation time upon completion is equal to the
|
||||
/// initial simulation time incremented by the specified duration, whether
|
||||
/// or not an event was scheduled for that time.
|
||||
pub fn step_by(&mut self, duration: Duration) {
|
||||
let target_time = self.time.read() + duration;
|
||||
|
||||
self.step_until_unchecked(target_time);
|
||||
}
|
||||
|
||||
/// Iteratively advances the simulation time and processes all events
|
||||
/// scheduled up to the specified target time.
|
||||
/// Iteratively advances the simulation time until the specified deadline,
|
||||
/// as if by calling [`Simulation::step()`] repeatedly.
|
||||
///
|
||||
/// This method may block. Once it returns, it is guaranteed that (i) all
|
||||
/// events scheduled up to the specified target time have completed and (ii)
|
||||
/// the final simulation time matches the target time.
|
||||
/// This method blocks until all events scheduled up to the specified target
|
||||
/// time have completed. The simulation time upon completion is equal to the
|
||||
/// specified target time, whether or not an event was scheduled for that
|
||||
/// time.
|
||||
pub fn step_until(&mut self, target_time: MonotonicTime) -> Result<(), SchedulingError> {
|
||||
if self.time.read() >= target_time {
|
||||
return Err(SchedulingError::InvalidScheduledTime);
|
||||
@ -477,7 +505,7 @@ impl Simulation {
|
||||
/// corresponding new simulation time is returned.
|
||||
fn step_to_next_bounded(&mut self, upper_time_bound: MonotonicTime) -> Option<MonotonicTime> {
|
||||
// Function pulling the next event. If the event is periodic, it is
|
||||
// immediately cloned and re-scheduled.
|
||||
// immediately re-scheduled.
|
||||
fn pull_next_event(
|
||||
scheduler_queue: &mut MutexGuard<SchedulerQueue>,
|
||||
) -> Box<dyn ScheduledEvent> {
|
||||
@ -545,9 +573,12 @@ impl Simulation {
|
||||
// Otherwise wait until all events have completed and return.
|
||||
_ => {
|
||||
drop(scheduler_queue); // make sure the queue's mutex is released.
|
||||
let current_time = current_key.0;
|
||||
// TODO: check synchronization status?
|
||||
self.clock.synchronize(current_time);
|
||||
self.executor.run();
|
||||
|
||||
return Some(current_key.0);
|
||||
return Some(current_time);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::model::Model;
|
||||
/// A mailbox is an entity associated to a model instance that collects all
|
||||
/// messages sent to that model. The size of its internal buffer can be
|
||||
/// optionally specified at construction time using
|
||||
/// [`with_capacity`](Mailbox::with_capacity).
|
||||
/// [`with_capacity()`](Mailbox::with_capacity).
|
||||
pub struct Mailbox<M: Model>(pub(crate) Receiver<M>);
|
||||
|
||||
impl<M: Model> Mailbox<M> {
|
||||
|
@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::executor::Executor;
|
||||
use crate::model::Model;
|
||||
use crate::time::Scheduler;
|
||||
use crate::time::{Clock, Scheduler};
|
||||
use crate::time::{MonotonicTime, SchedulerQueue, TearableAtomicTime};
|
||||
use crate::util::priority_queue::PriorityQueue;
|
||||
use crate::util::sync_cell::SyncCell;
|
||||
@ -58,12 +58,33 @@ impl SimInit {
|
||||
/// Builds a simulation initialized at the specified simulation time,
|
||||
/// executing the [`Model::init()`](crate::model::Model::init) method on all
|
||||
/// model initializers.
|
||||
///
|
||||
/// This is equivalent to calling [`SimInit::init_with_clock()`] with a
|
||||
/// [`NoClock`](crate::time::NoClock) argument and effectively makes the
|
||||
/// simulation run as fast as possible.
|
||||
pub fn init(mut self, start_time: MonotonicTime) -> Simulation {
|
||||
self.time.write(start_time);
|
||||
self.executor.run();
|
||||
|
||||
Simulation::new(self.executor, self.scheduler_queue, self.time)
|
||||
}
|
||||
|
||||
/// Builds a simulation synchronized with the provided
|
||||
/// [`Clock`](crate::time::Clock) and initialized at the specified
|
||||
/// simulation time, executing the
|
||||
/// [`Model::init()`](crate::model::Model::init) method on all model
|
||||
/// initializers.
|
||||
pub fn init_with_clock(
|
||||
mut self,
|
||||
start_time: MonotonicTime,
|
||||
mut clock: impl Clock + 'static,
|
||||
) -> Simulation {
|
||||
self.time.write(start_time);
|
||||
clock.synchronize(start_time);
|
||||
self.executor.run();
|
||||
|
||||
Simulation::with_clock(self.executor, self.scheduler_queue, self.time, clock)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimInit {
|
||||
|
@ -3,6 +3,8 @@
|
||||
//! This module provides most notably:
|
||||
//!
|
||||
//! * [`MonotonicTime`]: a monotonic timestamp based on the [TAI] time standard,
|
||||
//! * [`Clock`]: a trait for types that can synchronize a simulation,
|
||||
//! implemented for instance by [`SystemClock`] and [`AutoSystemClock`],
|
||||
//! * [`Scheduler`]: a model-local handle to the global scheduler that can be
|
||||
//! used by models to schedule future actions onto themselves.
|
||||
//!
|
||||
@ -45,9 +47,11 @@
|
||||
//! impl Model for AlarmClock {}
|
||||
//! ```
|
||||
|
||||
mod clock;
|
||||
mod monotonic_time;
|
||||
mod scheduler;
|
||||
|
||||
pub use clock::{AutoSystemClock, Clock, NoClock, SyncStatus, SystemClock};
|
||||
pub(crate) use monotonic_time::TearableAtomicTime;
|
||||
pub use monotonic_time::{MonotonicTime, SystemTimeError};
|
||||
pub(crate) use scheduler::{
|
||||
|
235
asynchronix/src/time/clock.rs
Normal file
235
asynchronix/src/time/clock.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use crate::time::MonotonicTime;
|
||||
|
||||
/// A type that can be used to synchronize a simulation.
|
||||
///
|
||||
/// This trait abstract over the different types of clocks, such as
|
||||
/// as-fast-as-possible and real-time clocks.
|
||||
///
|
||||
/// A clock can be associated to a simulation at initialization time by calling
|
||||
/// [`SimInit::init_with_clock()`](crate::simulation::SimInit::init_with_clock).
|
||||
pub trait Clock {
|
||||
/// Blocks until the deadline.
|
||||
fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus;
|
||||
}
|
||||
|
||||
/// The current synchronization status of a clock.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SyncStatus {
|
||||
/// The clock is synchronized.
|
||||
Synchronized,
|
||||
/// The clock is lagging behind by the specified offset.
|
||||
OutOfSync(Duration),
|
||||
}
|
||||
|
||||
/// A dummy [`Clock`] that ignores synchronization.
|
||||
///
|
||||
/// Choosing this clock effectively makes the simulation run as fast as
|
||||
/// possible.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct NoClock {}
|
||||
|
||||
impl NoClock {
|
||||
/// Constructs a new `NoClock` object.
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for NoClock {
|
||||
/// Returns immediately with status `SyncStatus::Synchronized`.
|
||||
fn synchronize(&mut self, _: MonotonicTime) -> SyncStatus {
|
||||
SyncStatus::Synchronized
|
||||
}
|
||||
}
|
||||
|
||||
/// A real-time [`Clock`] based on the system's monotonic clock.
|
||||
///
|
||||
/// This clock accepts an arbitrary reference time and remains synchronized with
|
||||
/// the system's monotonic clock.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SystemClock {
|
||||
wall_clock_ref: Instant,
|
||||
simulation_ref: MonotonicTime,
|
||||
}
|
||||
|
||||
impl SystemClock {
|
||||
/// Constructs a `SystemClock` with an offset between simulation clock and
|
||||
/// wall clock specified by a simulation time matched to an [`Instant`]
|
||||
/// timestamp.
|
||||
///
|
||||
/// The provided reference time may lie in the past or in the future.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::time::{Duration, Instant};
|
||||
///
|
||||
/// use asynchronix::simulation::SimInit;
|
||||
/// use asynchronix::time::{MonotonicTime, SystemClock};
|
||||
///
|
||||
/// let t0 = MonotonicTime::new(1_234_567_890, 0);
|
||||
///
|
||||
/// // Make the simulation start in 1s.
|
||||
/// let clock = SystemClock::from_instant(t0, Instant::now() + Duration::from_secs(1));
|
||||
///
|
||||
/// let simu = SimInit::new()
|
||||
/// // .add_model(...)
|
||||
/// // .add_model(...)
|
||||
/// .init_with_clock(t0, clock);
|
||||
/// ```
|
||||
pub fn from_instant(simulation_ref: MonotonicTime, wall_clock_ref: Instant) -> Self {
|
||||
Self {
|
||||
wall_clock_ref,
|
||||
simulation_ref,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a `SystemClock` with an offset between simulation clock and
|
||||
/// wall clock specified by a simulation time matched to a [`SystemTime`]
|
||||
/// timestamp.
|
||||
///
|
||||
/// The provided reference time may lie in the past or in the future.
|
||||
///
|
||||
/// Note that, even though the wall clock reference is specified with the
|
||||
/// (non-monotonic) system clock, the [`synchronize()`](Clock::synchronize)
|
||||
/// method will still use the system's _monotonic_ clock. This constructor
|
||||
/// makes a best-effort attempt at synchronizing the monotonic clock with
|
||||
/// the non-monotonic system clock _at construction time_, but this
|
||||
/// synchronization will be lost if the system clock is subsequently
|
||||
/// modified through administrative changes, introduction of leap second or
|
||||
/// otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::time::{Duration, UNIX_EPOCH};
|
||||
///
|
||||
/// use asynchronix::simulation::SimInit;
|
||||
/// use asynchronix::time::{MonotonicTime, SystemClock};
|
||||
///
|
||||
/// let t0 = MonotonicTime::new(1_234_567_890, 0);
|
||||
///
|
||||
/// // Make the simulation start at the next full second boundary.
|
||||
/// let now_secs = UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
/// let start_time = UNIX_EPOCH + Duration::from_secs(now_secs + 1);
|
||||
///
|
||||
/// let clock = SystemClock::from_system_time(t0, start_time);
|
||||
///
|
||||
/// let simu = SimInit::new()
|
||||
/// // .add_model(...)
|
||||
/// // .add_model(...)
|
||||
/// .init_with_clock(t0, clock);
|
||||
/// ```
|
||||
pub fn from_system_time(simulation_ref: MonotonicTime, wall_clock_ref: SystemTime) -> Self {
|
||||
// Select the best-correlated `Instant`/`SystemTime` pair from several
|
||||
// samples to improve robustness towards possible thread suspension
|
||||
// between the calls to `SystemTime::now()` and `Instant::now()`.
|
||||
const SAMPLES: usize = 3;
|
||||
|
||||
let mut last_instant = Instant::now();
|
||||
let mut min_delta = Duration::MAX;
|
||||
let mut ref_time = None;
|
||||
|
||||
// Select the best-correlated instant/date pair.
|
||||
for _ in 0..SAMPLES {
|
||||
// The inner loop is to work around monotonic clock platform bugs
|
||||
// that may cause `checked_duration_since` to fail.
|
||||
let (date, instant, delta) = loop {
|
||||
let date = SystemTime::now();
|
||||
let instant = Instant::now();
|
||||
let delta = instant.checked_duration_since(last_instant);
|
||||
last_instant = instant;
|
||||
|
||||
if let Some(delta) = delta {
|
||||
break (date, instant, delta);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the current instant/date if the time elapsed since the last
|
||||
// measurement is shorter than the previous candidate.
|
||||
if min_delta > delta {
|
||||
min_delta = delta;
|
||||
ref_time = Some((instant, date));
|
||||
}
|
||||
}
|
||||
|
||||
// Set the selected instant/date as the wall clock reference and adjust
|
||||
// the simulation reference accordingly.
|
||||
let (instant_ref, date_ref) = ref_time.unwrap();
|
||||
let simulation_ref = if date_ref > wall_clock_ref {
|
||||
let correction = date_ref.duration_since(wall_clock_ref).unwrap();
|
||||
|
||||
simulation_ref + correction
|
||||
} else {
|
||||
let correction = wall_clock_ref.duration_since(date_ref).unwrap();
|
||||
|
||||
simulation_ref - correction
|
||||
};
|
||||
|
||||
Self {
|
||||
wall_clock_ref: instant_ref,
|
||||
simulation_ref,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for SystemClock {
|
||||
/// Blocks until the system time corresponds to the specified simulation
|
||||
/// time.
|
||||
fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus {
|
||||
let target_time = if deadline >= self.simulation_ref {
|
||||
self.wall_clock_ref + deadline.duration_since(self.simulation_ref)
|
||||
} else {
|
||||
self.wall_clock_ref - self.simulation_ref.duration_since(deadline)
|
||||
};
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
match target_time.checked_duration_since(now) {
|
||||
Some(sleep_duration) => {
|
||||
spin_sleep::sleep(sleep_duration);
|
||||
|
||||
SyncStatus::Synchronized
|
||||
}
|
||||
None => SyncStatus::OutOfSync(now.duration_since(target_time)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An automatically initialized real-time [`Clock`] based on the system's
|
||||
/// monotonic clock.
|
||||
///
|
||||
/// This clock is similar to [`SystemClock`] except that the first call to
|
||||
/// [`synchronize()`](Clock::synchronize) never blocks and implicitly defines
|
||||
/// the reference time. In other words, the clock starts running on its first
|
||||
/// invocation.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub struct AutoSystemClock {
|
||||
inner: Option<SystemClock>,
|
||||
}
|
||||
|
||||
impl AutoSystemClock {
|
||||
/// Constructs a new `AutoSystemClock`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clock for AutoSystemClock {
|
||||
/// Initializes the time reference and returns immediately on the first
|
||||
/// call, otherwise blocks until the system time corresponds to the
|
||||
/// specified simulation time.
|
||||
fn synchronize(&mut self, deadline: MonotonicTime) -> SyncStatus {
|
||||
match &mut self.inner {
|
||||
None => {
|
||||
let now = Instant::now();
|
||||
self.inner = Some(SystemClock::from_instant(deadline, now));
|
||||
|
||||
SyncStatus::Synchronized
|
||||
}
|
||||
Some(clock) => clock.synchronize(deadline),
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ const NANOS_PER_SEC: u32 = 1_000_000_000;
|
||||
/// - if required, exact conversion to a Unix timestamp is trivial and only
|
||||
/// requires subtracting from this timestamp the number of leap seconds
|
||||
/// between TAI and UTC time (see also the
|
||||
/// [`as_unix_secs`](MonotonicTime::as_unix_secs) method).
|
||||
/// [`as_unix_secs()`](MonotonicTime::as_unix_secs) method).
|
||||
///
|
||||
/// Although no date-time conversion methods are provided, conversion from
|
||||
/// timestamp to TAI date-time representations and back can be easily performed
|
||||
@ -163,7 +163,8 @@ impl MonotonicTime {
|
||||
/// [`EPOCH`](MonotonicTime::EPOCH) (1970-01-01 00:00:00 TAI).
|
||||
///
|
||||
/// Consistently with the interpretation of seconds and nanoseconds in the
|
||||
/// [`new`][Self::new] constructor, seconds are always rounded towards `-∞`.
|
||||
/// [`new()`](Self::new) constructor, seconds are always rounded towards
|
||||
/// `-∞`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@ -192,12 +193,12 @@ impl MonotonicTime {
|
||||
/// current and historical values.
|
||||
///
|
||||
/// This method merely subtracts the offset from the value returned by
|
||||
/// [`as_secs`](Self::as_secs) and checks for potential overflow; its main
|
||||
/// [`as_secs()`](Self::as_secs) and checks for potential overflow; its main
|
||||
/// purpose is to prevent mistakes regarding the direction in which the
|
||||
/// offset should be applied.
|
||||
///
|
||||
/// Note that the nanosecond part of a Unix timestamp can be simply
|
||||
/// retrieved with [`subsec_nanos`][Self::subsec_nanos] since UTC and TAI
|
||||
/// retrieved with [`subsec_nanos()`](Self::subsec_nanos) since UTC and TAI
|
||||
/// differ by a whole number of seconds.
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -6,7 +6,7 @@ use asynchronix::model::{Model, Output};
|
||||
use asynchronix::simulation::{Address, EventStream, Mailbox, SimInit, Simulation};
|
||||
use asynchronix::time::MonotonicTime;
|
||||
|
||||
// Simple input-to-output pass-through model.
|
||||
// Input-to-output pass-through model.
|
||||
struct PassThroughModel<T: Clone + Send + 'static> {
|
||||
pub output: Output<T>,
|
||||
}
|
||||
@ -23,13 +23,10 @@ impl<T: Clone + Send + 'static> PassThroughModel<T> {
|
||||
impl<T: Clone + Send + 'static> Model for PassThroughModel<T> {}
|
||||
|
||||
/// A simple bench containing a single pass-through model (input forwarded to
|
||||
/// output).
|
||||
fn simple_bench<T: Clone + Send + 'static>() -> (
|
||||
Simulation,
|
||||
MonotonicTime,
|
||||
Address<PassThroughModel<T>>,
|
||||
EventStream<T>,
|
||||
) {
|
||||
/// output) running as fast as possible.
|
||||
fn passthrough_bench<T: Clone + Send + 'static>(
|
||||
t0: MonotonicTime,
|
||||
) -> (Simulation, Address<PassThroughModel<T>>, EventStream<T>) {
|
||||
// Bench assembly.
|
||||
let mut model = PassThroughModel::new();
|
||||
let mbox = Mailbox::new();
|
||||
@ -37,16 +34,15 @@ fn simple_bench<T: Clone + Send + 'static>() -> (
|
||||
let out_stream = model.output.connect_stream().0;
|
||||
let addr = mbox.address();
|
||||
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
|
||||
let simu = SimInit::new().add_model(model, mbox).init(t0);
|
||||
|
||||
(simu, t0, addr, out_stream)
|
||||
(simu, addr, out_stream)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulation_schedule_events() {
|
||||
let (mut simu, t0, addr, mut output) = simple_bench();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let (mut simu, addr, mut output) = passthrough_bench(t0);
|
||||
|
||||
// Queue 2 events at t0+3s and t0+2s, in reverse order.
|
||||
simu.schedule_event(Duration::from_secs(3), PassThroughModel::input, (), &addr)
|
||||
@ -82,7 +78,8 @@ fn simulation_schedule_events() {
|
||||
|
||||
#[test]
|
||||
fn simulation_schedule_keyed_events() {
|
||||
let (mut simu, t0, addr, mut output) = simple_bench();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let (mut simu, addr, mut output) = passthrough_bench(t0);
|
||||
|
||||
let event_t1 = simu
|
||||
.schedule_keyed_event(
|
||||
@ -120,7 +117,8 @@ fn simulation_schedule_keyed_events() {
|
||||
|
||||
#[test]
|
||||
fn simulation_schedule_periodic_events() {
|
||||
let (mut simu, t0, addr, mut output) = simple_bench();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let (mut simu, addr, mut output) = passthrough_bench(t0);
|
||||
|
||||
// Queue 2 periodic events at t0 + 3s + k*2s.
|
||||
simu.schedule_periodic_event(
|
||||
@ -155,7 +153,8 @@ fn simulation_schedule_periodic_events() {
|
||||
|
||||
#[test]
|
||||
fn simulation_schedule_periodic_keyed_events() {
|
||||
let (mut simu, t0, addr, mut output) = simple_bench();
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
let (mut simu, addr, mut output) = passthrough_bench(t0);
|
||||
|
||||
// Queue 2 periodic events at t0 + 3s + k*2s.
|
||||
simu.schedule_periodic_event(
|
||||
@ -197,3 +196,222 @@ fn simulation_schedule_periodic_keyed_events() {
|
||||
assert!(output.next().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
use std::time::{Instant, SystemTime};
|
||||
|
||||
#[cfg(not(miri))]
|
||||
use asynchronix::time::{AutoSystemClock, Clock, SystemClock};
|
||||
|
||||
// Model that outputs timestamps at init and each time its input is triggered.
|
||||
#[cfg(not(miri))]
|
||||
#[derive(Default)]
|
||||
struct TimestampModel {
|
||||
pub stamp: Output<(Instant, SystemTime)>,
|
||||
}
|
||||
#[cfg(not(miri))]
|
||||
impl TimestampModel {
|
||||
pub async fn trigger(&mut self) {
|
||||
self.stamp.send((Instant::now(), SystemTime::now())).await;
|
||||
}
|
||||
}
|
||||
#[cfg(not(miri))]
|
||||
impl Model for TimestampModel {
|
||||
fn init(
|
||||
mut self,
|
||||
_scheduler: &asynchronix::time::Scheduler<Self>,
|
||||
) -> std::pin::Pin<
|
||||
Box<
|
||||
dyn futures_util::Future<Output = asynchronix::model::InitializedModel<Self>>
|
||||
+ Send
|
||||
+ '_,
|
||||
>,
|
||||
> {
|
||||
Box::pin(async {
|
||||
self.stamp.send((Instant::now(), SystemTime::now())).await;
|
||||
|
||||
self.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple bench containing a single timestamping model with a custom clock.
|
||||
#[cfg(not(miri))]
|
||||
fn timestamp_bench(
|
||||
t0: MonotonicTime,
|
||||
clock: impl Clock + 'static,
|
||||
) -> (
|
||||
Simulation,
|
||||
Address<TimestampModel>,
|
||||
EventStream<(Instant, SystemTime)>,
|
||||
) {
|
||||
// Bench assembly.
|
||||
let mut model = TimestampModel::default();
|
||||
let mbox = Mailbox::new();
|
||||
|
||||
let stamp_stream = model.stamp.connect_stream().0;
|
||||
let addr = mbox.address();
|
||||
|
||||
let simu = SimInit::new()
|
||||
.add_model(model, mbox)
|
||||
.init_with_clock(t0, clock);
|
||||
|
||||
(simu, addr, stamp_stream)
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn simulation_system_clock_from_instant() {
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
const TOLERANCE: f64 = 0.0005; // [s]
|
||||
|
||||
// The reference simulation time is set in the past of t0 so that the
|
||||
// simulation starts in the future when the reference wall clock time is
|
||||
// close to the wall clock time when the simulation in initialized.
|
||||
let simulation_ref_offset = 0.3; // [s] must be greater than any `instant_offset`.
|
||||
let simulation_ref = t0 - Duration::from_secs_f64(simulation_ref_offset);
|
||||
|
||||
// Test reference wall clock times in the near past and near future.
|
||||
for wall_clock_offset in [-0.1, 0.1] {
|
||||
// The clock reference is the current time offset by `instant_offset`.
|
||||
let wall_clock_init = Instant::now();
|
||||
let wall_clock_ref = if wall_clock_offset >= 0.0 {
|
||||
wall_clock_init + Duration::from_secs_f64(wall_clock_offset)
|
||||
} else {
|
||||
wall_clock_init - Duration::from_secs_f64(-wall_clock_offset)
|
||||
};
|
||||
|
||||
let clock = SystemClock::from_instant(simulation_ref, wall_clock_ref);
|
||||
|
||||
let (mut simu, addr, mut stamp) = timestamp_bench(t0, clock);
|
||||
|
||||
// Queue a single event at t0 + 0.1s.
|
||||
simu.schedule_event(
|
||||
Duration::from_secs_f64(0.1),
|
||||
TimestampModel::trigger,
|
||||
(),
|
||||
&addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check the stamps.
|
||||
for expected_time in [
|
||||
simulation_ref_offset + wall_clock_offset,
|
||||
simulation_ref_offset + wall_clock_offset + 0.1,
|
||||
] {
|
||||
let measured_time = (stamp.next().unwrap().0 - wall_clock_init).as_secs_f64();
|
||||
assert!(
|
||||
(expected_time - measured_time).abs() <= TOLERANCE,
|
||||
"Expected t = {:.6}s +/- {:.6}s, measured t = {:.6}s",
|
||||
expected_time,
|
||||
TOLERANCE,
|
||||
measured_time,
|
||||
);
|
||||
|
||||
simu.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn simulation_system_clock_from_system_time() {
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
const TOLERANCE: f64 = 0.005; // [s]
|
||||
|
||||
// The reference simulation time is set in the past of t0 so that the
|
||||
// simulation starts in the future when the reference wall clock time is
|
||||
// close to the wall clock time when the simulation in initialized.
|
||||
let simulation_ref_offset = 0.3; // [s] must be greater than any `instant_offset`.
|
||||
let simulation_ref = t0 - Duration::from_secs_f64(simulation_ref_offset);
|
||||
|
||||
// Test reference wall clock times in the near past and near future.
|
||||
for wall_clock_offset in [-0.1, 0.1] {
|
||||
// The clock reference is the current time offset by `instant_offset`.
|
||||
let wall_clock_init = SystemTime::now();
|
||||
let wall_clock_ref = if wall_clock_offset >= 0.0 {
|
||||
wall_clock_init + Duration::from_secs_f64(wall_clock_offset)
|
||||
} else {
|
||||
wall_clock_init - Duration::from_secs_f64(-wall_clock_offset)
|
||||
};
|
||||
|
||||
let clock = SystemClock::from_system_time(simulation_ref, wall_clock_ref);
|
||||
|
||||
let (mut simu, addr, mut stamp) = timestamp_bench(t0, clock);
|
||||
|
||||
// Queue a single event at t0 + 0.1s.
|
||||
simu.schedule_event(
|
||||
Duration::from_secs_f64(0.1),
|
||||
TimestampModel::trigger,
|
||||
(),
|
||||
&addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check the stamps.
|
||||
for expected_time in [
|
||||
simulation_ref_offset + wall_clock_offset,
|
||||
simulation_ref_offset + wall_clock_offset + 0.1,
|
||||
] {
|
||||
let measured_time = stamp
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.duration_since(wall_clock_init)
|
||||
.unwrap()
|
||||
.as_secs_f64();
|
||||
assert!(
|
||||
(expected_time - measured_time).abs() <= TOLERANCE,
|
||||
"Expected t = {:.6}s +/- {:.6}s, measured t = {:.6}s",
|
||||
expected_time,
|
||||
TOLERANCE,
|
||||
measured_time,
|
||||
);
|
||||
|
||||
simu.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn simulation_auto_system_clock() {
|
||||
let t0 = MonotonicTime::EPOCH;
|
||||
const TOLERANCE: f64 = 0.005; // [s]
|
||||
|
||||
let (mut simu, addr, mut stamp) = timestamp_bench(t0, AutoSystemClock::new());
|
||||
let instant_t0 = Instant::now();
|
||||
|
||||
// Queue a periodic event at t0 + 0.2s + k*0.2s.
|
||||
simu.schedule_periodic_event(
|
||||
Duration::from_secs_f64(0.2),
|
||||
Duration::from_secs_f64(0.2),
|
||||
TimestampModel::trigger,
|
||||
(),
|
||||
&addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Queue a single event at t0 + 0.3s.
|
||||
simu.schedule_event(
|
||||
Duration::from_secs_f64(0.3),
|
||||
TimestampModel::trigger,
|
||||
(),
|
||||
&addr,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check the stamps.
|
||||
for expected_time in [0.0, 0.2, 0.3, 0.4, 0.6] {
|
||||
let measured_time = (stamp.next().unwrap().0 - instant_t0).as_secs_f64();
|
||||
assert!(
|
||||
(expected_time - measured_time).abs() <= TOLERANCE,
|
||||
"Expected t = {:.6}s +/- {:.6}s, measured t = {:.6}s",
|
||||
expected_time,
|
||||
TOLERANCE,
|
||||
measured_time,
|
||||
);
|
||||
|
||||
simu.step();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user