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