Skip to main content

vortex_trace/
replay.rs

1//! Generic deterministic replay framework.
2//!
3//! Since simulation is deterministic (same seed = same execution), we can
4//! replay to any point and examine state. This module provides a generic
5//! [`ReplayRunner`] that wraps any user-provided simulation function.
6//!
7//! Inspired by rr (Mozilla) — concept freely usable, independent implementation.
8
9use crate::{SimTrace, TraceEvent};
10
11/// A generic replay runner parameterized on user state type `S`.
12///
13/// The user provides a `sim_fn` that takes `(seed, target_tick)` and returns
14/// `(state, trace)`. The replay runner uses this to implement time-travel
15/// debugging primitives.
16///
17/// ```
18/// use vortex_trace::replay::ReplayRunner;
19/// use vortex_trace::SimTrace;
20///
21/// // Example: replay a simple counter simulation
22/// let runner = ReplayRunner::new(42, |seed, ticks| {
23///     let mut trace = SimTrace::new();
24///     let mut counter = 0u64;
25///     for t in 0..ticks {
26///         counter += 1;
27///         // Record events to trace as needed
28///     }
29///     (counter, trace)
30/// });
31///
32/// let (state, _trace) = runner.replay_to_tick(100);
33/// assert_eq!(state, 100);
34/// ```
35pub struct ReplayRunner<S, F>
36where
37    F: Fn(u64, u64) -> (S, SimTrace),
38{
39    seed: u64,
40    sim_fn: F,
41    _phantom: std::marker::PhantomData<S>,
42}
43
44impl<S, F> ReplayRunner<S, F>
45where
46    F: Fn(u64, u64) -> (S, SimTrace),
47{
48    /// Create a new replay runner.
49    ///
50    /// - `seed`: the deterministic seed for the simulation
51    /// - `sim_fn`: `(seed, target_tick) -> (state, trace)`
52    pub fn new(seed: u64, sim_fn: F) -> Self {
53        Self {
54            seed,
55            sim_fn,
56            _phantom: std::marker::PhantomData,
57        }
58    }
59
60    /// Replay the simulation to a specific tick.
61    ///
62    /// Creates a fresh simulation from the seed and runs to `target_tick`.
63    pub fn replay_to_tick(&self, target_tick: u64) -> (S, SimTrace) {
64        (self.sim_fn)(self.seed, target_tick)
65    }
66
67    /// Replay a window: run to `end_tick`, return events in `[start_tick, end_tick]`.
68    pub fn replay_window(&self, start_tick: u64, end_tick: u64) -> (S, Vec<TraceEvent>) {
69        let (state, trace) = (self.sim_fn)(self.seed, end_tick);
70        let window_events: Vec<TraceEvent> = trace
71            .events()
72            .iter()
73            .filter(|e| e.tick >= start_tick && e.tick <= end_tick)
74            .cloned()
75            .collect();
76        (state, window_events)
77    }
78
79    /// The seed used by this runner.
80    pub fn seed(&self) -> u64 {
81        self.seed
82    }
83}
84
85/// Replay until a predicate is satisfied (binary-search friendly).
86///
87/// Runs the simulation at increasing tick counts until `predicate` returns
88/// `true` or `max_ticks` is reached. Returns the tick where the predicate
89/// was first satisfied.
90///
91/// This is more efficient than running tick-by-tick because it allows the
92/// user's `sim_fn` to batch-run ticks internally.
93pub fn replay_until<S, F, P>(
94    seed: u64,
95    max_ticks: u64,
96    step: u64,
97    sim_fn: F,
98    predicate: P,
99) -> Option<u64>
100where
101    F: Fn(u64, u64) -> (S, SimTrace),
102    P: Fn(&S, &SimTrace) -> bool,
103{
104    let mut tick = step;
105    while tick <= max_ticks {
106        let (state, trace) = sim_fn(seed, tick);
107        if predicate(&state, &trace) {
108            // Found it — now binary search for the exact tick
109            if tick <= step {
110                return Some(tick);
111            }
112            let mut lo = tick - step;
113            let mut hi = tick;
114            while lo + 1 < hi {
115                let mid = lo + (hi - lo) / 2;
116                let (s, t) = sim_fn(seed, mid);
117                if predicate(&s, &t) {
118                    hi = mid;
119                } else {
120                    lo = mid;
121                }
122            }
123            return Some(hi);
124        }
125        tick += step;
126    }
127    None
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::TraceEventKind;
134
135    fn counter_sim(_seed: u64, ticks: u64) -> (u64, SimTrace) {
136        let mut trace = SimTrace::new();
137        let mut counter = 0u64;
138        for t in 1..=ticks {
139            counter += 1;
140            if t % 10 == 0 {
141                trace.record(
142                    t,
143                    1,
144                    TraceEventKind::Custom {
145                        tag: "counter".into(),
146                        data: format!("{counter}"),
147                    },
148                );
149            }
150        }
151        (counter, trace)
152    }
153
154    #[test]
155    fn test_replay_to_tick() {
156        let runner = ReplayRunner::new(42, counter_sim);
157        let (state, _trace) = runner.replay_to_tick(100);
158        assert_eq!(state, 100);
159    }
160
161    #[test]
162    fn test_replay_deterministic() {
163        let runner = ReplayRunner::new(42, counter_sim);
164        let (s1, t1) = runner.replay_to_tick(500);
165        let (s2, t2) = runner.replay_to_tick(500);
166        assert_eq!(s1, s2);
167        assert_eq!(t1.len(), t2.len());
168    }
169
170    #[test]
171    fn test_replay_window() {
172        let runner = ReplayRunner::new(42, counter_sim);
173        let (state, events) = runner.replay_window(50, 100);
174        assert_eq!(state, 100);
175        // Events at ticks 50, 60, 70, 80, 90, 100 — 6 events in window
176        assert_eq!(events.len(), 6);
177        assert!(events.iter().all(|e| e.tick >= 50 && e.tick <= 100));
178    }
179
180    #[test]
181    fn test_replay_until_found() {
182        let tick = replay_until(42, 1000, 10, counter_sim, |state, _trace| *state >= 75);
183        assert!(tick.is_some());
184        assert_eq!(tick.unwrap(), 75);
185    }
186
187    #[test]
188    fn test_replay_until_not_found() {
189        let tick = replay_until(42, 50, 10, counter_sim, |state, _trace| *state >= 100);
190        assert!(tick.is_none());
191    }
192
193    #[test]
194    fn test_replay_seed() {
195        let runner = ReplayRunner::new(123, counter_sim);
196        assert_eq!(runner.seed(), 123);
197    }
198}