A deterministic async runtime for Rust that lets you record and replay executions exactly. I built this to debug race conditions and concurrency bugs that are otherwise impossible to reproduce.
The core idea is simple: nothing happens unless the executor permits it. There's no real time, no actual randomness, no OS scheduler to inject non-determinism. Everything runs on a single thread with a virtual clock, so the same code with the same inputs produces the same behavior every single time.
The runtime mocks three things that normally introduce non-determinism:
time - sleep(10) doesn't actually sleep. It registers a callback for tick 10 on a virtual clock, then yields control. When no tasks are ready, the executor fast-forwards to the next scheduled tick.
randomness - MockRng is a seeded xorshift64 generator. Same seed, same sequence. Every generated value is recorded in the trace.
task ordering - Tasks run in a deterministic order based on when they were spawned and when their timers fire. No thread scheduling surprises.
Every operation gets logged to a trace that you can export as JSON, then load into the TUI debugger to step through execution one event at a time.
cargo run --example simple_demo
cargo run --bin chronos_cli demo_trace.jsonThe simple_demo spawns a few tasks with different sleep durations and logs some output. You'll see a demo_trace.json file appear, which you can load into the debugger.
For something more interesting, try the race condition demo:
cargo run --example race_condition
cargo run --bin chronos_cli crash.jsonThis one simulates a classic TOCTOU bug where two tasks read a shared balance, sleep for random durations, then write back their modifications. Depending on the interleaving, one task's write can clobber the other's. The demo loops through seeds until it finds a buggy execution, then saves that trace so you can step through exactly what happened.
use chronos_runtime::{Runtime, spawn, sleep, log, MockRng};
let rt = Runtime::new();
rt.block_on(async {
log!("starting");
spawn(async {
sleep(10).await;
log!("task 1 done");
});
spawn(async {
let mut rng = MockRng::new(42);
let delay = rng.gen_range(1..20);
sleep(delay).await;
log!("task 2 done after {} ticks", delay);
});
});
let trace = rt.export_trace();
std::fs::write("trace.json", trace.to_json().unwrap()).unwrap();The TUI has three views you can switch between with Tab:
- timeline shows task execution visually across time
- events lists every spawn, poll, completion, timer, rng call, and log message
- output shows just the log messages in order
Navigation is vim-style: h/l or arrow keys to step through time, j/k to select events, enter to jump, g/G for start/end.
If you have a trace from a buggy run, you can replay it:
let trace = Trace::from_json(&json).unwrap();
let rt = Runtime::from_trace(trace);
rt.block_on(async { /* same code as before */ });The runtime replays recorded RNG outputs and re-executes deterministically (single-threaded scheduler + virtual time), so runs are reproducible for deterministic workloads.
Chronos mocks time, randomness, and task scheduling, but it doesn't mock I/O. If your code makes actual network requests, reads from files, or calls into the OS, those operations will introduce non-determinism and break replay. For truly reproducible tests, you'll want to mock or stub any external I/O before running under Chronos.
chronos_runtime/ the deterministic runtime
chronos_cli/ tui debugger
examples/ demo programs
The runtime is self-contained with no async runtime dependencies. It implements its own executor, waker, and future polling.