Use time-paused tokio for zero sleeps#309
Conversation
Switch from sim-ln's old wall-clock SimulationClock (a fixed u16 speedup that still slept on real time) to its reimplemented SimulationClock, which advances virtual time on a paused single-threaded runtime. A run now completes as fast as the CPU allows instead of sleeping on real time, and is reproducible for a fixed seed. Tracks bitcoin-dev-project/sim-ln#309. - Pin simln-lib/sim-cli to the PR head. - Drive all three binaries through runtime::block_on_sped_up_clock, which owns the paused runtime; main() is therefore synchronous. - Reimplement the local InstantClock trait on tokio::time::Instant so that elapsed time tracks virtual time (no ln-resource-mgr changes required). - Seed the latency interceptor from a shared SIM_SEED, matching sim-ln's seeded RNG. - Remove the obsolete --clock-speedup multiplier flag; SimulationClock no longer takes one. - Bound the run at one virtual year as a safeguard, since virtual time has no wall-clock limit if an attack never triggers shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bd2a169 to
2b7c4d5
Compare
Switch from sim-ln's old wall-clock SimulationClock (a fixed u16 speedup that still slept on real time) to its reimplemented SimulationClock, which advances virtual time on a paused single-threaded runtime. A run now completes as fast as the CPU allows instead of sleeping on real time, and is reproducible for a fixed seed. Tracks bitcoin-dev-project/sim-ln#309. - Pin simln-lib/sim-cli to the PR head, enabling simln-lib's virtual-time feature (required for runtime::block_on_virtual_time, which pulls in tokio's test-util to drive the paused runtime). - Drive all three binaries through runtime::block_on_virtual_time, which owns the paused runtime; main() is therefore synchronous. - Reimplement the local InstantClock trait on tokio::time::Instant so that elapsed time tracks virtual time (no ln-resource-mgr changes required). - Seed the simulation config and latency interceptor from a shared SIM_SEED so that payment generation and sampled delays are reproducible. - Remove the obsolete --clock-speedup multiplier flag; SimulationClock no longer takes one. - Bound the run at one virtual year as a safeguard, since virtual time has no wall-clock limit if an attack never triggers shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move off sim-ln's old wall-clock SimulationClock (a fixed speedup multiplier that still slept on real time) onto its reimplemented virtual-time SimulationClock, which advances time on a paused single-threaded runtime so a run completes as fast as the CPU allows. - Pin simln-lib/sim-cli to the virtual-time PR head and enable simln-lib's virtual-time feature, which provides runtime::block_on_virtual_time. - Drive all three binaries through block_on_virtual_time, which owns the paused runtime and hands the simulation its clock; main() is therefore synchronous. - Adopt the new SimulationClock::new(start_time) constructor in place of the speedup-multiplier form across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so that elapsed time tracks virtual time. - Pass None to the latency interceptor's new seed parameter for now; seeding follows in a later commit. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
elnosh
left a comment
There was a problem hiding this comment.
ran a couple of simulations and it is indeed faster - a 6-month simulation that previously took hours now ran in 27 mins. However, I did run into an issue explained here carlaKC/jam-ln#133 (review) which is that any polling loop could cause issues if the comments on clock.rs are accurate
If the system never reaches the state where all tasks are waiting, the clock will not
advance. This may happen if there is an always-pollable loop, for example.
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
elnosh
left a comment
There was a problem hiding this comment.
Discussed offline and runtime introduced here doesn't seem to be the culprit for that specific simulation taking longer.
LGTM - some small nits
| // LDK only allows channel announcements up to 24h in the future. Use a fixed timestamp so that even if virtual | ||
| // time has advanced dramatically, we won't hit that limit. |
There was a problem hiding this comment.
if I'm understanding the behavior of SimulationClock correctly, then this comment is inaccurate (?) Line below calls clocks.now() which if running with virtual time runtime then this could return something far in the future. I guess it works fine because this method is called at the beginning when setting up the network so virtual time prob has not advanced much but still comment seems false.
There was a problem hiding this comment.
Yeah comment needs to go.
I guess it works fine because this method is called at the beginning when setting up the network so virtual time prob has not advanced much
We don't have any sleeps between clock creation and calling this, so the clock will be exactly at the wall clock time we started it with.
| mockall = "0.13.1" | ||
| futures = "0.3.31" | ||
| tempfile = "3" | ||
| # Features used only by the test suite: macros for #[tokio::test] and rt-multi-thread for multi-threaded test runtimes. |
There was a problem hiding this comment.
nit: i don't love these self-explanatory claude comments. Feel free to ignore tho
There was a problem hiding this comment.
Fair, I was on the fence about them - will remove.
| Ok(validated_activities) | ||
| } | ||
|
|
||
| async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result<PathBuf> { |
| let interceptors = if latency > 0 { | ||
| vec![Arc::new(LatencyIntercepor::new_poisson( | ||
| latency as f32, | ||
| cli.fix_seed, | ||
| )?) as Arc<dyn Interceptor>] |
There was a problem hiding this comment.
this could be in the branch where we call create_simulation_with_network since we only use it there.
| "Virtual time is only allowed when running on a simulated network; real nodes run on wall time" | ||
| )); | ||
| } | ||
| if self.total_time.is_none() { |
Replace the wall-time multiplier SimulationClock (a u16 speedup capped at 1000x) and the separate SystemClock with a single SimulationClock that reports time relative to a SystemTime start anchor. now() is the start time plus a captured tokio::time::Instant's elapsed(), and sleep() defers to the tokio timer. How the speedup works: when this clock is driven on a paused tokio runtime (start_paused), the runtime freezes the Instant clock and, once no task is runnable, auto-advances it to the nearest pending timer. So sleep() resolves with no real time elapsed and now() jumps straight to the next scheduled event, letting a simulation run as fast as the CPU allows while staying reproducible. On a regular (non-paused) runtime the exact same clock simply tracks real time relative to its start, which is how real-node runs use it. The paused runtime needs tokio's test-util at runtime, so it is gated behind the new opt-in `virtual-time` feature rather than always compiled in. Tests construct the clock with SystemTime::now() (live time) rather than a fixed epoch: LDK's network-graph gossip validation checks channel_update timestamps against the real wall clock and rejects anything older than two weeks, so a hardcoded past start time makes populate_network_graph fail. This commit only lands the clock itself. The runtime helper that drives it on a paused runtime and the CLI opt-in are removed here for the sake of atomic commits and reintroduced in later commits; the old --speedup-clock flag and its validation are dropped accordingly, so for now every run tracks real time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running a simulation on virtual time needs a single-threaded, time-paused runtime: single threaded so scheduling is deterministic, paused so virtual time auto-advances to the next timer instead of waiting on the wall clock. Leaving callers to set this up invites silently losing determinism or the virtual-time advance. Add runtime::block_on_virtual_time, which owns that runtime, constructs a SimulationClock on it, and runs a caller-provided build-and-run closure to completion. It rejects being called from within an existing runtime (returning the new SimulationError::RuntimeError) rather than panicking on a nested block_on. The module is gated behind the virtual-time feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nothing in the parse_sim_params call stack actually awaits: it only does blocking std::fs reads and a blocking dialoguer prompt. Drop the vestigial async/await from parse_sim_params, read_sim_path and select_sim_file. This is preparation for the following commit, which runs the simulation on a paused virtual-time runtime. There, main() becomes a plain sync fn that picks a runtime only after parsing, so parsing must not require an async context of its own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add --virtual-time, an opt-in flag for simulated networks. When set, the simulation runs on the library's paused single-threaded runtime, so virtual time advances instantly to the next event: a multi-day run finishes in seconds and is reproducible for a given seed. When absent, the simulation runs on a regular multi-threaded runtime in real time, as before. main parses and validates the simulation params once up front, then selects the runtime by the flag: virtual-time runs go through block_on_virtual_time (which owns the paused runtime and hands back a SimulationClock); regular runs use a standard runtime. The flag is validated to require a simulated network and --total-time (an unbounded run would otherwise advance virtual time forever). sim-cli enables simln-lib's virtual-time feature so the flag is always available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pin simln-lib/sim-cli to the virtual-time PR head (enabling simln-lib's virtual-time feature) and adapt to the reimplemented SimulationClock: - Construct the clock with the new SimulationClock::new(start_time) signature in place of the speedup multiplier, across the binaries, tests and graph setup. - Reimplement the local InstantClock trait on tokio::time::Instant so elapsed time tracks virtual time. - Pass the latency interceptor's new seed parameter (None for now; seeding follows in a later commit). The binaries still build their own clock and run on a standard runtime; moving them onto the paused virtual-time runtime happens next. Tracks bitcoin-dev-project/sim-ln#309. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Our current solution to waits is to use a sped up wall clock, which has all sorts of limitations.
Using a paused, single-threaded tokio runtime we can get discrete-event-like functionality without doing a large refactor.