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}