1
0
forked from ROMEO/nexosim

Merge pull request #9 from asynchronics/feature/clock

Add support for custom/real-time clocks
This commit is contained in:
Serge Barral 2023-08-29 12:58:03 +02:00 committed by GitHub
commit 9d78e4f72a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 579 additions and 59 deletions

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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);
}
};
}

View File

@ -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> {

View File

@ -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 {

View File

@ -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::{

View 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),
}
}
}

View File

@ -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

View File

@ -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();
}
}