-
Notifications
You must be signed in to change notification settings - Fork 37
sim-lib: add simulated clock to speed up simulations #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
carlaKC
wants to merge
5
commits into
bitcoin-dev-project:main
Choose a base branch
from
carlaKC:81-simulatedtime
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
eb4d719
sim-lib: add clock trait to mock time with default impl
carlaKC 3fbe84f
sim-lib: add implementation of simulated clock
carlaKC 79080b4
sim-lib: use sleep for timed shutdown instead of timeout
carlaKC 352e5dc
sim-lib/feat: make Simulation generic over Clock
carlaKC 8bb2b6e
sim-lib: use Clock trait to create network graph for sim-node
carlaKC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
use async_trait::async_trait; | ||
use std::ops::{Div, Mul}; | ||
use std::time::{Duration, SystemTime}; | ||
use tokio::time::{self, Instant}; | ||
|
||
use crate::SimulationError; | ||
|
||
#[async_trait] | ||
pub trait Clock: Send + Sync { | ||
fn now(&self) -> SystemTime; | ||
async fn sleep(&self, wait: Duration); | ||
} | ||
|
||
/// Provides a wall clock implementation of the Clock trait. | ||
#[derive(Clone)] | ||
pub struct SystemClock {} | ||
|
||
#[async_trait] | ||
impl Clock for SystemClock { | ||
fn now(&self) -> SystemTime { | ||
SystemTime::now() | ||
} | ||
|
||
async fn sleep(&self, wait: Duration) { | ||
time::sleep(wait).await; | ||
} | ||
} | ||
|
||
/// Provides an implementation of the Clock trait that speeds up wall clock time by some factor. | ||
#[derive(Clone)] | ||
pub struct SimulationClock { | ||
// The multiplier that the regular wall clock is sped up by. | ||
speedup_multiplier: u32, | ||
|
||
/// Tracked so that we can calculate our "fast-forwarded" present relative to the time that we started running at. | ||
/// This is useful, because it allows us to still rely on the wall clock, then just convert based on our speedup. | ||
/// This field is expressed as an Instant for convenience. | ||
start_instant: Instant, | ||
} | ||
|
||
impl SimulationClock { | ||
/// Creates a new simulated clock that will speed up wall clock time by the multiplier provided, which must be in | ||
/// [1;1000] because our asynchronous sleep only supports a duration of ms granularity. | ||
pub fn new(speedup_multiplier: u32) -> Result<Self, SimulationError> { | ||
if speedup_multiplier < 1 { | ||
return Err(SimulationError::SimulatedNetworkError( | ||
"speedup_multiplier must be at least 1".to_string(), | ||
)); | ||
} | ||
|
||
if speedup_multiplier > 1000 { | ||
return Err(SimulationError::SimulatedNetworkError( | ||
"speedup_multiplier must be less than 1000, because the simulation sleeps with millisecond | ||
granularity".to_string(), | ||
)); | ||
} | ||
|
||
Ok(SimulationClock { | ||
speedup_multiplier, | ||
start_instant: Instant::now(), | ||
}) | ||
} | ||
|
||
/// Calculates the current simulation time based on the current wall clock time. | ||
/// | ||
/// Separated for testing purposes so that we can fix the current wall clock time and elapsed interval. | ||
fn calc_now(&self, now: SystemTime, elapsed: Duration) -> SystemTime { | ||
now.checked_add(self.simulated_to_wall_clock(elapsed)) | ||
.expect("simulation time overflow") | ||
} | ||
|
||
/// Converts a duration expressed in wall clock time to the amount of equivalent time that should be used in our | ||
/// sped up time. | ||
fn wall_clock_to_simulated(&self, d: Duration) -> Duration { | ||
d.div(self.speedup_multiplier) | ||
} | ||
|
||
/// Converts a duration expressed in sped up simulation time to the be expressed in wall clock time. | ||
fn simulated_to_wall_clock(&self, d: Duration) -> Duration { | ||
d.mul(self.speedup_multiplier) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl Clock for SimulationClock { | ||
/// To get the current time according to our simulation clock, we get the amount of wall clock time that has | ||
/// elapsed since the simulator clock was created and multiply it by our speedup. | ||
fn now(&self) -> SystemTime { | ||
self.calc_now(SystemTime::now(), self.start_instant.elapsed()) | ||
} | ||
|
||
/// To provide a sped up sleep time, we scale the proposed wait time by our multiplier and sleep. | ||
async fn sleep(&self, wait: Duration) { | ||
time::sleep(self.wall_clock_to_simulated(wait)).await; | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::time::{Duration, SystemTime}; | ||
|
||
use crate::clock::SimulationClock; | ||
|
||
/// Tests validation and that a multplier of 1 is a regular clock. | ||
#[test] | ||
fn test_simulation_clock() { | ||
assert!(SimulationClock::new(0).is_err()); | ||
assert!(SimulationClock::new(1001).is_err()); | ||
|
||
let clock = SimulationClock::new(1).unwrap(); | ||
let now = SystemTime::now(); | ||
let elapsed = Duration::from_secs(15); | ||
|
||
assert_eq!( | ||
clock.calc_now(now, elapsed), | ||
now.checked_add(elapsed).unwrap(), | ||
); | ||
} | ||
|
||
/// Test that time is sped up by multiplier. | ||
#[test] | ||
fn test_clock_speedup() { | ||
let clock = SimulationClock::new(10).unwrap(); | ||
let now = SystemTime::now(); | ||
|
||
assert_eq!( | ||
clock.calc_now(now, Duration::from_secs(1)), | ||
now.checked_add(Duration::from_secs(10)).unwrap(), | ||
); | ||
|
||
assert_eq!( | ||
clock.calc_now(now, Duration::from_secs(50)), | ||
now.checked_add(Duration::from_secs(500)).unwrap(), | ||
); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since this can only be in the [1..1000] range, maybe it could be a
u16
instead?