Skip to main content

vortex_trace/
minimize.rs

1//! Trace minimization for failing simulation seeds.
2//!
3//! When a swarm run finds a failing seed over many ticks, debugging is hard.
4//! The minimizer finds the shortest trace that still reproduces the bug.
5//!
6//! Inspired by Delta Debugging (Zeller, "Simplifying and Isolating
7//! Failure-Inducing Input", 2002). Independent implementation.
8
9/// Result of trace minimization.
10#[derive(Debug, Clone)]
11pub struct MinimizedTrace {
12    /// Original tick duration.
13    pub original_ticks: u64,
14    /// Minimized tick duration that still reproduces the failure.
15    pub minimized_ticks: u64,
16    /// Original number of fault events.
17    pub original_faults: usize,
18    /// Minimized number of fault events.
19    pub minimized_faults: usize,
20    /// The essential fault indices (into the original schedule).
21    pub essential_fault_indices: Vec<usize>,
22}
23
24/// Minimize the tick duration needed to reproduce a failure.
25///
26/// Binary searches on the tick count to find the minimum number of ticks
27/// that still triggers the invariant violation.
28///
29/// - `reproduce_fn`: `(ticks) -> bool` — returns true if the failure reproduces
30///   at the given tick count.
31/// - `max_ticks`: upper bound (must reproduce at this count).
32/// - `min_ticks`: lower bound for the search (default: 1).
33///
34/// ```
35/// use vortex_trace::minimize::minimize_ticks;
36///
37/// // Example: find minimum ticks for a bug that appears after tick 750
38/// let result = minimize_ticks(|ticks| ticks >= 750, 2000, 1);
39/// assert!(result.minimized_ticks >= 750);
40/// assert!(result.minimized_ticks < 800); // Binary search precision
41/// ```
42pub fn minimize_ticks<F>(reproduce_fn: F, max_ticks: u64, min_ticks: u64) -> MinimizedTrace
43where
44    F: Fn(u64) -> bool,
45{
46    // Verify the failure reproduces at max_ticks
47    if !reproduce_fn(max_ticks) {
48        return MinimizedTrace {
49            original_ticks: max_ticks,
50            minimized_ticks: max_ticks,
51            original_faults: 0,
52            minimized_faults: 0,
53            essential_fault_indices: Vec::new(),
54        };
55    }
56
57    let mut lo = min_ticks;
58    let mut hi = max_ticks;
59
60    while lo + 1 < hi {
61        let mid = lo + (hi - lo) / 2;
62        if reproduce_fn(mid) {
63            hi = mid;
64        } else {
65            lo = mid;
66        }
67    }
68
69    MinimizedTrace {
70        original_ticks: max_ticks,
71        minimized_ticks: hi,
72        original_faults: 0,
73        minimized_faults: 0,
74        essential_fault_indices: Vec::new(),
75    }
76}
77
78/// Minimize a fault schedule using delta debugging.
79///
80/// Tries removing faults one by one (with paired events like crash/restart
81/// handled together). Returns the indices of essential faults.
82///
83/// - `num_faults`: total number of fault events in the original schedule.
84/// - `is_paired`: `(idx) -> Option<usize>` returns the paired event index
85///   (e.g., restart for a crash, heal for a partition).
86/// - `reproduce_with`: `(&[usize]) -> bool` takes a list of *active* fault
87///   indices and returns whether the failure reproduces.
88///
89/// ```
90/// use vortex_trace::minimize::minimize_faults;
91///
92/// // Example: 10 faults, only fault 3 is essential
93/// let result = minimize_faults(
94///     10,
95///     |_| None,  // no paired events
96///     |active| active.contains(&3),  // only reproduces if fault 3 is active
97/// );
98/// assert!(result.essential_fault_indices.contains(&3));
99/// assert!(result.minimized_faults <= 2); // fault 3 (+ maybe its pair)
100/// ```
101pub fn minimize_faults<P, R>(num_faults: usize, is_paired: P, reproduce_with: R) -> MinimizedTrace
102where
103    P: Fn(usize) -> Option<usize>,
104    R: Fn(&[usize]) -> bool,
105{
106    let mut active: Vec<bool> = vec![true; num_faults];
107
108    // Try removing each fault (and its pair)
109    for idx in 0..num_faults {
110        if !active[idx] {
111            continue;
112        }
113
114        let paired = is_paired(idx);
115
116        // Tentatively remove
117        active[idx] = false;
118        if let Some(pair_idx) = paired {
119            active[pair_idx] = false;
120        }
121
122        // Check if failure still reproduces without this fault
123        let active_indices: Vec<usize> = active
124            .iter()
125            .enumerate()
126            .filter(|(_, a)| **a)
127            .map(|(i, _)| i)
128            .collect();
129
130        if !reproduce_with(&active_indices) {
131            // This fault is essential — put it back
132            active[idx] = true;
133            if let Some(pair_idx) = paired {
134                active[pair_idx] = true;
135            }
136        }
137    }
138
139    let essential: Vec<usize> = active
140        .iter()
141        .enumerate()
142        .filter(|(_, a)| **a)
143        .map(|(i, _)| i)
144        .collect();
145
146    MinimizedTrace {
147        original_ticks: 0,
148        minimized_ticks: 0,
149        original_faults: num_faults,
150        minimized_faults: essential.len(),
151        essential_fault_indices: essential,
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_minimize_ticks_binary_search() {
161        // Bug appears at tick >= 500
162        let result = minimize_ticks(|ticks| ticks >= 500, 2000, 1);
163        assert_eq!(result.minimized_ticks, 500);
164        assert_eq!(result.original_ticks, 2000);
165    }
166
167    #[test]
168    fn test_minimize_ticks_no_reproduction() {
169        // Bug never reproduces
170        let result = minimize_ticks(|_| false, 1000, 1);
171        assert_eq!(result.minimized_ticks, 1000);
172    }
173
174    #[test]
175    fn test_minimize_ticks_always_reproduces() {
176        // Bug reproduces at any tick count — binary search converges to min_ticks + 1
177        // because lo starts at min_ticks and loop condition is lo + 1 < hi
178        let result = minimize_ticks(|_| true, 1000, 1);
179        assert_eq!(result.minimized_ticks, 2);
180
181        // With min_ticks=0, converges to 1
182        let result = minimize_ticks(|_| true, 1000, 0);
183        assert_eq!(result.minimized_ticks, 1);
184    }
185
186    #[test]
187    fn test_minimize_faults_single_essential() {
188        // 10 faults, only fault 5 is essential
189        let result = minimize_faults(10, |_| None, |active| active.contains(&5));
190        assert_eq!(result.minimized_faults, 1);
191        assert_eq!(result.essential_fault_indices, vec![5]);
192    }
193
194    #[test]
195    fn test_minimize_faults_multiple_essential() {
196        // Faults 2 AND 7 are both needed
197        let result = minimize_faults(
198            10,
199            |_| None,
200            |active| active.contains(&2) && active.contains(&7),
201        );
202        assert_eq!(result.minimized_faults, 2);
203        assert!(result.essential_fault_indices.contains(&2));
204        assert!(result.essential_fault_indices.contains(&7));
205    }
206
207    #[test]
208    fn test_minimize_faults_with_pairs() {
209        // Fault 0 (crash) is paired with fault 1 (restart), fault 2 is essential
210        let result = minimize_faults(
211            4,
212            |idx| match idx {
213                0 => Some(1), // crash -> restart
214                1 => Some(0), // restart -> crash
215                _ => None,
216            },
217            |active| active.contains(&2),
218        );
219        assert!(result.essential_fault_indices.contains(&2));
220        // Crash/restart pair (0,1) should have been removed since they're not essential
221        assert!(!result.essential_fault_indices.contains(&0));
222        assert!(!result.essential_fault_indices.contains(&1));
223    }
224
225    #[test]
226    fn test_minimize_faults_all_essential() {
227        // All faults are needed
228        let result = minimize_faults(3, |_| None, |active| active.len() == 3);
229        assert_eq!(result.minimized_faults, 3);
230    }
231
232    #[test]
233    fn test_minimize_faults_none_essential() {
234        // Bug reproduces with no faults
235        let result = minimize_faults(5, |_| None, |_| true);
236        assert_eq!(result.minimized_faults, 0);
237        assert!(result.essential_fault_indices.is_empty());
238    }
239}