1use shape_runtime::snapshot::SnapshotStore;
7use shape_value::ValueWord;
8use std::collections::VecDeque;
9
10#[derive(Debug, Clone)]
12pub enum CaptureMode {
13 FunctionBoundaries,
15 EveryNInstructions(u64),
17 Breakpoints(Vec<usize>),
19 Disabled,
21}
22
23impl Default for CaptureMode {
24 fn default() -> Self {
25 Self::Disabled
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct VmSnapshot {
32 pub index: u64,
34 pub ip: usize,
36 pub sp: usize,
38 pub call_depth: usize,
40 pub function_id: Option<u16>,
42 pub function_name: Option<String>,
44 pub instruction_count: u64,
46 pub stack_snapshot: Vec<ValueWord>,
48 pub module_bindings: Vec<ValueWord>,
50 pub reason: CaptureReason,
52}
53
54#[derive(Debug, Clone)]
56pub enum CaptureReason {
57 FunctionEntry(String),
58 FunctionExit(String),
59 InstructionInterval(u64),
60 Breakpoint(usize),
61 Manual,
62}
63
64impl std::fmt::Display for CaptureReason {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::FunctionEntry(name) => write!(f, "function entry: {name}"),
68 Self::FunctionExit(name) => write!(f, "function exit: {name}"),
69 Self::InstructionInterval(n) => write!(f, "every {n} instructions"),
70 Self::Breakpoint(ip) => write!(f, "breakpoint at ip={ip}"),
71 Self::Manual => write!(f, "manual capture"),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct TimeTravelConfig {
79 pub capture_mode: CaptureMode,
81 pub max_snapshots: usize,
83}
84
85impl Default for TimeTravelConfig {
86 fn default() -> Self {
87 Self {
88 capture_mode: CaptureMode::Disabled,
89 max_snapshots: 10_000,
90 }
91 }
92}
93
94pub struct TimeTravel {
96 config: TimeTravelConfig,
97 snapshots: VecDeque<VmSnapshot>,
99 cursor: usize,
101 next_index: u64,
103 instruction_counter: u64,
105 snapshot_store: Option<SnapshotStore>,
107}
108
109impl TimeTravel {
110 pub fn with_config(config: TimeTravelConfig) -> Self {
112 Self {
113 config,
114 snapshots: VecDeque::new(),
115 cursor: 0,
116 next_index: 0,
117 instruction_counter: 0,
118 snapshot_store: None,
119 }
120 }
121
122 pub fn new(mode: CaptureMode, max_entries: usize) -> Self {
128 Self::with_config(TimeTravelConfig {
129 capture_mode: mode,
130 max_snapshots: max_entries,
131 })
132 }
133
134 pub fn disabled() -> Self {
136 Self::with_config(TimeTravelConfig::default())
137 }
138
139 #[inline]
149 pub fn should_capture(
150 &mut self,
151 ip: usize,
152 _instruction_count: u64,
153 is_call_or_return: bool,
154 ) -> bool {
155 match &self.config.capture_mode {
156 CaptureMode::Disabled => false,
157 CaptureMode::FunctionBoundaries => is_call_or_return,
158 CaptureMode::EveryNInstructions(n) => {
159 self.instruction_counter += 1;
160 if self.instruction_counter >= *n {
161 self.instruction_counter = 0;
162 true
163 } else {
164 false
165 }
166 }
167 CaptureMode::Breakpoints(bps) => bps.contains(&ip),
168 }
169 }
170
171 pub fn on_function_entry(&mut self) -> bool {
173 matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
174 }
175
176 pub fn on_function_exit(&mut self) -> bool {
178 matches!(self.config.capture_mode, CaptureMode::FunctionBoundaries)
179 }
180
181 pub fn record(
188 &mut self,
189 _snapshot: shape_runtime::snapshot::VmSnapshot,
190 ip: usize,
191 instruction_count: u64,
192 call_depth: usize,
193 ) -> usize {
194 let internal = VmSnapshot {
195 index: self.next_index,
196 ip,
197 sp: 0,
198 call_depth,
199 function_id: None,
200 function_name: None,
201 instruction_count,
202 stack_snapshot: vec![],
203 module_bindings: vec![],
204 reason: CaptureReason::Manual,
205 };
206 self.capture(internal);
207 self.snapshots.len().saturating_sub(1)
208 }
209
210 pub fn snapshot_store(&mut self) -> Result<&SnapshotStore, String> {
215 if self.snapshot_store.is_none() {
216 let tmp = std::env::temp_dir().join("shape_time_travel");
217 self.snapshot_store = Some(
218 SnapshotStore::new(&tmp)
219 .map_err(|e| format!("failed to create snapshot store: {}", e))?,
220 );
221 }
222 Ok(self.snapshot_store.as_ref().unwrap())
223 }
224
225 pub fn capture(&mut self, snapshot: VmSnapshot) {
227 if self.snapshots.len() >= self.config.max_snapshots {
228 self.snapshots.pop_front();
229 if self.cursor > 0 {
231 self.cursor -= 1;
232 }
233 }
234 self.snapshots.push_back(snapshot);
235 self.cursor = self.snapshots.len().saturating_sub(1);
236 self.next_index += 1;
237 }
238
239 pub fn build_snapshot(
241 &self,
242 ip: usize,
243 sp: usize,
244 call_depth: usize,
245 function_id: Option<u16>,
246 function_name: Option<String>,
247 instruction_count: u64,
248 stack: &[ValueWord],
249 module_bindings: &[ValueWord],
250 reason: CaptureReason,
251 ) -> VmSnapshot {
252 VmSnapshot {
253 index: self.next_index,
254 ip,
255 sp,
256 call_depth,
257 function_id,
258 function_name,
259 instruction_count,
260 stack_snapshot: stack[..sp.min(stack.len())].to_vec(),
261 module_bindings: module_bindings.to_vec(),
262 reason,
263 }
264 }
265
266 pub fn step_back(&mut self) -> Option<&VmSnapshot> {
270 if self.cursor > 0 {
271 self.cursor -= 1;
272 }
273 self.snapshots.get(self.cursor)
274 }
275
276 pub fn step_forward(&mut self) -> Option<&VmSnapshot> {
278 if self.cursor + 1 < self.snapshots.len() {
279 self.cursor += 1;
280 }
281 self.snapshots.get(self.cursor)
282 }
283
284 pub fn goto(&mut self, index: u64) -> Option<&VmSnapshot> {
286 if let Some(pos) = self.snapshots.iter().position(|s| s.index == index) {
287 self.cursor = pos;
288 self.snapshots.get(self.cursor)
289 } else {
290 None
291 }
292 }
293
294 pub fn current(&self) -> Option<&VmSnapshot> {
296 self.snapshots.get(self.cursor)
297 }
298
299 pub fn latest(&self) -> Option<&VmSnapshot> {
301 self.snapshots.back()
302 }
303
304 pub fn snapshot_count(&self) -> usize {
306 self.snapshots.len()
307 }
308
309 pub fn cursor_position(&self) -> usize {
311 self.cursor
312 }
313
314 pub fn is_enabled(&self) -> bool {
316 !matches!(self.config.capture_mode, CaptureMode::Disabled)
317 }
318
319 pub fn clear(&mut self) {
321 self.snapshots.clear();
322 self.cursor = 0;
323 }
324
325 pub fn context_window(&self, radius: usize) -> Vec<&VmSnapshot> {
327 let start = self.cursor.saturating_sub(radius);
328 let end = (self.cursor + radius + 1).min(self.snapshots.len());
329 self.snapshots.range(start..end).collect()
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 fn make_snapshot(_tt: &TimeTravel, idx_override: u64, reason: CaptureReason) -> VmSnapshot {
338 VmSnapshot {
339 index: idx_override,
340 ip: 0,
341 sp: 0,
342 call_depth: 0,
343 function_id: None,
344 function_name: None,
345 instruction_count: 0,
346 stack_snapshot: vec![],
347 module_bindings: vec![],
348 reason,
349 }
350 }
351
352 #[test]
353 fn test_disabled_no_captures() {
354 let mut tt = TimeTravel::disabled();
355 assert!(!tt.should_capture(0, 0, false));
356 assert!(!tt.is_enabled());
357 }
358
359 #[test]
360 fn test_interval_capture() {
361 let mut tt = TimeTravel::with_config(TimeTravelConfig {
362 capture_mode: CaptureMode::EveryNInstructions(3),
363 max_snapshots: 100,
364 });
365
366 assert!(!tt.should_capture(0, 1, false)); assert!(!tt.should_capture(1, 2, false)); assert!(tt.should_capture(2, 3, false)); assert!(!tt.should_capture(3, 4, false)); }
371
372 #[test]
373 fn test_breakpoint_capture() {
374 let mut tt = TimeTravel::with_config(TimeTravelConfig {
375 capture_mode: CaptureMode::Breakpoints(vec![10, 20, 30]),
376 max_snapshots: 100,
377 });
378
379 assert!(!tt.should_capture(5, 1, false));
380 assert!(tt.should_capture(10, 2, false));
381 assert!(!tt.should_capture(15, 3, false));
382 assert!(tt.should_capture(20, 4, false));
383 }
384
385 #[test]
386 fn test_function_boundary_capture() {
387 let mut tt = TimeTravel::with_config(TimeTravelConfig {
388 capture_mode: CaptureMode::FunctionBoundaries,
389 max_snapshots: 100,
390 });
391
392 assert!(!tt.should_capture(0, 1, false));
394 assert!(tt.should_capture(0, 2, true));
396 }
397
398 #[test]
399 fn test_navigation() {
400 let mut tt = TimeTravel::with_config(TimeTravelConfig {
401 capture_mode: CaptureMode::FunctionBoundaries,
402 max_snapshots: 100,
403 });
404
405 for i in 0..5 {
406 let snap = make_snapshot(&tt, i, CaptureReason::FunctionEntry(format!("fn_{i}")));
407 tt.capture(VmSnapshot { index: i, ..snap });
408 tt.next_index = i + 1;
409 }
410
411 assert_eq!(tt.snapshot_count(), 5);
412 assert_eq!(tt.cursor_position(), 4); let prev = tt.step_back().unwrap();
416 assert_eq!(prev.index, 3);
417 assert_eq!(tt.cursor_position(), 3);
418
419 let next = tt.step_forward().unwrap();
421 assert_eq!(next.index, 4);
422
423 let target = tt.goto(1).unwrap();
425 assert_eq!(target.index, 1);
426 }
427
428 #[test]
429 fn test_ring_buffer_eviction() {
430 let mut tt = TimeTravel::with_config(TimeTravelConfig {
431 capture_mode: CaptureMode::FunctionBoundaries,
432 max_snapshots: 3,
433 });
434
435 for i in 0..5u64 {
436 tt.capture(VmSnapshot {
437 index: i,
438 ip: i as usize,
439 sp: 0,
440 call_depth: 0,
441 function_id: None,
442 function_name: None,
443 instruction_count: i,
444 stack_snapshot: vec![],
445 module_bindings: vec![],
446 reason: CaptureReason::Manual,
447 });
448 }
449
450 assert_eq!(tt.snapshot_count(), 3);
451 assert_eq!(tt.snapshots.front().unwrap().index, 2);
453 }
454
455 #[test]
456 fn test_context_window() {
457 let mut tt = TimeTravel::with_config(TimeTravelConfig {
458 capture_mode: CaptureMode::FunctionBoundaries,
459 max_snapshots: 100,
460 });
461
462 for i in 0..10u64 {
463 tt.capture(VmSnapshot {
464 index: i,
465 ip: 0,
466 sp: 0,
467 call_depth: 0,
468 function_id: None,
469 function_name: None,
470 instruction_count: 0,
471 stack_snapshot: vec![],
472 module_bindings: vec![],
473 reason: CaptureReason::Manual,
474 });
475 }
476
477 tt.goto(5);
478 let window = tt.context_window(2);
479 assert_eq!(window.len(), 5); assert_eq!(window[0].index, 3);
481 assert_eq!(window[4].index, 7);
482 }
483
484 #[test]
485 fn test_function_boundary_mode() {
486 let mut tt = TimeTravel::with_config(TimeTravelConfig {
487 capture_mode: CaptureMode::FunctionBoundaries,
488 max_snapshots: 100,
489 });
490
491 assert!(tt.on_function_entry());
492 assert!(tt.on_function_exit());
493 assert!(tt.is_enabled());
494 }
495
496 #[test]
497 fn test_clear() {
498 let mut tt = TimeTravel::with_config(TimeTravelConfig {
499 capture_mode: CaptureMode::FunctionBoundaries,
500 max_snapshots: 100,
501 });
502
503 tt.capture(VmSnapshot {
504 index: 0,
505 ip: 0,
506 sp: 0,
507 call_depth: 0,
508 function_id: None,
509 function_name: None,
510 instruction_count: 0,
511 stack_snapshot: vec![],
512 module_bindings: vec![],
513 reason: CaptureReason::Manual,
514 });
515
516 assert_eq!(tt.snapshot_count(), 1);
517 tt.clear();
518 assert_eq!(tt.snapshot_count(), 0);
519 }
520}