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