1use crate::app::{CuSimApplication, CurrentRuntimeCopperList};
11use crate::curuntime::KeyFrame;
12use crate::reflect::{ReflectTaskIntrospection, TypeRegistry, dump_type_registry_schema};
13use crate::simulation::SimOverride;
14use bincode::config::standard;
15use bincode::decode_from_std_read;
16use bincode::error::DecodeError;
17use cu29_clock::{CuTime, RobotClock, RobotClockMock};
18use cu29_traits::{CopperListTuple, CuError, CuResult, UnifiedLogType};
19use cu29_unifiedlog::{
20 LogPosition, SectionHeader, SectionStorage, UnifiedLogRead, UnifiedLogWrite, UnifiedLogger,
21 UnifiedLoggerBuilder, UnifiedLoggerRead,
22};
23use std::collections::{HashMap, VecDeque};
24use std::io;
25use std::marker::PhantomData;
26use std::path::Path;
27use std::sync::Arc;
28
29#[derive(Debug, Clone)]
31pub struct JumpOutcome {
32 pub culistid: u64,
34 pub keyframe_culistid: Option<u64>,
36 pub replayed: usize,
38}
39
40#[derive(Debug, Clone, Copy)]
42pub struct SectionCacheStats {
43 pub cap: usize,
44 pub entries: usize,
45 pub hits: u64,
46 pub misses: u64,
47 pub evictions: u64,
48}
49
50#[allow(dead_code)]
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub(crate) enum IndexedResolveMode {
53 Exact,
54 AtOrAfter,
55 AtOrBefore,
56}
57
58#[derive(Debug, Clone)]
60pub(crate) struct SectionIndexEntry {
61 pub(crate) pos: LogPosition,
62 pub(crate) start_idx: usize,
63 pub(crate) len: usize,
64 pub(crate) first_id: u64,
65 pub(crate) last_id: u64,
66 pub(crate) first_ts: Option<CuTime>,
67 pub(crate) last_ts: Option<CuTime>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) struct LogIndexProgress {
72 pub(crate) scanned_bytes: u64,
73 pub(crate) total_bytes: u64,
74 pub(crate) indexed_entries: usize,
75}
76
77#[derive(Debug, Clone)]
79struct CachedSection<P: CopperListTuple> {
80 entries: Vec<Arc<crate::copperlist::CopperList<P>>>,
81 timestamps: Vec<Option<CuTime>>,
82}
83
84const DEFAULT_SECTION_CACHE_CAP: usize = 8;
91pub struct CuDebugSession<App, P, CB, TF, S, L>
92where
93 P: CopperListTuple,
94 S: SectionStorage,
95 L: UnifiedLogWrite<S> + 'static,
96{
97 app: App,
98 robot_clock: RobotClock,
99 clock_mock: RobotClockMock,
100 log_reader: UnifiedLoggerRead,
101 sections: Vec<SectionIndexEntry>,
102 total_entries: usize,
103 keyframes: Vec<KeyFrame>,
104 #[cfg(feature = "clock-sync")]
105 clock_replay: crate::clock_sync::ClockReplay,
106 started: bool,
107 current_idx: Option<usize>,
108 last_keyframe: Option<u64>,
109 build_callback: CB,
110 time_of: TF,
111 cache: HashMap<usize, CachedSection<P>>,
113 cache_order: VecDeque<usize>,
114 cache_cap: usize,
115 cache_hits: u64,
116 cache_misses: u64,
117 cache_evictions: u64,
118 phantom: PhantomData<(S, L)>,
119}
120
121impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
122where
123 App: CuSimApplication<S, L>,
124 L: UnifiedLogWrite<S> + 'static,
125 S: SectionStorage,
126 P: CopperListTuple + 'static,
127 CB: for<'a> Fn(
128 &'a crate::copperlist::CopperList<P>,
129 RobotClock,
130 RobotClockMock,
131 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
132 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
133{
134 pub fn from_log(
136 log_base: &Path,
137 app: App,
138 robot_clock: RobotClock,
139 clock_mock: RobotClockMock,
140 build_callback: CB,
141 time_of: TF,
142 ) -> CuResult<Self> {
143 Self::from_log_with_progress(
144 log_base,
145 app,
146 robot_clock,
147 clock_mock,
148 build_callback,
149 time_of,
150 |_| {},
151 )
152 }
153
154 pub(crate) fn from_log_with_progress(
155 log_base: &Path,
156 app: App,
157 robot_clock: RobotClock,
158 clock_mock: RobotClockMock,
159 build_callback: CB,
160 time_of: TF,
161 mut progress: impl FnMut(LogIndexProgress),
162 ) -> CuResult<Self> {
163 let _ = crate::logcodec::seed_effective_config_from_log::<P>(log_base)?;
164 let (sections, keyframes, total_entries) =
165 index_log_with_progress::<P, _, _>(log_base, &time_of, &mut progress)?;
166 let log_reader = build_read_logger(log_base)?;
167 #[allow(unused_mut)]
168 let mut session = Self::new(
169 log_reader,
170 app,
171 robot_clock,
172 clock_mock,
173 sections,
174 total_entries,
175 keyframes,
176 build_callback,
177 time_of,
178 );
179 #[cfg(feature = "clock-sync")]
180 {
181 session.clock_replay = crate::clock_sync::ClockReplay::new(
182 crate::clock_sync::load_clock_sync_records(log_base)?,
183 );
184 }
185 Ok(session)
186 }
187
188 pub fn from_log_with_cache_cap(
190 log_base: &Path,
191 app: App,
192 robot_clock: RobotClock,
193 clock_mock: RobotClockMock,
194 build_callback: CB,
195 time_of: TF,
196 cache_cap: usize,
197 ) -> CuResult<Self> {
198 Self::from_log_with_cache_cap_and_progress(
199 log_base,
200 app,
201 robot_clock,
202 clock_mock,
203 build_callback,
204 time_of,
205 cache_cap,
206 |_| {},
207 )
208 }
209
210 #[allow(clippy::too_many_arguments)]
211 pub(crate) fn from_log_with_cache_cap_and_progress(
212 log_base: &Path,
213 app: App,
214 robot_clock: RobotClock,
215 clock_mock: RobotClockMock,
216 build_callback: CB,
217 time_of: TF,
218 cache_cap: usize,
219 mut progress: impl FnMut(LogIndexProgress),
220 ) -> CuResult<Self> {
221 let _ = crate::logcodec::seed_effective_config_from_log::<P>(log_base)?;
222 let (sections, keyframes, total_entries) =
223 index_log_with_progress::<P, _, _>(log_base, &time_of, &mut progress)?;
224 let log_reader = build_read_logger(log_base)?;
225 #[allow(unused_mut)]
226 let mut session = Self::new_with_cache_cap(
227 log_reader,
228 app,
229 robot_clock,
230 clock_mock,
231 sections,
232 total_entries,
233 keyframes,
234 build_callback,
235 time_of,
236 cache_cap,
237 );
238 #[cfg(feature = "clock-sync")]
239 {
240 session.clock_replay = crate::clock_sync::ClockReplay::new(
241 crate::clock_sync::load_clock_sync_records(log_base)?,
242 );
243 }
244 Ok(session)
245 }
246
247 #[allow(clippy::too_many_arguments)]
249 pub(crate) fn new(
250 log_reader: UnifiedLoggerRead,
251 app: App,
252 robot_clock: RobotClock,
253 clock_mock: RobotClockMock,
254 sections: Vec<SectionIndexEntry>,
255 total_entries: usize,
256 keyframes: Vec<KeyFrame>,
257 build_callback: CB,
258 time_of: TF,
259 ) -> Self {
260 Self::new_with_cache_cap(
261 log_reader,
262 app,
263 robot_clock,
264 clock_mock,
265 sections,
266 total_entries,
267 keyframes,
268 build_callback,
269 time_of,
270 DEFAULT_SECTION_CACHE_CAP,
271 )
272 }
273
274 #[allow(clippy::too_many_arguments)]
275 pub(crate) fn new_with_cache_cap(
276 log_reader: UnifiedLoggerRead,
277 app: App,
278 robot_clock: RobotClock,
279 clock_mock: RobotClockMock,
280 sections: Vec<SectionIndexEntry>,
281 total_entries: usize,
282 keyframes: Vec<KeyFrame>,
283 build_callback: CB,
284 time_of: TF,
285 cache_cap: usize,
286 ) -> Self {
287 Self {
288 app,
289 robot_clock,
290 clock_mock,
291 log_reader,
292 sections,
293 total_entries,
294 keyframes,
295 #[cfg(feature = "clock-sync")]
296 clock_replay: crate::clock_sync::ClockReplay::new(Vec::new()),
297 started: false,
298 current_idx: None,
299 last_keyframe: None,
300 build_callback,
301 time_of,
302 cache: HashMap::new(),
303 cache_order: VecDeque::new(),
304 cache_cap: cache_cap.max(1),
305 cache_hits: 0,
306 cache_misses: 0,
307 cache_evictions: 0,
308 phantom: PhantomData,
309 }
310 }
311
312 #[inline]
313 pub fn app(&self) -> &App {
314 &self.app
315 }
316
317 #[inline]
318 pub fn app_mut(&mut self) -> &mut App {
319 &mut self.app
320 }
321
322 #[allow(deprecated)]
324 fn ensure_started(&mut self) -> CuResult<()> {
325 if self.started {
326 return Ok(());
327 }
328 #[cfg(feature = "clock-sync")]
329 if self.total_entries > 0 {
330 let (entry, time) = self.copperlist_at(0)?;
331 if let Some(time) = time {
332 self.clock_replay
333 .apply(&self.robot_clock, &self.clock_mock, entry.id, time)?;
334 }
335 }
336 let mut noop = |_step: App::Step<'_>| SimOverride::ExecuteByRuntime;
337 self.app.start_all_tasks(&mut noop)?;
338 self.started = true;
339 Ok(())
340 }
341
342 fn nearest_keyframe(&self, target_culistid: u64) -> Option<KeyFrame> {
343 nearest_replay_keyframe(&self.keyframes, target_culistid)
344 }
345
346 fn restore_keyframe(&mut self, kf: &KeyFrame) -> CuResult<()> {
347 self.app.restore_keyframe(kf)?;
348 #[cfg(feature = "clock-sync")]
349 self.clock_replay.apply(
350 &self.robot_clock,
351 &self.clock_mock,
352 kf.culistid,
353 kf.timestamp,
354 )?;
355 #[cfg(not(feature = "clock-sync"))]
356 self.clock_mock.set_value(kf.timestamp.as_nanos());
357 self.last_keyframe = Some(kf.culistid);
358 Ok(())
359 }
360
361 fn clear_runtime_copperlist_snapshot(&mut self)
362 where
363 App: CurrentRuntimeCopperList<P>,
364 {
365 self.app.set_current_runtime_copperlist_bytes(None);
366 }
367
368 fn normalize_runtime_copperlist_snapshot(
369 &mut self,
370 recorded: &crate::copperlist::CopperList<P>,
371 ) -> CuResult<()>
372 where
373 App: CurrentRuntimeCopperList<P>,
374 {
375 let normalized = self
376 .app
377 .current_runtime_copperlist_bytes()
378 .map(|bytes| {
379 let (mut runtime_cl, _) = bincode::decode_from_slice::<
380 crate::copperlist::CopperList<P>,
381 _,
382 >(bytes, standard())
383 .map_err(|e| {
384 CuError::new_with_cause("Failed to decode runtime CopperList snapshot", e)
385 })?;
386 runtime_cl.id = recorded.id;
387 bincode::encode_to_vec(&runtime_cl, standard()).map_err(|e| {
388 CuError::new_with_cause("Failed to encode normalized CopperList snapshot", e)
389 })
390 })
391 .transpose()?;
392 self.app.set_current_runtime_copperlist_bytes(normalized);
393 Ok(())
394 }
395
396 fn find_section_for_index(&self, idx: usize) -> Option<usize> {
397 self.sections
398 .binary_search_by(|s| {
399 if idx < s.start_idx {
400 std::cmp::Ordering::Greater
401 } else if idx >= s.start_idx + s.len {
402 std::cmp::Ordering::Less
403 } else {
404 std::cmp::Ordering::Equal
405 }
406 })
407 .ok()
408 }
409
410 fn find_section_for_culistid(&self, culistid: u64) -> Option<usize> {
411 self.sections
412 .binary_search_by(|s| {
413 if culistid < s.first_id {
414 std::cmp::Ordering::Greater
415 } else if culistid > s.last_id {
416 std::cmp::Ordering::Less
417 } else {
418 std::cmp::Ordering::Equal
419 }
420 })
421 .ok()
422 }
423
424 fn touch_cache(&mut self, key: usize) {
425 if let Some(pos) = self.cache_order.iter().position(|k| *k == key) {
426 self.cache_order.remove(pos);
427 }
428 self.cache_order.push_back(key);
429 while self.cache_order.len() > self.cache_cap {
430 if let Some(old) = self.cache_order.pop_front()
431 && self.cache.remove(&old).is_some()
432 {
433 self.cache_evictions = self.cache_evictions.saturating_add(1);
434 }
435 }
436 }
437
438 fn load_section(&mut self, section_idx: usize) -> CuResult<&CachedSection<P>> {
439 if self.cache.contains_key(§ion_idx) {
440 self.cache_hits = self.cache_hits.saturating_add(1);
441 self.touch_cache(section_idx);
442 return Ok(self.cache.get(§ion_idx).unwrap());
444 }
445 self.cache_misses = self.cache_misses.saturating_add(1);
446
447 let entry = &self.sections[section_idx];
448 let (header, data) = read_section_at(&mut self.log_reader, entry.pos)?;
449 if header.entry_type != UnifiedLogType::CopperList {
450 return Err(CuError::from(
451 "Section type mismatch while loading copperlists",
452 ));
453 }
454
455 let (entries, timestamps) = decode_copperlists::<P, _>(&data, &self.time_of)?;
456 let cached = CachedSection {
457 entries,
458 timestamps,
459 };
460 self.cache.insert(section_idx, cached);
461 self.touch_cache(section_idx);
462 Ok(self.cache.get(§ion_idx).unwrap())
463 }
464
465 fn copperlist_at(
466 &mut self,
467 idx: usize,
468 ) -> CuResult<(Arc<crate::copperlist::CopperList<P>>, Option<CuTime>)> {
469 let section_idx = self
470 .find_section_for_index(idx)
471 .ok_or_else(|| CuError::from("Index outside copperlist log"))?;
472 let start_idx = self.sections[section_idx].start_idx;
473 let section = self.load_section(section_idx)?;
474 let local = idx - start_idx;
475 let cl = section
476 .entries
477 .get(local)
478 .ok_or_else(|| CuError::from("Corrupt section index vs cache"))?
479 .clone();
480 let ts = section.timestamps.get(local).copied().unwrap_or(None);
481 Ok((cl, ts))
482 }
483
484 fn first_section_with_last_id_at_least(&self, culistid: u64) -> usize {
485 let mut left = 0usize;
486 let mut right = self.sections.len();
487 while left < right {
488 let mid = left + (right - left) / 2;
489 if self.sections[mid].last_id < culistid {
490 left = mid + 1;
491 } else {
492 right = mid;
493 }
494 }
495 left
496 }
497
498 fn first_section_with_first_id_greater_than(&self, culistid: u64) -> usize {
499 let mut left = 0usize;
500 let mut right = self.sections.len();
501 while left < right {
502 let mid = left + (right - left) / 2;
503 if self.sections[mid].first_id <= culistid {
504 left = mid + 1;
505 } else {
506 right = mid;
507 }
508 }
509 left
510 }
511
512 fn index_for_culistid_at_or_after(&mut self, culistid: u64) -> CuResult<usize> {
513 let mut section_idx = self.first_section_with_last_id_at_least(culistid);
514 while section_idx < self.sections.len() {
515 let start_idx = self.sections[section_idx].start_idx;
516 let section = self.load_section(section_idx)?;
517 for (offset, cl) in section.entries.iter().enumerate() {
518 if cl.id >= culistid {
519 return Ok(start_idx + offset);
520 }
521 }
522 section_idx += 1;
523 }
524 Err(CuError::from(format!("No CL at/after target {culistid}")))
525 }
526
527 fn index_for_culistid_at_or_before(&mut self, culistid: u64) -> CuResult<usize> {
528 let mut section_idx = self.first_section_with_first_id_greater_than(culistid);
529 while section_idx > 0 {
530 section_idx -= 1;
531 let start_idx = self.sections[section_idx].start_idx;
532 let section = self.load_section(section_idx)?;
533 for (offset, cl) in section.entries.iter().enumerate().rev() {
534 if cl.id <= culistid {
535 return Ok(start_idx + offset);
536 }
537 }
538 }
539 Err(CuError::from(format!("No CL at/before target {culistid}")))
540 }
541
542 fn index_for_culistid(&mut self, culistid: u64) -> CuResult<usize> {
543 let section_idx = self
544 .find_section_for_culistid(culistid)
545 .ok_or_else(|| CuError::from("Requested culistid not present in log"))?;
546 let start_idx = self.sections[section_idx].start_idx;
547 let section = self.load_section(section_idx)?;
548 for (offset, cl) in section.entries.iter().enumerate() {
549 if cl.id == culistid {
550 return Ok(start_idx + offset);
551 }
552 }
553 Err(CuError::from("culistid not found inside indexed section"))
554 }
555
556 pub(crate) fn resolve_index_for_culistid(
557 &mut self,
558 culistid: u64,
559 mode: IndexedResolveMode,
560 ) -> CuResult<usize> {
561 match mode {
562 IndexedResolveMode::Exact => self
563 .index_for_culistid(culistid)
564 .map_err(|_| CuError::from(format!("No exact CL target for {culistid}"))),
565 IndexedResolveMode::AtOrAfter => self.index_for_culistid_at_or_after(culistid),
566 IndexedResolveMode::AtOrBefore => self.index_for_culistid_at_or_before(culistid),
567 }
568 }
569
570 fn index_for_time_at_or_after(&mut self, ts: CuTime) -> CuResult<usize> {
571 for section_idx in 0..self.sections.len() {
572 let section_entry = &self.sections[section_idx];
573 if matches!(section_entry.last_ts, Some(last) if last < ts) {
574 continue;
575 }
576
577 let start_idx = section_entry.start_idx;
578 let section_first_ts = section_entry.first_ts;
579 let section = self.load_section(section_idx)?;
580 for (offset, maybe_ts) in section.timestamps.iter().enumerate() {
581 if matches!(maybe_ts, Some(entry_ts) if *entry_ts >= ts) {
582 return Ok(start_idx + offset);
583 }
584 }
585
586 if matches!(section_first_ts, Some(first) if first > ts) {
587 break;
588 }
589 }
590
591 Err(CuError::from(format!(
592 "No timestamp at/after {}",
593 ts.as_nanos()
594 )))
595 }
596
597 fn index_for_time_at_or_before(&mut self, ts: CuTime) -> CuResult<usize> {
598 for section_idx in (0..self.sections.len()).rev() {
599 let section_entry = &self.sections[section_idx];
600 if matches!(section_entry.first_ts, Some(first) if first > ts) {
601 continue;
602 }
603
604 let start_idx = section_entry.start_idx;
605 let section = self.load_section(section_idx)?;
606 for (offset, maybe_ts) in section.timestamps.iter().enumerate().rev() {
607 if matches!(maybe_ts, Some(entry_ts) if *entry_ts <= ts) {
608 return Ok(start_idx + offset);
609 }
610 }
611 }
612
613 Err(CuError::from(format!(
614 "No timestamp at/before {}",
615 ts.as_nanos()
616 )))
617 }
618
619 fn index_for_exact_time(&mut self, ts: CuTime) -> CuResult<usize> {
620 for section_idx in 0..self.sections.len() {
621 let section_entry = &self.sections[section_idx];
622 if matches!(section_entry.last_ts, Some(last) if last < ts) {
623 continue;
624 }
625 if matches!(section_entry.first_ts, Some(first) if first > ts) {
626 break;
627 }
628
629 let start_idx = section_entry.start_idx;
630 let section = self.load_section(section_idx)?;
631 for (offset, maybe_ts) in section.timestamps.iter().enumerate() {
632 if matches!(maybe_ts, Some(entry_ts) if *entry_ts == ts) {
633 return Ok(start_idx + offset);
634 }
635 }
636 }
637
638 Err(CuError::from(format!(
639 "No exact timestamp target for {}",
640 ts.as_nanos()
641 )))
642 }
643
644 fn index_for_time(&mut self, ts: CuTime) -> CuResult<usize> {
645 self.resolve_index_for_time(ts, IndexedResolveMode::AtOrAfter)
646 }
647
648 pub(crate) fn resolve_index_for_time(
649 &mut self,
650 ts: CuTime,
651 mode: IndexedResolveMode,
652 ) -> CuResult<usize> {
653 match mode {
654 IndexedResolveMode::Exact => self.index_for_exact_time(ts),
655 IndexedResolveMode::AtOrAfter => self.index_for_time_at_or_after(ts),
656 IndexedResolveMode::AtOrBefore => self.index_for_time_at_or_before(ts),
657 }
658 }
659
660 #[allow(deprecated)]
662 fn replay_range(&mut self, start: usize, end: usize) -> CuResult<usize>
663 where
664 App: CurrentRuntimeCopperList<P>,
665 {
666 let mut replayed = 0usize;
667 for idx in start..=end {
668 let (entry, ts) = self.copperlist_at(idx)?;
669 let restored = (idx == start)
670 .then_some(self.last_keyframe)
671 .flatten()
672 .filter(|boundary| *boundary == entry.id);
673 let expected = if idx > 0 {
674 self.copperlist_at(idx - 1)?
675 .0
676 .id
677 .checked_add(1)
678 .ok_or_else(|| CuError::from("Replay CopperList id overflow"))?
679 } else {
680 0
681 };
682 crate::continuity::validate_replay_continuity(expected, entry.id, restored)?;
683 if let Some(ts) = ts {
684 #[cfg(feature = "clock-sync")]
685 self.clock_replay
686 .apply(&self.robot_clock, &self.clock_mock, entry.id, ts)?;
687 #[cfg(not(feature = "clock-sync"))]
688 self.clock_mock.set_value(ts.as_nanos());
689 }
690 let mut cb = build_replay_callback(
691 &self.build_callback,
692 entry.as_ref(),
693 self.robot_clock.clone(),
694 self.clock_mock.clone(),
695 );
696 self.app.run_one_iteration(&mut cb)?;
697 self.normalize_runtime_copperlist_snapshot(entry.as_ref())?;
698 replayed += 1;
699 self.current_idx = Some(idx);
700 }
701 Ok(replayed)
702 }
703
704 pub(crate) fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome>
705 where
706 App: CurrentRuntimeCopperList<P>,
707 {
708 self.ensure_started()?;
709 if target_idx >= self.total_entries {
710 return Err(CuError::from("Target index outside log"));
711 }
712 let (target_cl, _) = self.copperlist_at(target_idx)?;
713 let target_culistid = target_cl.id;
714
715 let keyframe_used: Option<u64>;
716 let replay_start: usize;
717
718 if let Some(current) = self.current_idx {
720 if target_idx == current {
721 return Ok(JumpOutcome {
722 culistid: target_culistid,
723 keyframe_culistid: self.last_keyframe,
724 replayed: 0,
725 });
726 }
727
728 if target_idx >= current {
729 let nearest_keyframe = self.nearest_keyframe(target_culistid);
730 let nearest_keyframe_idx = nearest_keyframe
731 .as_ref()
732 .and_then(|kf| self.index_for_culistid(kf.culistid).ok());
733
734 if let (Some(kf), Some(kf_idx)) = (nearest_keyframe, nearest_keyframe_idx)
735 && kf_idx > current
736 {
737 self.restore_keyframe(&kf)?;
738 self.clear_runtime_copperlist_snapshot();
739 keyframe_used = Some(kf.culistid);
740 replay_start = kf_idx;
741 } else {
742 replay_start = current + 1;
743 keyframe_used = self.last_keyframe;
744 }
745 } else {
746 let Some(kf) = self.nearest_keyframe(target_culistid) else {
748 return Err(CuError::from("No keyframe available to rewind"));
749 };
750 self.restore_keyframe(&kf)?;
751 self.clear_runtime_copperlist_snapshot();
752 keyframe_used = Some(kf.culistid);
753 replay_start = self.index_for_culistid(kf.culistid)?;
754 }
755 } else {
756 let Some(kf) = self.nearest_keyframe(target_culistid) else {
758 return Err(CuError::from("No keyframe found in log"));
759 };
760 self.restore_keyframe(&kf)?;
761 self.clear_runtime_copperlist_snapshot();
762 keyframe_used = Some(kf.culistid);
763 replay_start = self.index_for_culistid(kf.culistid)?;
764 }
765
766 if replay_start > target_idx {
767 return Err(CuError::from(
768 "Replay start past target index; log ordering issue",
769 ));
770 }
771
772 let replayed = self.replay_range(replay_start, target_idx)?;
773
774 Ok(JumpOutcome {
775 culistid: target_culistid,
776 keyframe_culistid: keyframe_used,
777 replayed,
778 })
779 }
780
781 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome>
783 where
784 App: CurrentRuntimeCopperList<P>,
785 {
786 let idx = self.resolve_index_for_culistid(culistid, IndexedResolveMode::Exact)?;
787 self.goto_index(idx)
788 }
789
790 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome>
792 where
793 App: CurrentRuntimeCopperList<P>,
794 {
795 let idx = self.index_for_time(ts)?;
796 self.goto_index(idx)
797 }
798
799 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome>
801 where
802 App: CurrentRuntimeCopperList<P>,
803 {
804 let current =
805 self.current_idx
806 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
807 let target = current + delta;
808 if target < 0 || target as usize >= self.total_entries {
809 return Err(CuError::from("Step would move outside log bounds"));
810 }
811 self.goto_index(target as usize)
812 }
813
814 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
816 match self.current_idx {
817 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
818 None => Ok(None),
819 }
820 }
821
822 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
824 if idx >= self.total_entries {
825 return Ok(None);
826 }
827 Ok(Some(self.copperlist_at(idx)?.0))
828 }
829
830 pub fn total_entries(&self) -> usize {
832 self.total_entries
833 }
834
835 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
837 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
838 }
839
840 pub fn is_keyframe_culistid(&self, target_culistid: u64) -> bool {
842 self.keyframes
843 .iter()
844 .any(|kf| kf.culistid == target_culistid)
845 }
846
847 pub fn section_cache_stats(&self) -> SectionCacheStats {
849 SectionCacheStats {
850 cap: self.cache_cap,
851 entries: self.cache.len(),
852 hits: self.cache_hits,
853 misses: self.cache_misses,
854 evictions: self.cache_evictions,
855 }
856 }
857
858 pub fn current_index(&self) -> Option<usize> {
860 self.current_idx
861 }
862
863 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
865 f(&mut self.app)
866 }
867}
868
869impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
870where
871 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
872 L: UnifiedLogWrite<S> + 'static,
873 S: SectionStorage,
874 P: CopperListTuple,
875 CB: for<'a> Fn(
876 &'a crate::copperlist::CopperList<P>,
877 RobotClock,
878 RobotClockMock,
879 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
880 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
881{
882 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
884 self.app
885 .reflect_task(task_id)
886 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
887 }
888
889 pub fn reflected_task_mut(
891 &mut self,
892 task_id: &str,
893 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
894 self.app
895 .reflect_task_mut(task_id)
896 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
897 }
898
899 pub fn with_debug_state<R>(
901 &self,
902 task_id: &str,
903 f: impl FnOnce(&dyn crate::reflect::Reflect) -> R,
904 ) -> CuResult<R> {
905 self.app
906 .with_debug_state(task_id, f)
907 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
908 }
909
910 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
912 let task = self.reflected_task(task_id)?;
913 #[cfg(not(feature = "reflect"))]
914 {
915 let _ = task;
916 Err(CuError::from(
917 "Task introspection is disabled. Rebuild with the `reflect` feature.",
918 ))
919 }
920
921 #[cfg(feature = "reflect")]
922 {
923 Ok(format!("{task:#?}"))
924 }
925 }
926
927 pub fn dump_reflected_task_schemas(&self) -> String {
929 #[cfg(feature = "reflect")]
930 let mut registry = TypeRegistry::default();
931 #[cfg(not(feature = "reflect"))]
932 let mut registry = TypeRegistry;
933 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
934 dump_type_registry_schema(®istry)
935 }
936}
937#[allow(clippy::type_complexity)]
939pub(crate) fn decode_copperlists<
940 P: CopperListTuple,
941 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
942>(
943 section: &[u8],
944 time_of: &TF,
945) -> CuResult<(
946 Vec<Arc<crate::copperlist::CopperList<P>>>,
947 Vec<Option<CuTime>>,
948)> {
949 let mut cursor = std::io::Cursor::new(section);
950 let mut entries = Vec::new();
951 let mut timestamps = Vec::new();
952 loop {
953 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
954 &mut cursor,
955 standard(),
956 ) {
957 Ok(cl) => {
958 timestamps.push(time_of(&cl));
959 entries.push(Arc::new(cl));
960 }
961 Err(DecodeError::UnexpectedEnd { .. }) => break,
962 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
963 break;
964 }
965 Err(e) => {
966 return Err(CuError::new_with_cause(
967 "Failed to decode CopperList section",
968 e,
969 ));
970 }
971 }
972 }
973 Ok((entries, timestamps))
974}
975
976#[allow(clippy::type_complexity)]
978fn scan_copperlist_section<
979 P: CopperListTuple,
980 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
981>(
982 section: &[u8],
983 time_of: &TF,
984) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
985 let mut cursor = std::io::Cursor::new(section);
986 let mut count = 0usize;
987 let mut first_id = None;
988 let mut last_id = None;
989 let mut first_ts = None;
990 let mut last_ts = None;
991 loop {
992 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
993 &mut cursor,
994 standard(),
995 ) {
996 Ok(cl) => {
997 let ts = time_of(&cl);
998 if ts.is_none() {
999 #[cfg(feature = "std")]
1000 eprintln!(
1001 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
1002 cl.id
1003 );
1004 }
1005 if first_id.is_none() {
1006 first_id = Some(cl.id);
1007 first_ts = ts;
1008 }
1009 if first_ts.is_none() {
1011 first_ts = ts;
1012 }
1013 last_id = Some(cl.id);
1014 last_ts = ts.or(last_ts);
1015 count += 1;
1016 }
1017 Err(DecodeError::UnexpectedEnd { .. }) => break,
1018 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
1019 break;
1020 }
1021 Err(e) => {
1022 return Err(CuError::new_with_cause(
1023 "Failed to scan copperlist section",
1024 e,
1025 ));
1026 }
1027 }
1028 }
1029 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
1030 let last_id = last_id.unwrap_or(first_id);
1031 Ok((count, first_id, last_id, first_ts, last_ts))
1032}
1033
1034pub(crate) fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
1036 let logger = UnifiedLoggerBuilder::new()
1037 .file_base_name(log_base)
1038 .build()
1039 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1040 let UnifiedLogger::Read(dl) = logger else {
1041 return Err(CuError::from("Expected read-only unified logger"));
1042 };
1043 Ok(dl)
1044}
1045
1046pub(crate) fn read_section_at(
1048 log_reader: &mut UnifiedLoggerRead,
1049 pos: LogPosition,
1050) -> CuResult<(SectionHeader, Vec<u8>)> {
1051 log_reader.seek(pos)?;
1052 log_reader.raw_read_section()
1053}
1054
1055pub(crate) fn index_log<P, TF>(
1057 log_base: &Path,
1058 time_of: &TF,
1059) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1060where
1061 P: CopperListTuple,
1062 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1063{
1064 index_log_with_progress(log_base, time_of, |_| {})
1065}
1066
1067fn index_log_with_progress<P, TF, PF>(
1068 log_base: &Path,
1069 time_of: &TF,
1070 mut progress: PF,
1071) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1072where
1073 P: CopperListTuple,
1074 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1075 PF: FnMut(LogIndexProgress),
1076{
1077 let sizing_logger = UnifiedLoggerBuilder::new()
1078 .file_base_name(log_base)
1079 .build()
1080 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1081 let UnifiedLogger::Read(mut sizing_reader) = sizing_logger else {
1082 return Err(CuError::from("Expected read-only unified logger"));
1083 };
1084 let mut total_bytes = 0u64;
1085 loop {
1086 let header = sizing_reader.raw_skip_section()?;
1087 if header.entry_type == UnifiedLogType::LastEntry {
1088 break;
1089 }
1090 total_bytes = total_bytes.saturating_add(header.used as u64);
1091 }
1092
1093 let logger = UnifiedLoggerBuilder::new()
1094 .file_base_name(log_base)
1095 .build()
1096 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1097 let UnifiedLogger::Read(mut dl) = logger else {
1098 return Err(CuError::from("Expected read-only unified logger"));
1099 };
1100
1101 let mut sections = Vec::new();
1102 let mut keyframes = Vec::new();
1103 let mut total_entries = 0usize;
1104 let mut scanned_bytes = 0u64;
1105 progress(LogIndexProgress {
1106 scanned_bytes,
1107 total_bytes,
1108 indexed_entries: total_entries,
1109 });
1110
1111 loop {
1112 let pos = dl.position();
1113 let (header, data) = dl.raw_read_section()?;
1114 if header.entry_type == UnifiedLogType::LastEntry {
1115 break;
1116 }
1117
1118 match header.entry_type {
1119 UnifiedLogType::CopperList => {
1120 let (len, first_id, last_id, first_ts, last_ts) =
1121 scan_copperlist_section::<P, _>(&data, time_of)?;
1122 if len > 0 {
1123 sections.push(SectionIndexEntry {
1124 pos,
1125 start_idx: total_entries,
1126 len,
1127 first_id,
1128 last_id,
1129 first_ts,
1130 last_ts,
1131 });
1132 total_entries += len;
1133 }
1134 }
1135 UnifiedLogType::FrozenTasks => {
1136 let mut cursor = std::io::Cursor::new(&data);
1138 loop {
1139 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
1140 Ok(kf) => keyframes.push(kf),
1141 Err(DecodeError::UnexpectedEnd { .. }) => break,
1142 Err(DecodeError::Io { inner, .. })
1143 if inner.kind() == io::ErrorKind::UnexpectedEof =>
1144 {
1145 break;
1146 }
1147 Err(e) => {
1148 return Err(CuError::new_with_cause(
1149 "Failed to decode keyframe section",
1150 e,
1151 ));
1152 }
1153 }
1154 }
1155 }
1156 _ => {
1157 }
1159 }
1160 scanned_bytes = scanned_bytes.saturating_add(header.used as u64);
1161 progress(LogIndexProgress {
1162 scanned_bytes,
1163 total_bytes,
1164 indexed_entries: total_entries,
1165 });
1166 }
1167
1168 Ok((sections, keyframes, total_entries))
1169}
1170
1171fn nearest_replay_keyframe(keyframes: &[KeyFrame], target_culistid: u64) -> Option<KeyFrame> {
1172 keyframes
1173 .iter()
1174 .filter(|kf| kf.culistid <= target_culistid)
1175 .max_by_key(|kf| kf.culistid)
1176 .cloned()
1177}
1178
1179fn build_replay_callback<'a, P, CB, Callback>(
1182 build_callback: &CB,
1183 entry: &'a crate::copperlist::CopperList<P>,
1184 clock: RobotClock,
1185 clock_mock: RobotClockMock,
1186) -> Callback
1187where
1188 P: CopperListTuple,
1189 CB: Fn(&'a crate::copperlist::CopperList<P>, RobotClock, RobotClockMock) -> Callback,
1190{
1191 build_callback(entry, clock, clock_mock)
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196 use super::*;
1197
1198 fn keyframe(culistid: u64) -> KeyFrame {
1199 KeyFrame::from_serialized(culistid, CuTime::from_nanos(culistid), Vec::new())
1200 }
1201
1202 #[test]
1203 fn replay_keyframe_selects_nearest_keyframe_at_or_before_target() {
1204 let keyframes = [keyframe(0), keyframe(100), keyframe(500)];
1205
1206 let keyframe = nearest_replay_keyframe(&keyframes, 533).expect("replay keyframe");
1207
1208 assert_eq!(keyframe.culistid, 500);
1209 }
1210
1211 #[test]
1212 fn replay_keyframe_uses_nearest_available_nonzero_keyframe() {
1213 let keyframes = [keyframe(100), keyframe(500), keyframe(900)];
1214
1215 let keyframe = nearest_replay_keyframe(&keyframes, 533).expect("replay keyframe");
1216
1217 assert_eq!(keyframe.culistid, 500);
1218 assert!(nearest_replay_keyframe(&keyframes, 99).is_none());
1219 }
1220}