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