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#[derive(Debug, Clone)]
52pub(crate) struct SectionIndexEntry {
53 pub(crate) pos: LogPosition,
54 pub(crate) start_idx: usize,
55 pub(crate) len: usize,
56 pub(crate) first_id: u64,
57 pub(crate) last_id: u64,
58 pub(crate) first_ts: Option<CuTime>,
59 pub(crate) last_ts: Option<CuTime>,
60}
61
62#[derive(Debug, Clone)]
64struct CachedSection<P: CopperListTuple> {
65 entries: Vec<Arc<crate::copperlist::CopperList<P>>>,
66 timestamps: Vec<Option<CuTime>>,
67}
68
69const DEFAULT_SECTION_CACHE_CAP: usize = 8;
76pub struct CuDebugSession<App, P, CB, TF, S, L>
77where
78 P: CopperListTuple,
79 S: SectionStorage,
80 L: UnifiedLogWrite<S> + 'static,
81{
82 app: App,
83 robot_clock: RobotClock,
84 clock_mock: RobotClockMock,
85 log_reader: UnifiedLoggerRead,
86 sections: Vec<SectionIndexEntry>,
87 total_entries: usize,
88 keyframes: Vec<KeyFrame>,
89 started: bool,
90 current_idx: Option<usize>,
91 last_keyframe: Option<u64>,
92 build_callback: CB,
93 time_of: TF,
94 cache: HashMap<usize, CachedSection<P>>,
96 cache_order: VecDeque<usize>,
97 cache_cap: usize,
98 cache_hits: u64,
99 cache_misses: u64,
100 cache_evictions: u64,
101 phantom: PhantomData<(S, L)>,
102}
103
104impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
105where
106 App: CuSimApplication<S, L>,
107 L: UnifiedLogWrite<S> + 'static,
108 S: SectionStorage,
109 P: CopperListTuple + 'static,
110 CB: for<'a> Fn(
111 &'a crate::copperlist::CopperList<P>,
112 RobotClock,
113 RobotClockMock,
114 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
115 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
116{
117 pub fn from_log(
119 log_base: &Path,
120 app: App,
121 robot_clock: RobotClock,
122 clock_mock: RobotClockMock,
123 build_callback: CB,
124 time_of: TF,
125 ) -> CuResult<Self> {
126 let _ = crate::logcodec::seed_effective_config_from_log::<P>(log_base)?;
127 let (sections, keyframes, total_entries) = index_log::<P, _>(log_base, &time_of)?;
128 let log_reader = build_read_logger(log_base)?;
129 Ok(Self::new(
130 log_reader,
131 app,
132 robot_clock,
133 clock_mock,
134 sections,
135 total_entries,
136 keyframes,
137 build_callback,
138 time_of,
139 ))
140 }
141
142 pub fn from_log_with_cache_cap(
144 log_base: &Path,
145 app: App,
146 robot_clock: RobotClock,
147 clock_mock: RobotClockMock,
148 build_callback: CB,
149 time_of: TF,
150 cache_cap: usize,
151 ) -> CuResult<Self> {
152 let _ = crate::logcodec::seed_effective_config_from_log::<P>(log_base)?;
153 let (sections, keyframes, total_entries) = index_log::<P, _>(log_base, &time_of)?;
154 let log_reader = build_read_logger(log_base)?;
155 Ok(Self::new_with_cache_cap(
156 log_reader,
157 app,
158 robot_clock,
159 clock_mock,
160 sections,
161 total_entries,
162 keyframes,
163 build_callback,
164 time_of,
165 cache_cap,
166 ))
167 }
168
169 #[allow(clippy::too_many_arguments)]
171 pub(crate) fn new(
172 log_reader: UnifiedLoggerRead,
173 app: App,
174 robot_clock: RobotClock,
175 clock_mock: RobotClockMock,
176 sections: Vec<SectionIndexEntry>,
177 total_entries: usize,
178 keyframes: Vec<KeyFrame>,
179 build_callback: CB,
180 time_of: TF,
181 ) -> Self {
182 Self::new_with_cache_cap(
183 log_reader,
184 app,
185 robot_clock,
186 clock_mock,
187 sections,
188 total_entries,
189 keyframes,
190 build_callback,
191 time_of,
192 DEFAULT_SECTION_CACHE_CAP,
193 )
194 }
195
196 #[allow(clippy::too_many_arguments)]
197 pub(crate) fn new_with_cache_cap(
198 log_reader: UnifiedLoggerRead,
199 app: App,
200 robot_clock: RobotClock,
201 clock_mock: RobotClockMock,
202 sections: Vec<SectionIndexEntry>,
203 total_entries: usize,
204 keyframes: Vec<KeyFrame>,
205 build_callback: CB,
206 time_of: TF,
207 cache_cap: usize,
208 ) -> Self {
209 Self {
210 app,
211 robot_clock,
212 clock_mock,
213 log_reader,
214 sections,
215 total_entries,
216 keyframes,
217 started: false,
218 current_idx: None,
219 last_keyframe: None,
220 build_callback,
221 time_of,
222 cache: HashMap::new(),
223 cache_order: VecDeque::new(),
224 cache_cap: cache_cap.max(1),
225 cache_hits: 0,
226 cache_misses: 0,
227 cache_evictions: 0,
228 phantom: PhantomData,
229 }
230 }
231
232 #[inline]
233 pub fn app(&self) -> &App {
234 &self.app
235 }
236
237 #[inline]
238 pub fn app_mut(&mut self) -> &mut App {
239 &mut self.app
240 }
241
242 fn ensure_started(&mut self) -> CuResult<()> {
243 if self.started {
244 return Ok(());
245 }
246 let mut noop = |_step: App::Step<'_>| SimOverride::ExecuteByRuntime;
247 self.app.start_all_tasks(&mut noop)?;
248 self.started = true;
249 Ok(())
250 }
251
252 fn nearest_keyframe(&self, target_culistid: u64) -> Option<KeyFrame> {
253 self.keyframes
254 .iter()
255 .filter(|kf| kf.culistid <= target_culistid)
256 .max_by_key(|kf| kf.culistid)
257 .cloned()
258 }
259
260 fn restore_keyframe(&mut self, kf: &KeyFrame) -> CuResult<()> {
261 self.app.restore_keyframe(kf)?;
262 self.clock_mock.set_value(kf.timestamp.as_nanos());
263 self.last_keyframe = Some(kf.culistid);
264 Ok(())
265 }
266
267 fn clear_runtime_copperlist_snapshot(&mut self)
268 where
269 App: CurrentRuntimeCopperList<P>,
270 {
271 self.app.set_current_runtime_copperlist_bytes(None);
272 }
273
274 fn normalize_runtime_copperlist_snapshot(
275 &mut self,
276 recorded: &crate::copperlist::CopperList<P>,
277 ) -> CuResult<()>
278 where
279 App: CurrentRuntimeCopperList<P>,
280 {
281 let normalized = self
282 .app
283 .current_runtime_copperlist_bytes()
284 .map(|bytes| {
285 let (mut runtime_cl, _) = bincode::decode_from_slice::<
286 crate::copperlist::CopperList<P>,
287 _,
288 >(bytes, standard())
289 .map_err(|e| {
290 CuError::new_with_cause("Failed to decode runtime CopperList snapshot", e)
291 })?;
292 runtime_cl.id = recorded.id;
293 runtime_cl.change_state(recorded.get_state());
294 bincode::encode_to_vec(&runtime_cl, standard()).map_err(|e| {
295 CuError::new_with_cause("Failed to encode normalized CopperList snapshot", e)
296 })
297 })
298 .transpose()?;
299 self.app.set_current_runtime_copperlist_bytes(normalized);
300 Ok(())
301 }
302
303 fn find_section_for_index(&self, idx: usize) -> Option<usize> {
304 self.sections
305 .binary_search_by(|s| {
306 if idx < s.start_idx {
307 std::cmp::Ordering::Greater
308 } else if idx >= s.start_idx + s.len {
309 std::cmp::Ordering::Less
310 } else {
311 std::cmp::Ordering::Equal
312 }
313 })
314 .ok()
315 }
316
317 fn find_section_for_culistid(&self, culistid: u64) -> Option<usize> {
318 self.sections
319 .binary_search_by(|s| {
320 if culistid < s.first_id {
321 std::cmp::Ordering::Greater
322 } else if culistid > s.last_id {
323 std::cmp::Ordering::Less
324 } else {
325 std::cmp::Ordering::Equal
326 }
327 })
328 .ok()
329 }
330
331 fn find_section_for_time(&self, ts: CuTime) -> Option<usize> {
335 if self.sections.is_empty() {
336 return None;
337 }
338
339 if self.sections.iter().all(|s| s.first_ts.is_some()) {
341 let idx = match self.sections.binary_search_by(|s| {
342 let a = s.first_ts.unwrap();
343 if a < ts {
344 std::cmp::Ordering::Less
345 } else if a > ts {
346 std::cmp::Ordering::Greater
347 } else {
348 std::cmp::Ordering::Equal
349 }
350 }) {
351 Ok(i) => i,
352 Err(i) => i, };
354
355 if idx < self.sections.len() {
356 return Some(idx);
357 }
358
359 let last = self.sections.last().unwrap();
362 if let Some(last_ts) = last.last_ts
363 && ts <= last_ts
364 {
365 return Some(self.sections.len() - 1);
366 }
367 return None;
368 }
369
370 if let Some(first_ts) = self.sections.first().and_then(|s| s.first_ts)
374 && ts <= first_ts
375 {
376 return Some(0);
377 }
378
379 if let Some(idx) = self
380 .sections
381 .iter()
382 .position(|s| match (s.first_ts, s.last_ts) {
383 (Some(a), Some(b)) => a <= ts && ts <= b,
384 (Some(a), None) => a <= ts,
385 _ => false,
386 })
387 {
388 return Some(idx);
389 }
390
391 let last = self.sections.last().unwrap();
392 match last.last_ts {
393 Some(b) if ts <= b => Some(self.sections.len() - 1),
394 _ => None,
395 }
396 }
397
398 fn touch_cache(&mut self, key: usize) {
399 if let Some(pos) = self.cache_order.iter().position(|k| *k == key) {
400 self.cache_order.remove(pos);
401 }
402 self.cache_order.push_back(key);
403 while self.cache_order.len() > self.cache_cap {
404 if let Some(old) = self.cache_order.pop_front()
405 && self.cache.remove(&old).is_some()
406 {
407 self.cache_evictions = self.cache_evictions.saturating_add(1);
408 }
409 }
410 }
411
412 fn load_section(&mut self, section_idx: usize) -> CuResult<&CachedSection<P>> {
413 if self.cache.contains_key(§ion_idx) {
414 self.cache_hits = self.cache_hits.saturating_add(1);
415 self.touch_cache(section_idx);
416 return Ok(self.cache.get(§ion_idx).unwrap());
418 }
419 self.cache_misses = self.cache_misses.saturating_add(1);
420
421 let entry = &self.sections[section_idx];
422 let (header, data) = read_section_at(&mut self.log_reader, entry.pos)?;
423 if header.entry_type != UnifiedLogType::CopperList {
424 return Err(CuError::from(
425 "Section type mismatch while loading copperlists",
426 ));
427 }
428
429 let (entries, timestamps) = decode_copperlists::<P, _>(&data, &self.time_of)?;
430 let cached = CachedSection {
431 entries,
432 timestamps,
433 };
434 self.cache.insert(section_idx, cached);
435 self.touch_cache(section_idx);
436 Ok(self.cache.get(§ion_idx).unwrap())
437 }
438
439 fn copperlist_at(
440 &mut self,
441 idx: usize,
442 ) -> CuResult<(Arc<crate::copperlist::CopperList<P>>, Option<CuTime>)> {
443 let section_idx = self
444 .find_section_for_index(idx)
445 .ok_or_else(|| CuError::from("Index outside copperlist log"))?;
446 let start_idx = self.sections[section_idx].start_idx;
447 let section = self.load_section(section_idx)?;
448 let local = idx - start_idx;
449 let cl = section
450 .entries
451 .get(local)
452 .ok_or_else(|| CuError::from("Corrupt section index vs cache"))?
453 .clone();
454 let ts = section.timestamps.get(local).copied().unwrap_or(None);
455 Ok((cl, ts))
456 }
457
458 fn index_for_culistid(&mut self, culistid: u64) -> CuResult<usize> {
459 let section_idx = self
460 .find_section_for_culistid(culistid)
461 .ok_or_else(|| CuError::from("Requested culistid not present in log"))?;
462 let start_idx = self.sections[section_idx].start_idx;
463 let section = self.load_section(section_idx)?;
464 for (offset, cl) in section.entries.iter().enumerate() {
465 if cl.id == culistid {
466 return Ok(start_idx + offset);
467 }
468 }
469 Err(CuError::from("culistid not found inside indexed section"))
470 }
471
472 fn index_for_time(&mut self, ts: CuTime) -> CuResult<usize> {
473 let section_idx = self
474 .find_section_for_time(ts)
475 .ok_or_else(|| CuError::from("No copperlist at or after requested timestamp"))?;
476 let start_idx = self.sections[section_idx].start_idx;
477 let section = self.load_section(section_idx)?;
478 let idx = start_idx;
479 for (i, maybe) in section.timestamps.iter().enumerate() {
480 if matches!(maybe, Some(t) if *t >= ts) {
481 return Ok(idx + i);
482 }
483 }
484 Err(CuError::from("Timestamp not found within section"))
485 }
486
487 fn replay_range(&mut self, start: usize, end: usize) -> CuResult<usize>
488 where
489 App: CurrentRuntimeCopperList<P>,
490 {
491 let mut replayed = 0usize;
492 for idx in start..=end {
493 let (entry, ts) = self.copperlist_at(idx)?;
494 if let Some(ts) = ts {
495 self.clock_mock.set_value(ts.as_nanos());
496 }
497 let clock_for_cb = self.robot_clock.clone();
498 let clock_mock_for_cb = self.clock_mock.clone();
499 let mut cb = (self.build_callback)(entry.as_ref(), clock_for_cb, clock_mock_for_cb);
500 self.app.run_one_iteration(&mut cb)?;
501 self.normalize_runtime_copperlist_snapshot(entry.as_ref())?;
502 replayed += 1;
503 self.current_idx = Some(idx);
504 }
505 Ok(replayed)
506 }
507
508 fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome>
509 where
510 App: CurrentRuntimeCopperList<P>,
511 {
512 self.ensure_started()?;
513 if target_idx >= self.total_entries {
514 return Err(CuError::from("Target index outside log"));
515 }
516 let (target_cl, _) = self.copperlist_at(target_idx)?;
517 let target_culistid = target_cl.id;
518
519 let keyframe_used: Option<u64>;
520 let replay_start: usize;
521
522 if let Some(current) = self.current_idx {
524 if target_idx == current {
525 return Ok(JumpOutcome {
526 culistid: target_culistid,
527 keyframe_culistid: self.last_keyframe,
528 replayed: 0,
529 });
530 }
531
532 if target_idx >= current {
533 replay_start = current + 1;
534 keyframe_used = self.last_keyframe;
535 } else {
536 let Some(kf) = self.nearest_keyframe(target_culistid) else {
538 return Err(CuError::from("No keyframe available to rewind"));
539 };
540 self.restore_keyframe(&kf)?;
541 self.clear_runtime_copperlist_snapshot();
542 keyframe_used = Some(kf.culistid);
543 replay_start = self.index_for_culistid(kf.culistid)?;
544 }
545 } else {
546 let Some(kf) = self.nearest_keyframe(target_culistid) else {
548 return Err(CuError::from("No keyframe found in log"));
549 };
550 self.restore_keyframe(&kf)?;
551 self.clear_runtime_copperlist_snapshot();
552 keyframe_used = Some(kf.culistid);
553 replay_start = self.index_for_culistid(kf.culistid)?;
554 }
555
556 if replay_start > target_idx {
557 return Err(CuError::from(
558 "Replay start past target index; log ordering issue",
559 ));
560 }
561
562 let replayed = self.replay_range(replay_start, target_idx)?;
563
564 Ok(JumpOutcome {
565 culistid: target_culistid,
566 keyframe_culistid: keyframe_used,
567 replayed,
568 })
569 }
570
571 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome>
573 where
574 App: CurrentRuntimeCopperList<P>,
575 {
576 let idx = self.index_for_culistid(culistid)?;
577 self.goto_index(idx)
578 }
579
580 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome>
582 where
583 App: CurrentRuntimeCopperList<P>,
584 {
585 let idx = self.index_for_time(ts)?;
586 self.goto_index(idx)
587 }
588
589 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome>
591 where
592 App: CurrentRuntimeCopperList<P>,
593 {
594 let current =
595 self.current_idx
596 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
597 let target = current + delta;
598 if target < 0 || target as usize >= self.total_entries {
599 return Err(CuError::from("Step would move outside log bounds"));
600 }
601 self.goto_index(target as usize)
602 }
603
604 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
606 match self.current_idx {
607 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
608 None => Ok(None),
609 }
610 }
611
612 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
614 if idx >= self.total_entries {
615 return Ok(None);
616 }
617 Ok(Some(self.copperlist_at(idx)?.0))
618 }
619
620 pub fn total_entries(&self) -> usize {
622 self.total_entries
623 }
624
625 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
627 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
628 }
629
630 pub fn section_cache_stats(&self) -> SectionCacheStats {
632 SectionCacheStats {
633 cap: self.cache_cap,
634 entries: self.cache.len(),
635 hits: self.cache_hits,
636 misses: self.cache_misses,
637 evictions: self.cache_evictions,
638 }
639 }
640
641 pub fn current_index(&self) -> Option<usize> {
643 self.current_idx
644 }
645
646 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
648 f(&mut self.app)
649 }
650}
651
652impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
653where
654 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
655 L: UnifiedLogWrite<S> + 'static,
656 S: SectionStorage,
657 P: CopperListTuple,
658 CB: for<'a> Fn(
659 &'a crate::copperlist::CopperList<P>,
660 RobotClock,
661 RobotClockMock,
662 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
663 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
664{
665 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
667 self.app
668 .reflect_task(task_id)
669 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
670 }
671
672 pub fn reflected_task_mut(
674 &mut self,
675 task_id: &str,
676 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
677 self.app
678 .reflect_task_mut(task_id)
679 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
680 }
681
682 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
684 let task = self.reflected_task(task_id)?;
685 #[cfg(not(feature = "reflect"))]
686 {
687 let _ = task;
688 Err(CuError::from(
689 "Task introspection is disabled. Rebuild with the `reflect` feature.",
690 ))
691 }
692
693 #[cfg(feature = "reflect")]
694 {
695 Ok(format!("{task:#?}"))
696 }
697 }
698
699 pub fn dump_reflected_task_schemas(&self) -> String {
701 #[cfg(feature = "reflect")]
702 let mut registry = TypeRegistry::default();
703 #[cfg(not(feature = "reflect"))]
704 let mut registry = TypeRegistry;
705 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
706 dump_type_registry_schema(®istry)
707 }
708}
709#[allow(clippy::type_complexity)]
711pub(crate) fn decode_copperlists<
712 P: CopperListTuple,
713 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
714>(
715 section: &[u8],
716 time_of: &TF,
717) -> CuResult<(
718 Vec<Arc<crate::copperlist::CopperList<P>>>,
719 Vec<Option<CuTime>>,
720)> {
721 let mut cursor = std::io::Cursor::new(section);
722 let mut entries = Vec::new();
723 let mut timestamps = Vec::new();
724 loop {
725 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
726 &mut cursor,
727 standard(),
728 ) {
729 Ok(cl) => {
730 timestamps.push(time_of(&cl));
731 entries.push(Arc::new(cl));
732 }
733 Err(DecodeError::UnexpectedEnd { .. }) => break,
734 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
735 break;
736 }
737 Err(e) => {
738 return Err(CuError::new_with_cause(
739 "Failed to decode CopperList section",
740 e,
741 ));
742 }
743 }
744 }
745 Ok((entries, timestamps))
746}
747
748#[allow(clippy::type_complexity)]
750fn scan_copperlist_section<
751 P: CopperListTuple,
752 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
753>(
754 section: &[u8],
755 time_of: &TF,
756) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
757 let mut cursor = std::io::Cursor::new(section);
758 let mut count = 0usize;
759 let mut first_id = None;
760 let mut last_id = None;
761 let mut first_ts = None;
762 let mut last_ts = None;
763 loop {
764 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
765 &mut cursor,
766 standard(),
767 ) {
768 Ok(cl) => {
769 let ts = time_of(&cl);
770 if ts.is_none() {
771 #[cfg(feature = "std")]
772 eprintln!(
773 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
774 cl.id
775 );
776 }
777 if first_id.is_none() {
778 first_id = Some(cl.id);
779 first_ts = ts;
780 }
781 if first_ts.is_none() {
783 first_ts = ts;
784 }
785 last_id = Some(cl.id);
786 last_ts = ts.or(last_ts);
787 count += 1;
788 }
789 Err(DecodeError::UnexpectedEnd { .. }) => break,
790 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
791 break;
792 }
793 Err(e) => {
794 return Err(CuError::new_with_cause(
795 "Failed to scan copperlist section",
796 e,
797 ));
798 }
799 }
800 }
801 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
802 let last_id = last_id.unwrap_or(first_id);
803 Ok((count, first_id, last_id, first_ts, last_ts))
804}
805
806pub(crate) fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
808 let logger = UnifiedLoggerBuilder::new()
809 .file_base_name(log_base)
810 .build()
811 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
812 let UnifiedLogger::Read(dl) = logger else {
813 return Err(CuError::from("Expected read-only unified logger"));
814 };
815 Ok(dl)
816}
817
818pub(crate) fn read_section_at(
820 log_reader: &mut UnifiedLoggerRead,
821 pos: LogPosition,
822) -> CuResult<(SectionHeader, Vec<u8>)> {
823 log_reader.seek(pos)?;
824 log_reader.raw_read_section()
825}
826
827pub(crate) fn index_log<P, TF>(
829 log_base: &Path,
830 time_of: &TF,
831) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
832where
833 P: CopperListTuple,
834 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
835{
836 let logger = UnifiedLoggerBuilder::new()
837 .file_base_name(log_base)
838 .build()
839 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
840 let UnifiedLogger::Read(mut dl) = logger else {
841 return Err(CuError::from("Expected read-only unified logger"));
842 };
843
844 let mut sections = Vec::new();
845 let mut keyframes = Vec::new();
846 let mut total_entries = 0usize;
847
848 loop {
849 let pos = dl.position();
850 let (header, data) = dl.raw_read_section()?;
851 if header.entry_type == UnifiedLogType::LastEntry {
852 break;
853 }
854
855 match header.entry_type {
856 UnifiedLogType::CopperList => {
857 let (len, first_id, last_id, first_ts, last_ts) =
858 scan_copperlist_section::<P, _>(&data, time_of)?;
859 if len == 0 {
860 continue;
861 }
862 sections.push(SectionIndexEntry {
863 pos,
864 start_idx: total_entries,
865 len,
866 first_id,
867 last_id,
868 first_ts,
869 last_ts,
870 });
871 total_entries += len;
872 }
873 UnifiedLogType::FrozenTasks => {
874 let mut cursor = std::io::Cursor::new(&data);
876 loop {
877 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
878 Ok(kf) => keyframes.push(kf),
879 Err(DecodeError::UnexpectedEnd { .. }) => break,
880 Err(DecodeError::Io { inner, .. })
881 if inner.kind() == io::ErrorKind::UnexpectedEof =>
882 {
883 break;
884 }
885 Err(e) => {
886 return Err(CuError::new_with_cause(
887 "Failed to decode keyframe section",
888 e,
889 ));
890 }
891 }
892 }
893 }
894 _ => {
895 }
897 }
898 }
899
900 Ok((sections, keyframes, total_entries))
901}