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 clock_for_cb = self.robot_clock.clone();
651 let clock_mock_for_cb = self.clock_mock.clone();
652 let mut cb = (self.build_callback)(entry.as_ref(), clock_for_cb, clock_mock_for_cb);
653 self.app.run_one_iteration(&mut cb)?;
654 self.normalize_runtime_copperlist_snapshot(entry.as_ref())?;
655 replayed += 1;
656 self.current_idx = Some(idx);
657 }
658 Ok(replayed)
659 }
660
661 pub(crate) fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome>
662 where
663 App: CurrentRuntimeCopperList<P>,
664 {
665 self.ensure_started()?;
666 if target_idx >= self.total_entries {
667 return Err(CuError::from("Target index outside log"));
668 }
669 let (target_cl, _) = self.copperlist_at(target_idx)?;
670 let target_culistid = target_cl.id;
671
672 let keyframe_used: Option<u64>;
673 let replay_start: usize;
674
675 if let Some(current) = self.current_idx {
677 if target_idx == current {
678 return Ok(JumpOutcome {
679 culistid: target_culistid,
680 keyframe_culistid: self.last_keyframe,
681 replayed: 0,
682 });
683 }
684
685 if target_idx >= current {
686 let nearest_keyframe = self.nearest_keyframe(target_culistid);
687 let nearest_keyframe_idx = nearest_keyframe
688 .as_ref()
689 .and_then(|kf| self.index_for_culistid(kf.culistid).ok());
690
691 if let (Some(kf), Some(kf_idx)) = (nearest_keyframe, nearest_keyframe_idx)
692 && kf_idx > current
693 {
694 self.restore_keyframe(&kf)?;
695 self.clear_runtime_copperlist_snapshot();
696 keyframe_used = Some(kf.culistid);
697 replay_start = kf_idx;
698 } else {
699 replay_start = current + 1;
700 keyframe_used = self.last_keyframe;
701 }
702 } else {
703 let Some(kf) = self.nearest_keyframe(target_culistid) else {
705 return Err(CuError::from("No keyframe available to rewind"));
706 };
707 self.restore_keyframe(&kf)?;
708 self.clear_runtime_copperlist_snapshot();
709 keyframe_used = Some(kf.culistid);
710 replay_start = self.index_for_culistid(kf.culistid)?;
711 }
712 } else {
713 let Some(kf) = self.nearest_keyframe(target_culistid) else {
715 return Err(CuError::from("No keyframe found in log"));
716 };
717 self.restore_keyframe(&kf)?;
718 self.clear_runtime_copperlist_snapshot();
719 keyframe_used = Some(kf.culistid);
720 replay_start = self.index_for_culistid(kf.culistid)?;
721 }
722
723 if replay_start > target_idx {
724 return Err(CuError::from(
725 "Replay start past target index; log ordering issue",
726 ));
727 }
728
729 let replayed = self.replay_range(replay_start, target_idx)?;
730
731 Ok(JumpOutcome {
732 culistid: target_culistid,
733 keyframe_culistid: keyframe_used,
734 replayed,
735 })
736 }
737
738 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome>
740 where
741 App: CurrentRuntimeCopperList<P>,
742 {
743 let idx = self.resolve_index_for_culistid(culistid, IndexedResolveMode::Exact)?;
744 self.goto_index(idx)
745 }
746
747 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome>
749 where
750 App: CurrentRuntimeCopperList<P>,
751 {
752 let idx = self.index_for_time(ts)?;
753 self.goto_index(idx)
754 }
755
756 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome>
758 where
759 App: CurrentRuntimeCopperList<P>,
760 {
761 let current =
762 self.current_idx
763 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
764 let target = current + delta;
765 if target < 0 || target as usize >= self.total_entries {
766 return Err(CuError::from("Step would move outside log bounds"));
767 }
768 self.goto_index(target as usize)
769 }
770
771 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
773 match self.current_idx {
774 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
775 None => Ok(None),
776 }
777 }
778
779 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
781 if idx >= self.total_entries {
782 return Ok(None);
783 }
784 Ok(Some(self.copperlist_at(idx)?.0))
785 }
786
787 pub fn total_entries(&self) -> usize {
789 self.total_entries
790 }
791
792 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
794 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
795 }
796
797 pub fn is_keyframe_culistid(&self, target_culistid: u64) -> bool {
799 self.keyframes
800 .iter()
801 .any(|kf| kf.culistid == target_culistid)
802 }
803
804 pub fn section_cache_stats(&self) -> SectionCacheStats {
806 SectionCacheStats {
807 cap: self.cache_cap,
808 entries: self.cache.len(),
809 hits: self.cache_hits,
810 misses: self.cache_misses,
811 evictions: self.cache_evictions,
812 }
813 }
814
815 pub fn current_index(&self) -> Option<usize> {
817 self.current_idx
818 }
819
820 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
822 f(&mut self.app)
823 }
824}
825
826impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
827where
828 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
829 L: UnifiedLogWrite<S> + 'static,
830 S: SectionStorage,
831 P: CopperListTuple,
832 CB: for<'a> Fn(
833 &'a crate::copperlist::CopperList<P>,
834 RobotClock,
835 RobotClockMock,
836 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
837 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
838{
839 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
841 self.app
842 .reflect_task(task_id)
843 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
844 }
845
846 pub fn reflected_task_mut(
848 &mut self,
849 task_id: &str,
850 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
851 self.app
852 .reflect_task_mut(task_id)
853 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
854 }
855
856 pub fn with_debug_state<R>(
858 &self,
859 task_id: &str,
860 f: impl FnOnce(&dyn crate::reflect::Reflect) -> R,
861 ) -> CuResult<R> {
862 self.app
863 .with_debug_state(task_id, f)
864 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
865 }
866
867 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
869 let task = self.reflected_task(task_id)?;
870 #[cfg(not(feature = "reflect"))]
871 {
872 let _ = task;
873 Err(CuError::from(
874 "Task introspection is disabled. Rebuild with the `reflect` feature.",
875 ))
876 }
877
878 #[cfg(feature = "reflect")]
879 {
880 Ok(format!("{task:#?}"))
881 }
882 }
883
884 pub fn dump_reflected_task_schemas(&self) -> String {
886 #[cfg(feature = "reflect")]
887 let mut registry = TypeRegistry::default();
888 #[cfg(not(feature = "reflect"))]
889 let mut registry = TypeRegistry;
890 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
891 dump_type_registry_schema(®istry)
892 }
893}
894#[allow(clippy::type_complexity)]
896pub(crate) fn decode_copperlists<
897 P: CopperListTuple,
898 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
899>(
900 section: &[u8],
901 time_of: &TF,
902) -> CuResult<(
903 Vec<Arc<crate::copperlist::CopperList<P>>>,
904 Vec<Option<CuTime>>,
905)> {
906 let mut cursor = std::io::Cursor::new(section);
907 let mut entries = Vec::new();
908 let mut timestamps = Vec::new();
909 loop {
910 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
911 &mut cursor,
912 standard(),
913 ) {
914 Ok(cl) => {
915 timestamps.push(time_of(&cl));
916 entries.push(Arc::new(cl));
917 }
918 Err(DecodeError::UnexpectedEnd { .. }) => break,
919 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
920 break;
921 }
922 Err(e) => {
923 return Err(CuError::new_with_cause(
924 "Failed to decode CopperList section",
925 e,
926 ));
927 }
928 }
929 }
930 Ok((entries, timestamps))
931}
932
933#[allow(clippy::type_complexity)]
935fn scan_copperlist_section<
936 P: CopperListTuple,
937 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
938>(
939 section: &[u8],
940 time_of: &TF,
941) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
942 let mut cursor = std::io::Cursor::new(section);
943 let mut count = 0usize;
944 let mut first_id = None;
945 let mut last_id = None;
946 let mut first_ts = None;
947 let mut last_ts = None;
948 loop {
949 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
950 &mut cursor,
951 standard(),
952 ) {
953 Ok(cl) => {
954 let ts = time_of(&cl);
955 if ts.is_none() {
956 #[cfg(feature = "std")]
957 eprintln!(
958 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
959 cl.id
960 );
961 }
962 if first_id.is_none() {
963 first_id = Some(cl.id);
964 first_ts = ts;
965 }
966 if first_ts.is_none() {
968 first_ts = ts;
969 }
970 last_id = Some(cl.id);
971 last_ts = ts.or(last_ts);
972 count += 1;
973 }
974 Err(DecodeError::UnexpectedEnd { .. }) => break,
975 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
976 break;
977 }
978 Err(e) => {
979 return Err(CuError::new_with_cause(
980 "Failed to scan copperlist section",
981 e,
982 ));
983 }
984 }
985 }
986 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
987 let last_id = last_id.unwrap_or(first_id);
988 Ok((count, first_id, last_id, first_ts, last_ts))
989}
990
991pub(crate) fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
993 let logger = UnifiedLoggerBuilder::new()
994 .file_base_name(log_base)
995 .build()
996 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
997 let UnifiedLogger::Read(dl) = logger else {
998 return Err(CuError::from("Expected read-only unified logger"));
999 };
1000 Ok(dl)
1001}
1002
1003pub(crate) fn read_section_at(
1005 log_reader: &mut UnifiedLoggerRead,
1006 pos: LogPosition,
1007) -> CuResult<(SectionHeader, Vec<u8>)> {
1008 log_reader.seek(pos)?;
1009 log_reader.raw_read_section()
1010}
1011
1012pub(crate) fn index_log<P, TF>(
1014 log_base: &Path,
1015 time_of: &TF,
1016) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1017where
1018 P: CopperListTuple,
1019 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1020{
1021 index_log_with_progress(log_base, time_of, |_| {})
1022}
1023
1024fn index_log_with_progress<P, TF, PF>(
1025 log_base: &Path,
1026 time_of: &TF,
1027 mut progress: PF,
1028) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1029where
1030 P: CopperListTuple,
1031 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1032 PF: FnMut(LogIndexProgress),
1033{
1034 let sizing_logger = UnifiedLoggerBuilder::new()
1035 .file_base_name(log_base)
1036 .build()
1037 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1038 let UnifiedLogger::Read(mut sizing_reader) = sizing_logger else {
1039 return Err(CuError::from("Expected read-only unified logger"));
1040 };
1041 let mut total_bytes = 0u64;
1042 loop {
1043 let header = sizing_reader.raw_skip_section()?;
1044 if header.entry_type == UnifiedLogType::LastEntry {
1045 break;
1046 }
1047 total_bytes = total_bytes.saturating_add(header.used as u64);
1048 }
1049
1050 let logger = UnifiedLoggerBuilder::new()
1051 .file_base_name(log_base)
1052 .build()
1053 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1054 let UnifiedLogger::Read(mut dl) = logger else {
1055 return Err(CuError::from("Expected read-only unified logger"));
1056 };
1057
1058 let mut sections = Vec::new();
1059 let mut keyframes = Vec::new();
1060 let mut total_entries = 0usize;
1061 let mut scanned_bytes = 0u64;
1062 progress(LogIndexProgress {
1063 scanned_bytes,
1064 total_bytes,
1065 indexed_entries: total_entries,
1066 });
1067
1068 loop {
1069 let pos = dl.position();
1070 let (header, data) = dl.raw_read_section()?;
1071 if header.entry_type == UnifiedLogType::LastEntry {
1072 break;
1073 }
1074
1075 match header.entry_type {
1076 UnifiedLogType::CopperList => {
1077 let (len, first_id, last_id, first_ts, last_ts) =
1078 scan_copperlist_section::<P, _>(&data, time_of)?;
1079 if len > 0 {
1080 sections.push(SectionIndexEntry {
1081 pos,
1082 start_idx: total_entries,
1083 len,
1084 first_id,
1085 last_id,
1086 first_ts,
1087 last_ts,
1088 });
1089 total_entries += len;
1090 }
1091 }
1092 UnifiedLogType::FrozenTasks => {
1093 let mut cursor = std::io::Cursor::new(&data);
1095 loop {
1096 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
1097 Ok(kf) => keyframes.push(kf),
1098 Err(DecodeError::UnexpectedEnd { .. }) => break,
1099 Err(DecodeError::Io { inner, .. })
1100 if inner.kind() == io::ErrorKind::UnexpectedEof =>
1101 {
1102 break;
1103 }
1104 Err(e) => {
1105 return Err(CuError::new_with_cause(
1106 "Failed to decode keyframe section",
1107 e,
1108 ));
1109 }
1110 }
1111 }
1112 }
1113 _ => {
1114 }
1116 }
1117 scanned_bytes = scanned_bytes.saturating_add(header.used as u64);
1118 progress(LogIndexProgress {
1119 scanned_bytes,
1120 total_bytes,
1121 indexed_entries: total_entries,
1122 });
1123 }
1124
1125 Ok((sections, keyframes, total_entries))
1126}
1127
1128fn nearest_replay_keyframe(keyframes: &[KeyFrame], target_culistid: u64) -> Option<KeyFrame> {
1129 keyframes
1130 .iter()
1131 .filter(|kf| kf.culistid <= target_culistid)
1132 .max_by_key(|kf| kf.culistid)
1133 .cloned()
1134}
1135
1136#[cfg(test)]
1137mod tests {
1138 use super::*;
1139
1140 fn keyframe(culistid: u64) -> KeyFrame {
1141 KeyFrame {
1142 culistid,
1143 timestamp: CuTime::from_nanos(culistid),
1144 serialized_tasks: Vec::new(),
1145 }
1146 }
1147
1148 #[test]
1149 fn replay_keyframe_selects_nearest_keyframe_at_or_before_target() {
1150 let keyframes = [keyframe(0), keyframe(100), keyframe(500)];
1151
1152 let keyframe = nearest_replay_keyframe(&keyframes, 533).expect("replay keyframe");
1153
1154 assert_eq!(keyframe.culistid, 500);
1155 }
1156
1157 #[test]
1158 fn replay_keyframe_uses_nearest_available_nonzero_keyframe() {
1159 let keyframes = [keyframe(100), keyframe(500), keyframe(900)];
1160
1161 let keyframe = nearest_replay_keyframe(&keyframes, 533).expect("replay keyframe");
1162
1163 assert_eq!(keyframe.culistid, 500);
1164 assert!(nearest_replay_keyframe(&keyframes, 99).is_none());
1165 }
1166}