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 fn ensure_started(&mut self) -> CuResult<()> {
233 if self.started {
234 return Ok(());
235 }
236 let mut noop = |_step: App::Step<'_>| SimOverride::ExecuteByRuntime;
237 self.app.start_all_tasks(&mut noop)?;
238 self.started = true;
239 Ok(())
240 }
241
242 fn nearest_keyframe(&self, target_culistid: u64) -> Option<KeyFrame> {
243 self.keyframes
244 .iter()
245 .filter(|kf| kf.culistid <= target_culistid)
246 .max_by_key(|kf| kf.culistid)
247 .cloned()
248 }
249
250 fn restore_keyframe(&mut self, kf: &KeyFrame) -> CuResult<()> {
251 self.app.restore_keyframe(kf)?;
252 self.clock_mock.set_value(kf.timestamp.as_nanos());
253 self.last_keyframe = Some(kf.culistid);
254 Ok(())
255 }
256
257 fn clear_runtime_copperlist_snapshot(&mut self)
258 where
259 App: CurrentRuntimeCopperList<P>,
260 {
261 self.app.set_current_runtime_copperlist_bytes(None);
262 }
263
264 fn normalize_runtime_copperlist_snapshot(
265 &mut self,
266 recorded: &crate::copperlist::CopperList<P>,
267 ) -> CuResult<()>
268 where
269 App: CurrentRuntimeCopperList<P>,
270 {
271 let normalized = self
272 .app
273 .current_runtime_copperlist_bytes()
274 .map(|bytes| {
275 let (mut runtime_cl, _) = bincode::decode_from_slice::<
276 crate::copperlist::CopperList<P>,
277 _,
278 >(bytes, standard())
279 .map_err(|e| {
280 CuError::new_with_cause("Failed to decode runtime CopperList snapshot", e)
281 })?;
282 runtime_cl.id = recorded.id;
283 runtime_cl.change_state(recorded.get_state());
284 bincode::encode_to_vec(&runtime_cl, standard()).map_err(|e| {
285 CuError::new_with_cause("Failed to encode normalized CopperList snapshot", e)
286 })
287 })
288 .transpose()?;
289 self.app.set_current_runtime_copperlist_bytes(normalized);
290 Ok(())
291 }
292
293 fn find_section_for_index(&self, idx: usize) -> Option<usize> {
294 self.sections
295 .binary_search_by(|s| {
296 if idx < s.start_idx {
297 std::cmp::Ordering::Greater
298 } else if idx >= s.start_idx + s.len {
299 std::cmp::Ordering::Less
300 } else {
301 std::cmp::Ordering::Equal
302 }
303 })
304 .ok()
305 }
306
307 fn find_section_for_culistid(&self, culistid: u64) -> Option<usize> {
308 self.sections
309 .binary_search_by(|s| {
310 if culistid < s.first_id {
311 std::cmp::Ordering::Greater
312 } else if culistid > s.last_id {
313 std::cmp::Ordering::Less
314 } else {
315 std::cmp::Ordering::Equal
316 }
317 })
318 .ok()
319 }
320
321 fn find_section_for_time(&self, ts: CuTime) -> Option<usize> {
325 if self.sections.is_empty() {
326 return None;
327 }
328
329 if self.sections.iter().all(|s| s.first_ts.is_some()) {
331 let idx = match self.sections.binary_search_by(|s| {
332 let a = s.first_ts.unwrap();
333 if a < ts {
334 std::cmp::Ordering::Less
335 } else if a > ts {
336 std::cmp::Ordering::Greater
337 } else {
338 std::cmp::Ordering::Equal
339 }
340 }) {
341 Ok(i) => i,
342 Err(i) => i, };
344
345 if idx < self.sections.len() {
346 return Some(idx);
347 }
348
349 let last = self.sections.last().unwrap();
352 if let Some(last_ts) = last.last_ts
353 && ts <= last_ts
354 {
355 return Some(self.sections.len() - 1);
356 }
357 return None;
358 }
359
360 if let Some(first_ts) = self.sections.first().and_then(|s| s.first_ts)
364 && ts <= first_ts
365 {
366 return Some(0);
367 }
368
369 if let Some(idx) = self
370 .sections
371 .iter()
372 .position(|s| match (s.first_ts, s.last_ts) {
373 (Some(a), Some(b)) => a <= ts && ts <= b,
374 (Some(a), None) => a <= ts,
375 _ => false,
376 })
377 {
378 return Some(idx);
379 }
380
381 let last = self.sections.last().unwrap();
382 match last.last_ts {
383 Some(b) if ts <= b => Some(self.sections.len() - 1),
384 _ => None,
385 }
386 }
387
388 fn touch_cache(&mut self, key: usize) {
389 if let Some(pos) = self.cache_order.iter().position(|k| *k == key) {
390 self.cache_order.remove(pos);
391 }
392 self.cache_order.push_back(key);
393 while self.cache_order.len() > self.cache_cap {
394 if let Some(old) = self.cache_order.pop_front()
395 && self.cache.remove(&old).is_some()
396 {
397 self.cache_evictions = self.cache_evictions.saturating_add(1);
398 }
399 }
400 }
401
402 fn load_section(&mut self, section_idx: usize) -> CuResult<&CachedSection<P>> {
403 if self.cache.contains_key(§ion_idx) {
404 self.cache_hits = self.cache_hits.saturating_add(1);
405 self.touch_cache(section_idx);
406 return Ok(self.cache.get(§ion_idx).unwrap());
408 }
409 self.cache_misses = self.cache_misses.saturating_add(1);
410
411 let entry = &self.sections[section_idx];
412 let (header, data) = read_section_at(&mut self.log_reader, entry.pos)?;
413 if header.entry_type != UnifiedLogType::CopperList {
414 return Err(CuError::from(
415 "Section type mismatch while loading copperlists",
416 ));
417 }
418
419 let (entries, timestamps) = decode_copperlists::<P, _>(&data, &self.time_of)?;
420 let cached = CachedSection {
421 entries,
422 timestamps,
423 };
424 self.cache.insert(section_idx, cached);
425 self.touch_cache(section_idx);
426 Ok(self.cache.get(§ion_idx).unwrap())
427 }
428
429 fn copperlist_at(
430 &mut self,
431 idx: usize,
432 ) -> CuResult<(Arc<crate::copperlist::CopperList<P>>, Option<CuTime>)> {
433 let section_idx = self
434 .find_section_for_index(idx)
435 .ok_or_else(|| CuError::from("Index outside copperlist log"))?;
436 let start_idx = self.sections[section_idx].start_idx;
437 let section = self.load_section(section_idx)?;
438 let local = idx - start_idx;
439 let cl = section
440 .entries
441 .get(local)
442 .ok_or_else(|| CuError::from("Corrupt section index vs cache"))?
443 .clone();
444 let ts = section.timestamps.get(local).copied().unwrap_or(None);
445 Ok((cl, ts))
446 }
447
448 fn index_for_culistid(&mut self, culistid: u64) -> CuResult<usize> {
449 let section_idx = self
450 .find_section_for_culistid(culistid)
451 .ok_or_else(|| CuError::from("Requested culistid not present in log"))?;
452 let start_idx = self.sections[section_idx].start_idx;
453 let section = self.load_section(section_idx)?;
454 for (offset, cl) in section.entries.iter().enumerate() {
455 if cl.id == culistid {
456 return Ok(start_idx + offset);
457 }
458 }
459 Err(CuError::from("culistid not found inside indexed section"))
460 }
461
462 fn index_for_time(&mut self, ts: CuTime) -> CuResult<usize> {
463 let section_idx = self
464 .find_section_for_time(ts)
465 .ok_or_else(|| CuError::from("No copperlist at or after requested timestamp"))?;
466 let start_idx = self.sections[section_idx].start_idx;
467 let section = self.load_section(section_idx)?;
468 let idx = start_idx;
469 for (i, maybe) in section.timestamps.iter().enumerate() {
470 if matches!(maybe, Some(t) if *t >= ts) {
471 return Ok(idx + i);
472 }
473 }
474 Err(CuError::from("Timestamp not found within section"))
475 }
476
477 fn replay_range(&mut self, start: usize, end: usize) -> CuResult<usize>
478 where
479 App: CurrentRuntimeCopperList<P>,
480 {
481 let mut replayed = 0usize;
482 for idx in start..=end {
483 let (entry, ts) = self.copperlist_at(idx)?;
484 if let Some(ts) = ts {
485 self.clock_mock.set_value(ts.as_nanos());
486 }
487 let clock_for_cb = self.robot_clock.clone();
488 let clock_mock_for_cb = self.clock_mock.clone();
489 let mut cb = (self.build_callback)(entry.as_ref(), clock_for_cb, clock_mock_for_cb);
490 self.app.run_one_iteration(&mut cb)?;
491 self.normalize_runtime_copperlist_snapshot(entry.as_ref())?;
492 replayed += 1;
493 self.current_idx = Some(idx);
494 }
495 Ok(replayed)
496 }
497
498 fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome>
499 where
500 App: CurrentRuntimeCopperList<P>,
501 {
502 self.ensure_started()?;
503 if target_idx >= self.total_entries {
504 return Err(CuError::from("Target index outside log"));
505 }
506 let (target_cl, _) = self.copperlist_at(target_idx)?;
507 let target_culistid = target_cl.id;
508
509 let keyframe_used: Option<u64>;
510 let replay_start: usize;
511
512 if let Some(current) = self.current_idx {
514 if target_idx == current {
515 return Ok(JumpOutcome {
516 culistid: target_culistid,
517 keyframe_culistid: self.last_keyframe,
518 replayed: 0,
519 });
520 }
521
522 if target_idx >= current {
523 replay_start = current + 1;
524 keyframe_used = self.last_keyframe;
525 } else {
526 let Some(kf) = self.nearest_keyframe(target_culistid) else {
528 return Err(CuError::from("No keyframe available to rewind"));
529 };
530 self.restore_keyframe(&kf)?;
531 self.clear_runtime_copperlist_snapshot();
532 keyframe_used = Some(kf.culistid);
533 replay_start = self.index_for_culistid(kf.culistid)?;
534 }
535 } else {
536 let Some(kf) = self.nearest_keyframe(target_culistid) else {
538 return Err(CuError::from("No keyframe found in log"));
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
546 if replay_start > target_idx {
547 return Err(CuError::from(
548 "Replay start past target index; log ordering issue",
549 ));
550 }
551
552 let replayed = self.replay_range(replay_start, target_idx)?;
553
554 Ok(JumpOutcome {
555 culistid: target_culistid,
556 keyframe_culistid: keyframe_used,
557 replayed,
558 })
559 }
560
561 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome>
563 where
564 App: CurrentRuntimeCopperList<P>,
565 {
566 let idx = self.index_for_culistid(culistid)?;
567 self.goto_index(idx)
568 }
569
570 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome>
572 where
573 App: CurrentRuntimeCopperList<P>,
574 {
575 let idx = self.index_for_time(ts)?;
576 self.goto_index(idx)
577 }
578
579 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome>
581 where
582 App: CurrentRuntimeCopperList<P>,
583 {
584 let current =
585 self.current_idx
586 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
587 let target = current + delta;
588 if target < 0 || target as usize >= self.total_entries {
589 return Err(CuError::from("Step would move outside log bounds"));
590 }
591 self.goto_index(target as usize)
592 }
593
594 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
596 match self.current_idx {
597 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
598 None => Ok(None),
599 }
600 }
601
602 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
604 if idx >= self.total_entries {
605 return Ok(None);
606 }
607 Ok(Some(self.copperlist_at(idx)?.0))
608 }
609
610 pub fn total_entries(&self) -> usize {
612 self.total_entries
613 }
614
615 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
617 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
618 }
619
620 pub fn section_cache_stats(&self) -> SectionCacheStats {
622 SectionCacheStats {
623 cap: self.cache_cap,
624 entries: self.cache.len(),
625 hits: self.cache_hits,
626 misses: self.cache_misses,
627 evictions: self.cache_evictions,
628 }
629 }
630
631 pub fn current_index(&self) -> Option<usize> {
633 self.current_idx
634 }
635
636 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
638 f(&mut self.app)
639 }
640}
641
642impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
643where
644 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
645 L: UnifiedLogWrite<S> + 'static,
646 S: SectionStorage,
647 P: CopperListTuple,
648 CB: for<'a> Fn(
649 &'a crate::copperlist::CopperList<P>,
650 RobotClock,
651 RobotClockMock,
652 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
653 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
654{
655 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
657 self.app
658 .reflect_task(task_id)
659 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
660 }
661
662 pub fn reflected_task_mut(
664 &mut self,
665 task_id: &str,
666 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
667 self.app
668 .reflect_task_mut(task_id)
669 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
670 }
671
672 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
674 let task = self.reflected_task(task_id)?;
675 #[cfg(not(feature = "reflect"))]
676 {
677 let _ = task;
678 Err(CuError::from(
679 "Task introspection is disabled. Rebuild with the `reflect` feature.",
680 ))
681 }
682
683 #[cfg(feature = "reflect")]
684 {
685 Ok(format!("{task:#?}"))
686 }
687 }
688
689 pub fn dump_reflected_task_schemas(&self) -> String {
691 #[cfg(feature = "reflect")]
692 let mut registry = TypeRegistry::default();
693 #[cfg(not(feature = "reflect"))]
694 let mut registry = TypeRegistry;
695 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
696 dump_type_registry_schema(®istry)
697 }
698}
699#[allow(clippy::type_complexity)]
701pub(crate) fn decode_copperlists<
702 P: CopperListTuple,
703 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
704>(
705 section: &[u8],
706 time_of: &TF,
707) -> CuResult<(
708 Vec<Arc<crate::copperlist::CopperList<P>>>,
709 Vec<Option<CuTime>>,
710)> {
711 let mut cursor = std::io::Cursor::new(section);
712 let mut entries = Vec::new();
713 let mut timestamps = Vec::new();
714 loop {
715 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
716 &mut cursor,
717 standard(),
718 ) {
719 Ok(cl) => {
720 timestamps.push(time_of(&cl));
721 entries.push(Arc::new(cl));
722 }
723 Err(DecodeError::UnexpectedEnd { .. }) => break,
724 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
725 break;
726 }
727 Err(e) => {
728 return Err(CuError::new_with_cause(
729 "Failed to decode CopperList section",
730 e,
731 ));
732 }
733 }
734 }
735 Ok((entries, timestamps))
736}
737
738#[allow(clippy::type_complexity)]
740fn scan_copperlist_section<
741 P: CopperListTuple,
742 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
743>(
744 section: &[u8],
745 time_of: &TF,
746) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
747 let mut cursor = std::io::Cursor::new(section);
748 let mut count = 0usize;
749 let mut first_id = None;
750 let mut last_id = None;
751 let mut first_ts = None;
752 let mut last_ts = None;
753 loop {
754 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
755 &mut cursor,
756 standard(),
757 ) {
758 Ok(cl) => {
759 let ts = time_of(&cl);
760 if ts.is_none() {
761 #[cfg(feature = "std")]
762 eprintln!(
763 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
764 cl.id
765 );
766 }
767 if first_id.is_none() {
768 first_id = Some(cl.id);
769 first_ts = ts;
770 }
771 if first_ts.is_none() {
773 first_ts = ts;
774 }
775 last_id = Some(cl.id);
776 last_ts = ts.or(last_ts);
777 count += 1;
778 }
779 Err(DecodeError::UnexpectedEnd { .. }) => break,
780 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
781 break;
782 }
783 Err(e) => {
784 return Err(CuError::new_with_cause(
785 "Failed to scan copperlist section",
786 e,
787 ));
788 }
789 }
790 }
791 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
792 let last_id = last_id.unwrap_or(first_id);
793 Ok((count, first_id, last_id, first_ts, last_ts))
794}
795
796pub(crate) fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
798 let logger = UnifiedLoggerBuilder::new()
799 .file_base_name(log_base)
800 .build()
801 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
802 let UnifiedLogger::Read(dl) = logger else {
803 return Err(CuError::from("Expected read-only unified logger"));
804 };
805 Ok(dl)
806}
807
808pub(crate) fn read_section_at(
810 log_reader: &mut UnifiedLoggerRead,
811 pos: LogPosition,
812) -> CuResult<(SectionHeader, Vec<u8>)> {
813 log_reader.seek(pos)?;
814 log_reader.raw_read_section()
815}
816
817pub(crate) fn index_log<P, TF>(
819 log_base: &Path,
820 time_of: &TF,
821) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
822where
823 P: CopperListTuple,
824 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
825{
826 let logger = UnifiedLoggerBuilder::new()
827 .file_base_name(log_base)
828 .build()
829 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
830 let UnifiedLogger::Read(mut dl) = logger else {
831 return Err(CuError::from("Expected read-only unified logger"));
832 };
833
834 let mut sections = Vec::new();
835 let mut keyframes = Vec::new();
836 let mut total_entries = 0usize;
837
838 loop {
839 let pos = dl.position();
840 let (header, data) = dl.raw_read_section()?;
841 if header.entry_type == UnifiedLogType::LastEntry {
842 break;
843 }
844
845 match header.entry_type {
846 UnifiedLogType::CopperList => {
847 let (len, first_id, last_id, first_ts, last_ts) =
848 scan_copperlist_section::<P, _>(&data, time_of)?;
849 if len == 0 {
850 continue;
851 }
852 sections.push(SectionIndexEntry {
853 pos,
854 start_idx: total_entries,
855 len,
856 first_id,
857 last_id,
858 first_ts,
859 last_ts,
860 });
861 total_entries += len;
862 }
863 UnifiedLogType::FrozenTasks => {
864 let mut cursor = std::io::Cursor::new(&data);
866 loop {
867 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
868 Ok(kf) => keyframes.push(kf),
869 Err(DecodeError::UnexpectedEnd { .. }) => break,
870 Err(DecodeError::Io { inner, .. })
871 if inner.kind() == io::ErrorKind::UnexpectedEof =>
872 {
873 break;
874 }
875 Err(e) => {
876 return Err(CuError::new_with_cause(
877 "Failed to decode keyframe section",
878 e,
879 ));
880 }
881 }
882 }
883 }
884 _ => {
885 }
887 }
888 }
889
890 Ok((sections, keyframes, total_entries))
891}