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 fn ensure_started(&mut self) -> CuResult<()> {
303 if self.started {
304 return Ok(());
305 }
306 let mut noop = |_step: App::Step<'_>| SimOverride::ExecuteByRuntime;
307 self.app.start_all_tasks(&mut noop)?;
308 self.started = true;
309 Ok(())
310 }
311
312 fn nearest_keyframe(&self, target_culistid: u64) -> Option<KeyFrame> {
313 nearest_replay_anchor(&self.keyframes, target_culistid)
314 }
315
316 fn restore_keyframe(&mut self, kf: &KeyFrame) -> CuResult<()> {
317 self.app.restore_keyframe(kf)?;
318 self.clock_mock.set_value(kf.timestamp.as_nanos());
319 self.last_keyframe = Some(kf.culistid);
320 Ok(())
321 }
322
323 fn clear_runtime_copperlist_snapshot(&mut self)
324 where
325 App: CurrentRuntimeCopperList<P>,
326 {
327 self.app.set_current_runtime_copperlist_bytes(None);
328 }
329
330 fn normalize_runtime_copperlist_snapshot(
331 &mut self,
332 recorded: &crate::copperlist::CopperList<P>,
333 ) -> CuResult<()>
334 where
335 App: CurrentRuntimeCopperList<P>,
336 {
337 let normalized = self
338 .app
339 .current_runtime_copperlist_bytes()
340 .map(|bytes| {
341 let (mut runtime_cl, _) = bincode::decode_from_slice::<
342 crate::copperlist::CopperList<P>,
343 _,
344 >(bytes, standard())
345 .map_err(|e| {
346 CuError::new_with_cause("Failed to decode runtime CopperList snapshot", e)
347 })?;
348 runtime_cl.id = recorded.id;
349 runtime_cl.change_state(recorded.get_state());
350 bincode::encode_to_vec(&runtime_cl, standard()).map_err(|e| {
351 CuError::new_with_cause("Failed to encode normalized CopperList snapshot", e)
352 })
353 })
354 .transpose()?;
355 self.app.set_current_runtime_copperlist_bytes(normalized);
356 Ok(())
357 }
358
359 fn find_section_for_index(&self, idx: usize) -> Option<usize> {
360 self.sections
361 .binary_search_by(|s| {
362 if idx < s.start_idx {
363 std::cmp::Ordering::Greater
364 } else if idx >= s.start_idx + s.len {
365 std::cmp::Ordering::Less
366 } else {
367 std::cmp::Ordering::Equal
368 }
369 })
370 .ok()
371 }
372
373 fn find_section_for_culistid(&self, culistid: u64) -> Option<usize> {
374 self.sections
375 .binary_search_by(|s| {
376 if culistid < s.first_id {
377 std::cmp::Ordering::Greater
378 } else if culistid > s.last_id {
379 std::cmp::Ordering::Less
380 } else {
381 std::cmp::Ordering::Equal
382 }
383 })
384 .ok()
385 }
386
387 fn touch_cache(&mut self, key: usize) {
388 if let Some(pos) = self.cache_order.iter().position(|k| *k == key) {
389 self.cache_order.remove(pos);
390 }
391 self.cache_order.push_back(key);
392 while self.cache_order.len() > self.cache_cap {
393 if let Some(old) = self.cache_order.pop_front()
394 && self.cache.remove(&old).is_some()
395 {
396 self.cache_evictions = self.cache_evictions.saturating_add(1);
397 }
398 }
399 }
400
401 fn load_section(&mut self, section_idx: usize) -> CuResult<&CachedSection<P>> {
402 if self.cache.contains_key(§ion_idx) {
403 self.cache_hits = self.cache_hits.saturating_add(1);
404 self.touch_cache(section_idx);
405 return Ok(self.cache.get(§ion_idx).unwrap());
407 }
408 self.cache_misses = self.cache_misses.saturating_add(1);
409
410 let entry = &self.sections[section_idx];
411 let (header, data) = read_section_at(&mut self.log_reader, entry.pos)?;
412 if header.entry_type != UnifiedLogType::CopperList {
413 return Err(CuError::from(
414 "Section type mismatch while loading copperlists",
415 ));
416 }
417
418 let (entries, timestamps) = decode_copperlists::<P, _>(&data, &self.time_of)?;
419 let cached = CachedSection {
420 entries,
421 timestamps,
422 };
423 self.cache.insert(section_idx, cached);
424 self.touch_cache(section_idx);
425 Ok(self.cache.get(§ion_idx).unwrap())
426 }
427
428 fn copperlist_at(
429 &mut self,
430 idx: usize,
431 ) -> CuResult<(Arc<crate::copperlist::CopperList<P>>, Option<CuTime>)> {
432 let section_idx = self
433 .find_section_for_index(idx)
434 .ok_or_else(|| CuError::from("Index outside copperlist log"))?;
435 let start_idx = self.sections[section_idx].start_idx;
436 let section = self.load_section(section_idx)?;
437 let local = idx - start_idx;
438 let cl = section
439 .entries
440 .get(local)
441 .ok_or_else(|| CuError::from("Corrupt section index vs cache"))?
442 .clone();
443 let ts = section.timestamps.get(local).copied().unwrap_or(None);
444 Ok((cl, ts))
445 }
446
447 fn first_section_with_last_id_at_least(&self, culistid: u64) -> usize {
448 let mut left = 0usize;
449 let mut right = self.sections.len();
450 while left < right {
451 let mid = left + (right - left) / 2;
452 if self.sections[mid].last_id < culistid {
453 left = mid + 1;
454 } else {
455 right = mid;
456 }
457 }
458 left
459 }
460
461 fn first_section_with_first_id_greater_than(&self, culistid: u64) -> usize {
462 let mut left = 0usize;
463 let mut right = self.sections.len();
464 while left < right {
465 let mid = left + (right - left) / 2;
466 if self.sections[mid].first_id <= culistid {
467 left = mid + 1;
468 } else {
469 right = mid;
470 }
471 }
472 left
473 }
474
475 fn index_for_culistid_at_or_after(&mut self, culistid: u64) -> CuResult<usize> {
476 let mut section_idx = self.first_section_with_last_id_at_least(culistid);
477 while section_idx < self.sections.len() {
478 let start_idx = self.sections[section_idx].start_idx;
479 let section = self.load_section(section_idx)?;
480 for (offset, cl) in section.entries.iter().enumerate() {
481 if cl.id >= culistid {
482 return Ok(start_idx + offset);
483 }
484 }
485 section_idx += 1;
486 }
487 Err(CuError::from(format!("No CL at/after target {culistid}")))
488 }
489
490 fn index_for_culistid_at_or_before(&mut self, culistid: u64) -> CuResult<usize> {
491 let mut section_idx = self.first_section_with_first_id_greater_than(culistid);
492 while section_idx > 0 {
493 section_idx -= 1;
494 let start_idx = self.sections[section_idx].start_idx;
495 let section = self.load_section(section_idx)?;
496 for (offset, cl) in section.entries.iter().enumerate().rev() {
497 if cl.id <= culistid {
498 return Ok(start_idx + offset);
499 }
500 }
501 }
502 Err(CuError::from(format!("No CL at/before target {culistid}")))
503 }
504
505 fn index_for_culistid(&mut self, culistid: u64) -> CuResult<usize> {
506 let section_idx = self
507 .find_section_for_culistid(culistid)
508 .ok_or_else(|| CuError::from("Requested culistid not present in log"))?;
509 let start_idx = self.sections[section_idx].start_idx;
510 let section = self.load_section(section_idx)?;
511 for (offset, cl) in section.entries.iter().enumerate() {
512 if cl.id == culistid {
513 return Ok(start_idx + offset);
514 }
515 }
516 Err(CuError::from("culistid not found inside indexed section"))
517 }
518
519 pub(crate) fn resolve_index_for_culistid(
520 &mut self,
521 culistid: u64,
522 mode: IndexedResolveMode,
523 ) -> CuResult<usize> {
524 match mode {
525 IndexedResolveMode::Exact => self
526 .index_for_culistid(culistid)
527 .map_err(|_| CuError::from(format!("No exact CL target for {culistid}"))),
528 IndexedResolveMode::AtOrAfter => self.index_for_culistid_at_or_after(culistid),
529 IndexedResolveMode::AtOrBefore => self.index_for_culistid_at_or_before(culistid),
530 }
531 }
532
533 fn index_for_time_at_or_after(&mut self, ts: CuTime) -> CuResult<usize> {
534 for section_idx in 0..self.sections.len() {
535 let section_entry = &self.sections[section_idx];
536 if matches!(section_entry.last_ts, Some(last) if last < ts) {
537 continue;
538 }
539
540 let start_idx = section_entry.start_idx;
541 let section_first_ts = section_entry.first_ts;
542 let section = self.load_section(section_idx)?;
543 for (offset, maybe_ts) in section.timestamps.iter().enumerate() {
544 if matches!(maybe_ts, Some(entry_ts) if *entry_ts >= ts) {
545 return Ok(start_idx + offset);
546 }
547 }
548
549 if matches!(section_first_ts, Some(first) if first > ts) {
550 break;
551 }
552 }
553
554 Err(CuError::from(format!(
555 "No timestamp at/after {}",
556 ts.as_nanos()
557 )))
558 }
559
560 fn index_for_time_at_or_before(&mut self, ts: CuTime) -> CuResult<usize> {
561 for section_idx in (0..self.sections.len()).rev() {
562 let section_entry = &self.sections[section_idx];
563 if matches!(section_entry.first_ts, Some(first) if first > ts) {
564 continue;
565 }
566
567 let start_idx = section_entry.start_idx;
568 let section = self.load_section(section_idx)?;
569 for (offset, maybe_ts) in section.timestamps.iter().enumerate().rev() {
570 if matches!(maybe_ts, Some(entry_ts) if *entry_ts <= ts) {
571 return Ok(start_idx + offset);
572 }
573 }
574 }
575
576 Err(CuError::from(format!(
577 "No timestamp at/before {}",
578 ts.as_nanos()
579 )))
580 }
581
582 fn index_for_exact_time(&mut self, ts: CuTime) -> CuResult<usize> {
583 for section_idx in 0..self.sections.len() {
584 let section_entry = &self.sections[section_idx];
585 if matches!(section_entry.last_ts, Some(last) if last < ts) {
586 continue;
587 }
588 if matches!(section_entry.first_ts, Some(first) if first > ts) {
589 break;
590 }
591
592 let start_idx = section_entry.start_idx;
593 let section = self.load_section(section_idx)?;
594 for (offset, maybe_ts) in section.timestamps.iter().enumerate() {
595 if matches!(maybe_ts, Some(entry_ts) if *entry_ts == ts) {
596 return Ok(start_idx + offset);
597 }
598 }
599 }
600
601 Err(CuError::from(format!(
602 "No exact timestamp target for {}",
603 ts.as_nanos()
604 )))
605 }
606
607 fn index_for_time(&mut self, ts: CuTime) -> CuResult<usize> {
608 self.resolve_index_for_time(ts, IndexedResolveMode::AtOrAfter)
609 }
610
611 pub(crate) fn resolve_index_for_time(
612 &mut self,
613 ts: CuTime,
614 mode: IndexedResolveMode,
615 ) -> CuResult<usize> {
616 match mode {
617 IndexedResolveMode::Exact => self.index_for_exact_time(ts),
618 IndexedResolveMode::AtOrAfter => self.index_for_time_at_or_after(ts),
619 IndexedResolveMode::AtOrBefore => self.index_for_time_at_or_before(ts),
620 }
621 }
622
623 fn replay_range(&mut self, start: usize, end: usize) -> CuResult<usize>
624 where
625 App: CurrentRuntimeCopperList<P>,
626 {
627 let mut replayed = 0usize;
628 for idx in start..=end {
629 let (entry, ts) = self.copperlist_at(idx)?;
630 if let Some(ts) = ts {
631 self.clock_mock.set_value(ts.as_nanos());
632 }
633 let clock_for_cb = self.robot_clock.clone();
634 let clock_mock_for_cb = self.clock_mock.clone();
635 let mut cb = (self.build_callback)(entry.as_ref(), clock_for_cb, clock_mock_for_cb);
636 self.app.run_one_iteration(&mut cb)?;
637 self.normalize_runtime_copperlist_snapshot(entry.as_ref())?;
638 replayed += 1;
639 self.current_idx = Some(idx);
640 }
641 Ok(replayed)
642 }
643
644 pub(crate) fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome>
645 where
646 App: CurrentRuntimeCopperList<P>,
647 {
648 self.ensure_started()?;
649 if target_idx >= self.total_entries {
650 return Err(CuError::from("Target index outside log"));
651 }
652 let (target_cl, _) = self.copperlist_at(target_idx)?;
653 let target_culistid = target_cl.id;
654
655 let keyframe_used: Option<u64>;
656 let replay_start: usize;
657
658 if let Some(current) = self.current_idx {
660 if target_idx == current {
661 return Ok(JumpOutcome {
662 culistid: target_culistid,
663 keyframe_culistid: self.last_keyframe,
664 replayed: 0,
665 });
666 }
667
668 if target_idx >= current {
669 let nearest_keyframe = self.nearest_keyframe(target_culistid);
670 let nearest_keyframe_idx = nearest_keyframe
671 .as_ref()
672 .and_then(|kf| self.index_for_culistid(kf.culistid).ok());
673
674 if let (Some(kf), Some(kf_idx)) = (nearest_keyframe, nearest_keyframe_idx)
675 && kf_idx > current
676 {
677 self.restore_keyframe(&kf)?;
678 self.clear_runtime_copperlist_snapshot();
679 keyframe_used = Some(kf.culistid);
680 replay_start = kf_idx;
681 } else {
682 replay_start = current + 1;
683 keyframe_used = self.last_keyframe;
684 }
685 } else {
686 let Some(kf) = self.nearest_keyframe(target_culistid) else {
688 return Err(CuError::from("No keyframe available to rewind"));
689 };
690 self.restore_keyframe(&kf)?;
691 self.clear_runtime_copperlist_snapshot();
692 keyframe_used = Some(kf.culistid);
693 replay_start = self.index_for_culistid(kf.culistid)?;
694 }
695 } else {
696 let Some(kf) = self.nearest_keyframe(target_culistid) else {
698 return Err(CuError::from("No keyframe found in log"));
699 };
700 self.restore_keyframe(&kf)?;
701 self.clear_runtime_copperlist_snapshot();
702 keyframe_used = Some(kf.culistid);
703 replay_start = self.index_for_culistid(kf.culistid)?;
704 }
705
706 if replay_start > target_idx {
707 return Err(CuError::from(
708 "Replay start past target index; log ordering issue",
709 ));
710 }
711
712 let replayed = self.replay_range(replay_start, target_idx)?;
713
714 Ok(JumpOutcome {
715 culistid: target_culistid,
716 keyframe_culistid: keyframe_used,
717 replayed,
718 })
719 }
720
721 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome>
723 where
724 App: CurrentRuntimeCopperList<P>,
725 {
726 let idx = self.resolve_index_for_culistid(culistid, IndexedResolveMode::Exact)?;
727 self.goto_index(idx)
728 }
729
730 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome>
732 where
733 App: CurrentRuntimeCopperList<P>,
734 {
735 let idx = self.index_for_time(ts)?;
736 self.goto_index(idx)
737 }
738
739 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome>
741 where
742 App: CurrentRuntimeCopperList<P>,
743 {
744 let current =
745 self.current_idx
746 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
747 let target = current + delta;
748 if target < 0 || target as usize >= self.total_entries {
749 return Err(CuError::from("Step would move outside log bounds"));
750 }
751 self.goto_index(target as usize)
752 }
753
754 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
756 match self.current_idx {
757 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
758 None => Ok(None),
759 }
760 }
761
762 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
764 if idx >= self.total_entries {
765 return Ok(None);
766 }
767 Ok(Some(self.copperlist_at(idx)?.0))
768 }
769
770 pub fn total_entries(&self) -> usize {
772 self.total_entries
773 }
774
775 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
777 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
778 }
779
780 pub fn is_keyframe_culistid(&self, target_culistid: u64) -> bool {
782 self.keyframes
783 .iter()
784 .any(|kf| kf.culistid == target_culistid)
785 }
786
787 pub fn section_cache_stats(&self) -> SectionCacheStats {
789 SectionCacheStats {
790 cap: self.cache_cap,
791 entries: self.cache.len(),
792 hits: self.cache_hits,
793 misses: self.cache_misses,
794 evictions: self.cache_evictions,
795 }
796 }
797
798 pub fn current_index(&self) -> Option<usize> {
800 self.current_idx
801 }
802
803 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
805 f(&mut self.app)
806 }
807}
808
809impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
810where
811 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
812 L: UnifiedLogWrite<S> + 'static,
813 S: SectionStorage,
814 P: CopperListTuple,
815 CB: for<'a> Fn(
816 &'a crate::copperlist::CopperList<P>,
817 RobotClock,
818 RobotClockMock,
819 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
820 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
821{
822 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
824 self.app
825 .reflect_task(task_id)
826 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
827 }
828
829 pub fn reflected_task_mut(
831 &mut self,
832 task_id: &str,
833 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
834 self.app
835 .reflect_task_mut(task_id)
836 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
837 }
838
839 pub fn with_debug_state<R>(
841 &self,
842 task_id: &str,
843 f: impl FnOnce(&dyn crate::reflect::Reflect) -> R,
844 ) -> CuResult<R> {
845 self.app
846 .with_debug_state(task_id, f)
847 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
848 }
849
850 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
852 let task = self.reflected_task(task_id)?;
853 #[cfg(not(feature = "reflect"))]
854 {
855 let _ = task;
856 Err(CuError::from(
857 "Task introspection is disabled. Rebuild with the `reflect` feature.",
858 ))
859 }
860
861 #[cfg(feature = "reflect")]
862 {
863 Ok(format!("{task:#?}"))
864 }
865 }
866
867 pub fn dump_reflected_task_schemas(&self) -> String {
869 #[cfg(feature = "reflect")]
870 let mut registry = TypeRegistry::default();
871 #[cfg(not(feature = "reflect"))]
872 let mut registry = TypeRegistry;
873 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
874 dump_type_registry_schema(®istry)
875 }
876}
877#[allow(clippy::type_complexity)]
879pub(crate) fn decode_copperlists<
880 P: CopperListTuple,
881 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
882>(
883 section: &[u8],
884 time_of: &TF,
885) -> CuResult<(
886 Vec<Arc<crate::copperlist::CopperList<P>>>,
887 Vec<Option<CuTime>>,
888)> {
889 let mut cursor = std::io::Cursor::new(section);
890 let mut entries = Vec::new();
891 let mut timestamps = Vec::new();
892 loop {
893 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
894 &mut cursor,
895 standard(),
896 ) {
897 Ok(cl) => {
898 timestamps.push(time_of(&cl));
899 entries.push(Arc::new(cl));
900 }
901 Err(DecodeError::UnexpectedEnd { .. }) => break,
902 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
903 break;
904 }
905 Err(e) => {
906 return Err(CuError::new_with_cause(
907 "Failed to decode CopperList section",
908 e,
909 ));
910 }
911 }
912 }
913 Ok((entries, timestamps))
914}
915
916#[allow(clippy::type_complexity)]
918fn scan_copperlist_section<
919 P: CopperListTuple,
920 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
921>(
922 section: &[u8],
923 time_of: &TF,
924) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
925 let mut cursor = std::io::Cursor::new(section);
926 let mut count = 0usize;
927 let mut first_id = None;
928 let mut last_id = None;
929 let mut first_ts = None;
930 let mut last_ts = None;
931 loop {
932 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
933 &mut cursor,
934 standard(),
935 ) {
936 Ok(cl) => {
937 let ts = time_of(&cl);
938 if ts.is_none() {
939 #[cfg(feature = "std")]
940 eprintln!(
941 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
942 cl.id
943 );
944 }
945 if first_id.is_none() {
946 first_id = Some(cl.id);
947 first_ts = ts;
948 }
949 if first_ts.is_none() {
951 first_ts = ts;
952 }
953 last_id = Some(cl.id);
954 last_ts = ts.or(last_ts);
955 count += 1;
956 }
957 Err(DecodeError::UnexpectedEnd { .. }) => break,
958 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
959 break;
960 }
961 Err(e) => {
962 return Err(CuError::new_with_cause(
963 "Failed to scan copperlist section",
964 e,
965 ));
966 }
967 }
968 }
969 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
970 let last_id = last_id.unwrap_or(first_id);
971 Ok((count, first_id, last_id, first_ts, last_ts))
972}
973
974pub(crate) fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
976 let logger = UnifiedLoggerBuilder::new()
977 .file_base_name(log_base)
978 .build()
979 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
980 let UnifiedLogger::Read(dl) = logger else {
981 return Err(CuError::from("Expected read-only unified logger"));
982 };
983 Ok(dl)
984}
985
986pub(crate) fn read_section_at(
988 log_reader: &mut UnifiedLoggerRead,
989 pos: LogPosition,
990) -> CuResult<(SectionHeader, Vec<u8>)> {
991 log_reader.seek(pos)?;
992 log_reader.raw_read_section()
993}
994
995pub(crate) fn index_log<P, TF>(
997 log_base: &Path,
998 time_of: &TF,
999) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1000where
1001 P: CopperListTuple,
1002 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1003{
1004 index_log_with_progress(log_base, time_of, |_| {})
1005}
1006
1007fn index_log_with_progress<P, TF, PF>(
1008 log_base: &Path,
1009 time_of: &TF,
1010 mut progress: PF,
1011) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
1012where
1013 P: CopperListTuple,
1014 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
1015 PF: FnMut(LogIndexProgress),
1016{
1017 let sizing_logger = UnifiedLoggerBuilder::new()
1018 .file_base_name(log_base)
1019 .build()
1020 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1021 let UnifiedLogger::Read(mut sizing_reader) = sizing_logger else {
1022 return Err(CuError::from("Expected read-only unified logger"));
1023 };
1024 let mut total_bytes = 0u64;
1025 loop {
1026 let header = sizing_reader.raw_skip_section()?;
1027 if header.entry_type == UnifiedLogType::LastEntry {
1028 break;
1029 }
1030 total_bytes = total_bytes.saturating_add(header.used as u64);
1031 }
1032
1033 let logger = UnifiedLoggerBuilder::new()
1034 .file_base_name(log_base)
1035 .build()
1036 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
1037 let UnifiedLogger::Read(mut dl) = logger else {
1038 return Err(CuError::from("Expected read-only unified logger"));
1039 };
1040
1041 let mut sections = Vec::new();
1042 let mut keyframes = Vec::new();
1043 let mut total_entries = 0usize;
1044 let mut scanned_bytes = 0u64;
1045 progress(LogIndexProgress {
1046 scanned_bytes,
1047 total_bytes,
1048 indexed_entries: total_entries,
1049 });
1050
1051 loop {
1052 let pos = dl.position();
1053 let (header, data) = dl.raw_read_section()?;
1054 if header.entry_type == UnifiedLogType::LastEntry {
1055 break;
1056 }
1057
1058 match header.entry_type {
1059 UnifiedLogType::CopperList => {
1060 let (len, first_id, last_id, first_ts, last_ts) =
1061 scan_copperlist_section::<P, _>(&data, time_of)?;
1062 if len > 0 {
1063 sections.push(SectionIndexEntry {
1064 pos,
1065 start_idx: total_entries,
1066 len,
1067 first_id,
1068 last_id,
1069 first_ts,
1070 last_ts,
1071 });
1072 total_entries += len;
1073 }
1074 }
1075 UnifiedLogType::FrozenTasks => {
1076 let mut cursor = std::io::Cursor::new(&data);
1078 loop {
1079 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
1080 Ok(kf) => keyframes.push(kf),
1081 Err(DecodeError::UnexpectedEnd { .. }) => break,
1082 Err(DecodeError::Io { inner, .. })
1083 if inner.kind() == io::ErrorKind::UnexpectedEof =>
1084 {
1085 break;
1086 }
1087 Err(e) => {
1088 return Err(CuError::new_with_cause(
1089 "Failed to decode keyframe section",
1090 e,
1091 ));
1092 }
1093 }
1094 }
1095 }
1096 _ => {
1097 }
1099 }
1100 scanned_bytes = scanned_bytes.saturating_add(header.used as u64);
1101 progress(LogIndexProgress {
1102 scanned_bytes,
1103 total_bytes,
1104 indexed_entries: total_entries,
1105 });
1106 }
1107
1108 Ok((sections, keyframes, total_entries))
1109}
1110
1111fn nearest_replay_anchor(keyframes: &[KeyFrame], target_culistid: u64) -> Option<KeyFrame> {
1112 keyframes
1113 .iter()
1114 .filter(|kf| kf.culistid <= target_culistid)
1115 .max_by_key(|kf| kf.culistid)
1116 .cloned()
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 fn keyframe(culistid: u64) -> KeyFrame {
1124 KeyFrame {
1125 culistid,
1126 timestamp: CuTime::from_nanos(culistid),
1127 serialized_tasks: Vec::new(),
1128 }
1129 }
1130
1131 #[test]
1132 fn replay_anchor_selects_nearest_keyframe_at_or_before_target() {
1133 let keyframes = [keyframe(0), keyframe(100), keyframe(500)];
1134
1135 let anchor = nearest_replay_anchor(&keyframes, 533).expect("replay anchor");
1136
1137 assert_eq!(anchor.culistid, 500);
1138 }
1139
1140 #[test]
1141 fn replay_anchor_uses_nearest_available_nonzero_keyframe() {
1142 let keyframes = [keyframe(100), keyframe(500), keyframe(900)];
1143
1144 let anchor = nearest_replay_anchor(&keyframes, 533).expect("replay anchor");
1145
1146 assert_eq!(anchor.culistid, 500);
1147 assert!(nearest_replay_anchor(&keyframes, 99).is_none());
1148 }
1149}