diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d26bb91..e25d852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/asynchronix/Cargo.toml b/asynchronix/Cargo.toml index e6c9f45..2d6b306 100644 --- a/asynchronix/Cargo.toml +++ b/asynchronix/Cargo.toml @@ -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] diff --git a/asynchronix/src/lib.rs b/asynchronix/src/lib.rs index 76974d8..c27bd70 100644 --- a/asynchronix/src/lib.rs +++ b/asynchronix/src/lib.rs @@ -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 diff --git a/asynchronix/src/simulation.rs b/asynchronix/src/simulation.rs index d3ff941..1fba49f 100644 --- a/asynchronix/src/simulation.rs +++ b/asynchronix/src/simulation.rs @@ -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>, time: SyncCell, + clock: Box, } 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>, + time: SyncCell, + 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 { // 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, ) -> Box { @@ -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); } }; } diff --git a/asynchronix/src/simulation/mailbox.rs b/asynchronix/src/simulation/mailbox.rs index 76f91c9..76eb84d 100644 --- a/asynchronix/src/simulation/mailbox.rs +++ b/asynchronix/src/simulation/mailbox.rs @@ -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(pub(crate) Receiver); impl Mailbox { diff --git a/asynchronix/src/simulation/sim_init.rs b/asynchronix/src/simulation/sim_init.rs index 4cf4e8f..dbfa7ff 100644 --- a/asynchronix/src/simulation/sim_init.rs +++ b/asynchronix/src/simulation/sim_init.rs @@ -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 { diff --git a/asynchronix/src/time.rs b/asynchronix/src/time.rs index dc63e5b..fc8232e 100644 --- a/asynchronix/src/time.rs +++ b/asynchronix/src/time.rs @@ -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::{ diff --git a/asynchronix/src/time/clock.rs b/asynchronix/src/time/clock.rs new file mode 100644 index 0000000..816bcb5 --- /dev/null +++ b/asynchronix/src/time/clock.rs @@ -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, +} + +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), + } + } +} diff --git a/asynchronix/src/time/monotonic_time.rs b/asynchronix/src/time/monotonic_time.rs index ad59af0..85a5579 100644 --- a/asynchronix/src/time/monotonic_time.rs +++ b/asynchronix/src/time/monotonic_time.rs @@ -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 diff --git a/asynchronix/tests/simulation_scheduling.rs b/asynchronix/tests/simulation_scheduling.rs index 59b5003..858f81e 100644 --- a/asynchronix/tests/simulation_scheduling.rs +++ b/asynchronix/tests/simulation_scheduling.rs @@ -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 { pub output: Output, } @@ -23,13 +23,10 @@ impl PassThroughModel { impl Model for PassThroughModel {} /// A simple bench containing a single pass-through model (input forwarded to -/// output). -fn simple_bench() -> ( - Simulation, - MonotonicTime, - Address>, - EventStream, -) { +/// output) running as fast as possible. +fn passthrough_bench( + t0: MonotonicTime, +) -> (Simulation, Address>, EventStream) { // Bench assembly. let mut model = PassThroughModel::new(); let mbox = Mailbox::new(); @@ -37,16 +34,15 @@ fn simple_bench() -> ( 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, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future> + + 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, + 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(); + } +}