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 for (offset, cl) in section.entries.iter().enumerate() {
416 if cl.id == culistid {
417 return Ok(start_idx + offset);
418 }
419 }
420 Err(CuError::from("culistid not found inside indexed section"))
421 }
422
423 fn index_for_time(&mut self, ts: CuTime) -> CuResult<usize> {
424 let section_idx = self
425 .find_section_for_time(ts)
426 .ok_or_else(|| CuError::from("No copperlist at or after requested timestamp"))?;
427 let start_idx = self.sections[section_idx].start_idx;
428 let section = self.load_section(section_idx)?;
429 let idx = start_idx;
430 for (i, maybe) in section.timestamps.iter().enumerate() {
431 if matches!(maybe, Some(t) if *t >= ts) {
432 return Ok(idx + i);
433 }
434 }
435 Err(CuError::from("Timestamp not found within section"))
436 }
437
438 fn replay_range(&mut self, start: usize, end: usize) -> CuResult<usize> {
439 let mut replayed = 0usize;
440 for idx in start..=end {
441 let (entry, ts) = self.copperlist_at(idx)?;
442 if let Some(ts) = ts {
443 self.clock_mock.set_value(ts.as_nanos());
444 }
445 let clock_for_cb = self.robot_clock.clone();
446 let mut cb = (self.build_callback)(entry.as_ref(), clock_for_cb);
447 self.app.run_one_iteration(&mut cb)?;
448 replayed += 1;
449 self.current_idx = Some(idx);
450 }
451 Ok(replayed)
452 }
453
454 fn goto_index(&mut self, target_idx: usize) -> CuResult<JumpOutcome> {
455 self.ensure_started()?;
456 if target_idx >= self.total_entries {
457 return Err(CuError::from("Target index outside log"));
458 }
459 let (target_cl, _) = self.copperlist_at(target_idx)?;
460 let target_culistid = target_cl.id;
461
462 let keyframe_used: Option<u64>;
463 let replay_start: usize;
464
465 if let Some(current) = self.current_idx {
467 if target_idx == current {
468 return Ok(JumpOutcome {
469 culistid: target_culistid,
470 keyframe_culistid: self.last_keyframe,
471 replayed: 0,
472 });
473 }
474
475 if target_idx >= current {
476 replay_start = current + 1;
477 keyframe_used = self.last_keyframe;
478 } else {
479 let Some(kf) = self.nearest_keyframe(target_culistid) else {
481 return Err(CuError::from("No keyframe available to rewind"));
482 };
483 self.restore_keyframe(&kf)?;
484 keyframe_used = Some(kf.culistid);
485 replay_start = self.index_for_culistid(kf.culistid)?;
486 }
487 } else {
488 let Some(kf) = self.nearest_keyframe(target_culistid) else {
490 return Err(CuError::from("No keyframe found in log"));
491 };
492 self.restore_keyframe(&kf)?;
493 keyframe_used = Some(kf.culistid);
494 replay_start = self.index_for_culistid(kf.culistid)?;
495 }
496
497 if replay_start > target_idx {
498 return Err(CuError::from(
499 "Replay start past target index; log ordering issue",
500 ));
501 }
502
503 let replayed = self.replay_range(replay_start, target_idx)?;
504
505 Ok(JumpOutcome {
506 culistid: target_culistid,
507 keyframe_culistid: keyframe_used,
508 replayed,
509 })
510 }
511
512 pub fn goto_cl(&mut self, culistid: u64) -> CuResult<JumpOutcome> {
514 let idx = self.index_for_culistid(culistid)?;
515 self.goto_index(idx)
516 }
517
518 pub fn goto_time(&mut self, ts: CuTime) -> CuResult<JumpOutcome> {
520 let idx = self.index_for_time(ts)?;
521 self.goto_index(idx)
522 }
523
524 pub fn step(&mut self, delta: i32) -> CuResult<JumpOutcome> {
526 let current =
527 self.current_idx
528 .ok_or_else(|| CuError::from("Cannot step before any jump"))? as i32;
529 let target = current + delta;
530 if target < 0 || target as usize >= self.total_entries {
531 return Err(CuError::from("Step would move outside log bounds"));
532 }
533 self.goto_index(target as usize)
534 }
535
536 pub fn current_cl(&mut self) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
538 match self.current_idx {
539 Some(idx) => Ok(Some(self.copperlist_at(idx)?.0)),
540 None => Ok(None),
541 }
542 }
543
544 pub fn cl_at(&mut self, idx: usize) -> CuResult<Option<Arc<crate::copperlist::CopperList<P>>>> {
546 if idx >= self.total_entries {
547 return Ok(None);
548 }
549 Ok(Some(self.copperlist_at(idx)?.0))
550 }
551
552 pub fn total_entries(&self) -> usize {
554 self.total_entries
555 }
556
557 pub fn nearest_keyframe_culistid(&self, target_culistid: u64) -> Option<u64> {
559 self.nearest_keyframe(target_culistid).map(|kf| kf.culistid)
560 }
561
562 pub fn section_cache_stats(&self) -> SectionCacheStats {
564 SectionCacheStats {
565 cap: self.cache_cap,
566 entries: self.cache.len(),
567 hits: self.cache_hits,
568 misses: self.cache_misses,
569 evictions: self.cache_evictions,
570 }
571 }
572
573 pub fn current_index(&self) -> Option<usize> {
575 self.current_idx
576 }
577
578 pub fn with_app<R>(&mut self, f: impl FnOnce(&mut App) -> R) -> R {
580 f(&mut self.app)
581 }
582}
583
584impl<App, P, CB, TF, S, L> CuDebugSession<App, P, CB, TF, S, L>
585where
586 App: CuSimApplication<S, L> + ReflectTaskIntrospection,
587 L: UnifiedLogWrite<S> + 'static,
588 S: SectionStorage,
589 P: CopperListTuple,
590 CB: for<'a> Fn(
591 &'a crate::copperlist::CopperList<P>,
592 RobotClock,
593 ) -> Box<dyn for<'z> FnMut(App::Step<'z>) -> SimOverride + 'a>,
594 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime> + Clone,
595{
596 pub fn reflected_task(&self, task_id: &str) -> CuResult<&dyn crate::reflect::Reflect> {
598 self.app
599 .reflect_task(task_id)
600 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
601 }
602
603 pub fn reflected_task_mut(
605 &mut self,
606 task_id: &str,
607 ) -> CuResult<&mut dyn crate::reflect::Reflect> {
608 self.app
609 .reflect_task_mut(task_id)
610 .ok_or_else(|| CuError::from(format!("Task '{task_id}' was not found.")))
611 }
612
613 pub fn dump_reflected_task(&self, task_id: &str) -> CuResult<String> {
615 let task = self.reflected_task(task_id)?;
616 #[cfg(not(feature = "reflect"))]
617 {
618 let _ = task;
619 Err(CuError::from(
620 "Task introspection is disabled. Rebuild with the `reflect` feature.",
621 ))
622 }
623
624 #[cfg(feature = "reflect")]
625 {
626 Ok(format!("{task:#?}"))
627 }
628 }
629
630 pub fn dump_reflected_task_schemas(&self) -> String {
632 #[cfg(feature = "reflect")]
633 let mut registry = TypeRegistry::default();
634 #[cfg(not(feature = "reflect"))]
635 let mut registry = TypeRegistry;
636 <App as ReflectTaskIntrospection>::register_reflect_types(&mut registry);
637 dump_type_registry_schema(®istry)
638 }
639}
640#[allow(clippy::type_complexity)]
642fn decode_copperlists<
643 P: CopperListTuple,
644 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
645>(
646 section: &[u8],
647 time_of: &TF,
648) -> CuResult<(
649 Vec<Arc<crate::copperlist::CopperList<P>>>,
650 Vec<Option<CuTime>>,
651)> {
652 let mut cursor = std::io::Cursor::new(section);
653 let mut entries = Vec::new();
654 let mut timestamps = Vec::new();
655 loop {
656 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
657 &mut cursor,
658 standard(),
659 ) {
660 Ok(cl) => {
661 timestamps.push(time_of(&cl));
662 entries.push(Arc::new(cl));
663 }
664 Err(DecodeError::UnexpectedEnd { .. }) => break,
665 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
666 break;
667 }
668 Err(e) => {
669 return Err(CuError::new_with_cause(
670 "Failed to decode CopperList section",
671 e,
672 ));
673 }
674 }
675 }
676 Ok((entries, timestamps))
677}
678
679#[allow(clippy::type_complexity)]
681fn scan_copperlist_section<
682 P: CopperListTuple,
683 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
684>(
685 section: &[u8],
686 time_of: &TF,
687) -> CuResult<(usize, u64, u64, Option<CuTime>, Option<CuTime>)> {
688 let mut cursor = std::io::Cursor::new(section);
689 let mut count = 0usize;
690 let mut first_id = None;
691 let mut last_id = None;
692 let mut first_ts = None;
693 let mut last_ts = None;
694 loop {
695 match decode_from_std_read::<crate::copperlist::CopperList<P>, _, _>(
696 &mut cursor,
697 standard(),
698 ) {
699 Ok(cl) => {
700 let ts = time_of(&cl);
701 if ts.is_none() {
702 #[cfg(feature = "std")]
703 eprintln!(
704 "CuDebug index warning: missing timestamp on culistid {}; time-based seek may be less accurate",
705 cl.id
706 );
707 }
708 if first_id.is_none() {
709 first_id = Some(cl.id);
710 first_ts = ts;
711 }
712 if first_ts.is_none() {
714 first_ts = ts;
715 }
716 last_id = Some(cl.id);
717 last_ts = ts.or(last_ts);
718 count += 1;
719 }
720 Err(DecodeError::UnexpectedEnd { .. }) => break,
721 Err(DecodeError::Io { inner, .. }) if inner.kind() == io::ErrorKind::UnexpectedEof => {
722 break;
723 }
724 Err(e) => {
725 return Err(CuError::new_with_cause(
726 "Failed to scan copperlist section",
727 e,
728 ));
729 }
730 }
731 }
732 let first_id = first_id.ok_or_else(|| CuError::from("Empty copperlist section"))?;
733 let last_id = last_id.unwrap_or(first_id);
734 Ok((count, first_id, last_id, first_ts, last_ts))
735}
736
737fn build_read_logger(log_base: &Path) -> CuResult<UnifiedLoggerRead> {
739 let logger = UnifiedLoggerBuilder::new()
740 .file_base_name(log_base)
741 .build()
742 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
743 let UnifiedLogger::Read(dl) = logger else {
744 return Err(CuError::from("Expected read-only unified logger"));
745 };
746 Ok(dl)
747}
748
749fn read_section_at(
751 log_reader: &mut UnifiedLoggerRead,
752 pos: LogPosition,
753) -> CuResult<(SectionHeader, Vec<u8>)> {
754 log_reader.seek(pos)?;
755 log_reader.raw_read_section()
756}
757
758fn index_log<P, TF>(
760 log_base: &Path,
761 time_of: &TF,
762) -> CuResult<(Vec<SectionIndexEntry>, Vec<KeyFrame>, usize)>
763where
764 P: CopperListTuple,
765 TF: Fn(&crate::copperlist::CopperList<P>) -> Option<CuTime>,
766{
767 let logger = UnifiedLoggerBuilder::new()
768 .file_base_name(log_base)
769 .build()
770 .map_err(|e| CuError::new_with_cause("Failed to open unified log", e))?;
771 let UnifiedLogger::Read(mut dl) = logger else {
772 return Err(CuError::from("Expected read-only unified logger"));
773 };
774
775 let mut sections = Vec::new();
776 let mut keyframes = Vec::new();
777 let mut total_entries = 0usize;
778
779 loop {
780 let pos = dl.position();
781 let (header, data) = dl.raw_read_section()?;
782 if header.entry_type == UnifiedLogType::LastEntry {
783 break;
784 }
785
786 match header.entry_type {
787 UnifiedLogType::CopperList => {
788 let (len, first_id, last_id, first_ts, last_ts) =
789 scan_copperlist_section::<P, _>(&data, time_of)?;
790 if len == 0 {
791 continue;
792 }
793 sections.push(SectionIndexEntry {
794 pos,
795 start_idx: total_entries,
796 len,
797 first_id,
798 last_id,
799 first_ts,
800 last_ts,
801 });
802 total_entries += len;
803 }
804 UnifiedLogType::FrozenTasks => {
805 let mut cursor = std::io::Cursor::new(&data);
807 loop {
808 match decode_from_std_read::<KeyFrame, _, _>(&mut cursor, standard()) {
809 Ok(kf) => keyframes.push(kf),
810 Err(DecodeError::UnexpectedEnd { .. }) => break,
811 Err(DecodeError::Io { inner, .. })
812 if inner.kind() == io::ErrorKind::UnexpectedEof =>
813 {
814 break;
815 }
816 Err(e) => {
817 return Err(CuError::new_with_cause(
818 "Failed to decode keyframe section",
819 e,
820 ));
821 }
822 }
823 }
824 }
825 _ => {
826 }
828 }
829 }
830
831 Ok((sections, keyframes, total_entries))
832}