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