Skip to content

Commit b8a563c

Browse files
authored
Merge pull request #242 from carlaKC/81-simulatedtime
sim-lib: add simulated clock to speed up simulations
2 parents 3dc2eb7 + 1c91815 commit b8a563c

File tree

5 files changed

+231
-31
lines changed

5 files changed

+231
-31
lines changed

sim-cli/src/parsing.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use bitcoin::secp256k1::PublicKey;
33
use clap::{builder::TypedValueParser, Parser};
44
use log::LevelFilter;
55
use serde::{Deserialize, Serialize};
6+
use simln_lib::clock::SimulationClock;
67
use simln_lib::sim_node::{
78
ln_node_from_graph, populate_network_graph, ChannelPolicy, SimGraph, SimulatedChannel,
89
};
@@ -83,6 +84,10 @@ pub struct Cli {
8384
/// Seed to run random activity generator deterministically
8485
#[clap(long, short)]
8586
pub fix_seed: Option<u64>,
87+
/// A multiplier to wall time to speed up the simulation's clock. Only available when when running on a network of
88+
/// simulated nodes.
89+
#[clap(long)]
90+
pub speedup_clock: Option<u16>,
8691
}
8792

8893
impl Cli {
@@ -102,6 +107,12 @@ impl Cli {
102107
nodes or sim_graph to run with simulated nodes"
103108
));
104109
}
110+
if !sim_params.nodes.is_empty() && self.speedup_clock.is_some() {
111+
return Err(anyhow!(
112+
"Clock speedup is only allowed when running on a simulated network"
113+
));
114+
}
115+
105116
Ok(())
106117
}
107118
}
@@ -198,7 +209,7 @@ pub async fn create_simulation_with_network(
198209
cli: &Cli,
199210
sim_params: &SimParams,
200211
tasks: TaskTracker,
201-
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
212+
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
202213
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
203214
let SimParams {
204215
nodes: _,
@@ -228,11 +239,13 @@ pub async fn create_simulation_with_network(
228239
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
229240
));
230241

242+
let clock = Arc::new(SimulationClock::new(cli.speedup_clock.unwrap_or(1))?);
243+
231244
// Copy all simulated channels into a read-only routing graph, allowing to pathfind for
232245
// individual payments without locking th simulation graph (this is a duplication of the channels,
233246
// but the performance tradeoff is worthwhile for concurrent pathfinding).
234247
let routing_graph = Arc::new(
235-
populate_network_graph(channels)
248+
populate_network_graph(channels, clock.clone())
236249
.map_err(|e| SimulationError::SimulatedNetworkError(format!("{:?}", e)))?,
237250
);
238251

@@ -241,7 +254,14 @@ pub async fn create_simulation_with_network(
241254
get_validated_activities(&nodes, nodes_info, sim_params.activity.clone()).await?;
242255

243256
Ok((
244-
Simulation::new(cfg, nodes, tasks, shutdown_trigger, shutdown_listener),
257+
Simulation::new(
258+
cfg,
259+
nodes,
260+
tasks,
261+
clock,
262+
shutdown_trigger,
263+
shutdown_listener,
264+
),
245265
validated_activities,
246266
))
247267
}
@@ -252,7 +272,7 @@ pub async fn create_simulation(
252272
cli: &Cli,
253273
sim_params: &SimParams,
254274
tasks: TaskTracker,
255-
) -> Result<(Simulation, Vec<ActivityDefinition>), anyhow::Error> {
275+
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
256276
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
257277
let SimParams {
258278
nodes,
@@ -267,7 +287,16 @@ pub async fn create_simulation(
267287
get_validated_activities(&clients, clients_info, sim_params.activity.clone()).await?;
268288

269289
Ok((
270-
Simulation::new(cfg, clients, tasks, shutdown_trigger, shutdown_listener),
290+
Simulation::new(
291+
cfg,
292+
clients,
293+
tasks,
294+
// When running on a real network, the underlying node may use wall time so we always use a clock with no
295+
// speedup.
296+
Arc::new(SimulationClock::new(1)?),
297+
shutdown_trigger,
298+
shutdown_listener,
299+
),
271300
validated_activities,
272301
))
273302
}

simln-lib/src/clock.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use async_trait::async_trait;
2+
use std::ops::{Div, Mul};
3+
use std::time::{Duration, SystemTime};
4+
use tokio::time::{self, Instant};
5+
6+
use crate::SimulationError;
7+
8+
#[async_trait]
9+
pub trait Clock: Send + Sync {
10+
fn now(&self) -> SystemTime;
11+
async fn sleep(&self, wait: Duration);
12+
}
13+
14+
/// Provides a wall clock implementation of the Clock trait.
15+
#[derive(Clone)]
16+
pub struct SystemClock {}
17+
18+
#[async_trait]
19+
impl Clock for SystemClock {
20+
fn now(&self) -> SystemTime {
21+
SystemTime::now()
22+
}
23+
24+
async fn sleep(&self, wait: Duration) {
25+
time::sleep(wait).await;
26+
}
27+
}
28+
29+
/// Provides an implementation of the Clock trait that speeds up wall clock time by some factor.
30+
#[derive(Clone)]
31+
pub struct SimulationClock {
32+
// The multiplier that the regular wall clock is sped up by, must be in [1, 1000].
33+
speedup_multiplier: u16,
34+
35+
/// Tracked so that we can calculate our "fast-forwarded" present relative to the time that we started running at.
36+
/// This is useful, because it allows us to still rely on the wall clock, then just convert based on our speedup.
37+
/// This field is expressed as an Instant for convenience.
38+
start_instant: Instant,
39+
}
40+
41+
impl SimulationClock {
42+
/// Creates a new simulated clock that will speed up wall clock time by the multiplier provided, which must be in
43+
/// [1;1000] because our asynchronous sleep only supports a duration of ms granularity.
44+
pub fn new(speedup_multiplier: u16) -> Result<Self, SimulationError> {
45+
if speedup_multiplier < 1 {
46+
return Err(SimulationError::SimulatedNetworkError(
47+
"speedup_multiplier must be at least 1".to_string(),
48+
));
49+
}
50+
51+
if speedup_multiplier > 1000 {
52+
return Err(SimulationError::SimulatedNetworkError(
53+
"speedup_multiplier must be less than 1000, because the simulation sleeps with millisecond
54+
granularity".to_string(),
55+
));
56+
}
57+
58+
Ok(SimulationClock {
59+
speedup_multiplier,
60+
start_instant: Instant::now(),
61+
})
62+
}
63+
64+
/// Calculates the current simulation time based on the current wall clock time.
65+
///
66+
/// Separated for testing purposes so that we can fix the current wall clock time and elapsed interval.
67+
fn calc_now(&self, now: SystemTime, elapsed: Duration) -> SystemTime {
68+
now.checked_add(self.simulated_to_wall_clock(elapsed))
69+
.expect("simulation time overflow")
70+
}
71+
72+
/// Converts a duration expressed in wall clock time to the amount of equivalent time that should be used in our
73+
/// sped up time.
74+
fn wall_clock_to_simulated(&self, d: Duration) -> Duration {
75+
d.div(self.speedup_multiplier.into())
76+
}
77+
78+
/// Converts a duration expressed in sped up simulation time to the be expressed in wall clock time.
79+
fn simulated_to_wall_clock(&self, d: Duration) -> Duration {
80+
d.mul(self.speedup_multiplier.into())
81+
}
82+
}
83+
84+
#[async_trait]
85+
impl Clock for SimulationClock {
86+
/// To get the current time according to our simulation clock, we get the amount of wall clock time that has
87+
/// elapsed since the simulator clock was created and multiply it by our speedup.
88+
fn now(&self) -> SystemTime {
89+
self.calc_now(SystemTime::now(), self.start_instant.elapsed())
90+
}
91+
92+
/// To provide a sped up sleep time, we scale the proposed wait time by our multiplier and sleep.
93+
async fn sleep(&self, wait: Duration) {
94+
time::sleep(self.wall_clock_to_simulated(wait)).await;
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use std::time::{Duration, SystemTime};
101+
102+
use crate::clock::SimulationClock;
103+
104+
/// Tests validation and that a multplier of 1 is a regular clock.
105+
#[test]
106+
fn test_simulation_clock() {
107+
assert!(SimulationClock::new(0).is_err());
108+
assert!(SimulationClock::new(1001).is_err());
109+
110+
let clock = SimulationClock::new(1).unwrap();
111+
let now = SystemTime::now();
112+
let elapsed = Duration::from_secs(15);
113+
114+
assert_eq!(
115+
clock.calc_now(now, elapsed),
116+
now.checked_add(elapsed).unwrap(),
117+
);
118+
}
119+
120+
/// Test that time is sped up by multiplier.
121+
#[test]
122+
fn test_clock_speedup() {
123+
let clock = SimulationClock::new(10).unwrap();
124+
let now = SystemTime::now();
125+
126+
assert_eq!(
127+
clock.calc_now(now, Duration::from_secs(1)),
128+
now.checked_add(Duration::from_secs(10)).unwrap(),
129+
);
130+
131+
assert_eq!(
132+
clock.calc_now(now, Duration::from_secs(50)),
133+
now.checked_add(Duration::from_secs(500)).unwrap(),
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)