1use crate::app::Subsystem;
6#[cfg(feature = "std")]
7use crate::arena::{CuSlotLease, allocate_slots};
8use crate::config::{ComponentConfig, DEFAULT_KEYFRAME_INTERVAL, Node, TaskKind};
9use crate::config::{
10 CuConfig, CuGraph, MAX_RATE_TARGET_HZ, NodeId, RuntimeConfig, resolve_task_kind_for_id,
11};
12use crate::copperlist::{CopperList, CopperListState, CuListZeroedInit, CuListsManager};
13use crate::cutask::{BincodeAdapter, Freezable};
14#[cfg(feature = "std")]
15use crate::monitoring::ExecutionProbeHandle;
16#[cfg(feature = "std")]
17use crate::monitoring::MonitorExecutionProbe;
18use crate::monitoring::{
19 ComponentId, CopperListInfo, CuMonitor, CuMonitoringMetadata, CuMonitoringRuntime,
20 ExecutionMarker, MonitorComponentMetadata, RuntimeExecutionProbe, build_monitor_topology,
21 take_last_completed_handle_bytes,
22};
23#[cfg(all(feature = "std", feature = "parallel-rt"))]
24use crate::parallel_rt::ParallelRt;
25#[cfg(feature = "std")]
26use crate::parallel_rt::ParallelRtMetadata;
27use crate::planner::{check_order, plan_from_order, topo_bfs_order};
28use crate::resource::ResourceManager;
29#[cfg(feature = "std")]
30use alloc::sync::Arc;
31use compact_str::CompactString;
32use cu29_clock::{ClockProvider, CuDuration, CuTime, RobotClock};
33use cu29_traits::CuResult;
34use cu29_traits::WriteStream;
35use cu29_traits::{CopperListTuple, CuError};
36#[cfg(feature = "std")]
37use rayon::ThreadPool;
38
39#[cfg(target_os = "none")]
40#[allow(unused_imports)]
41use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
42#[cfg(target_os = "none")]
43#[allow(unused_imports)]
44use cu29_log_derive::info;
45#[cfg(target_os = "none")]
46#[allow(unused_imports)]
47use cu29_log_runtime::log;
48#[cfg(all(target_os = "none", debug_assertions))]
49#[allow(unused_imports)]
50use cu29_log_runtime::log_debug_mode;
51#[cfg(target_os = "none")]
52#[allow(unused_imports)]
53use cu29_value::to_value;
54
55use alloc::boxed::Box;
56use alloc::format;
57use alloc::string::{String, ToString};
58use alloc::vec::Vec;
59use bincode::de::read::Reader;
60use bincode::de::{Decoder, DecoderImpl};
61use bincode::enc::EncoderImpl;
62use bincode::enc::write::{SizeWriter, Writer};
63use bincode::error::{DecodeError, EncodeError};
64use bincode::{Decode, Encode};
65use core::fmt::Result as FmtResult;
66use core::fmt::{Debug, Formatter};
67use core::marker::PhantomData;
68
69#[cfg(all(feature = "std", feature = "async-cl-io"))]
70use rtrb::{Consumer, PopError, Producer, PushError, RingBuffer};
71#[cfg(all(feature = "std", feature = "async-cl-io"))]
72use std::sync::atomic::{AtomicBool, Ordering};
73#[cfg(all(feature = "std", feature = "async-cl-io"))]
74use std::thread::{JoinHandle, Thread};
75
76#[cfg(feature = "std")]
77#[doc(hidden)]
78pub type TasksInstantiator<CT> = for<'c> fn(
79 Vec<Option<&'c ComponentConfig>>,
80 &mut ResourceManager,
81 &[Option<Arc<ThreadPool>>],
82) -> CuResult<CT>;
83#[cfg(not(feature = "std"))]
84#[doc(hidden)]
85pub type TasksInstantiator<CT> =
86 for<'c> fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>;
87#[doc(hidden)]
88pub type BridgesInstantiator<CB> = fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>;
89#[cfg(feature = "std")]
96#[doc(hidden)]
97pub type ThreadPoolsInstantiator = fn(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>;
98#[doc(hidden)]
99pub type MonitorInstantiator<M> = fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M;
100
101#[doc(hidden)]
102pub struct CuRuntimeParts<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI> {
103 pub tasks_instanciator: TI,
104 pub monitored_components: &'static [MonitorComponentMetadata],
105 pub culist_component_mapping: &'static [ComponentId],
106 #[cfg(feature = "std")]
107 pub parallel_rt_metadata: &'static ParallelRtMetadata,
108 pub monitor_instanciator: MI,
109 pub bridges_instanciator: BI,
110 _payload: PhantomData<(CT, CB, P, M, [(); NBCL])>,
111}
112
113impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI>
114 CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>
115{
116 pub const fn new(
117 tasks_instanciator: TI,
118 monitored_components: &'static [MonitorComponentMetadata],
119 culist_component_mapping: &'static [ComponentId],
120 #[cfg(feature = "std")] parallel_rt_metadata: &'static ParallelRtMetadata,
121 monitor_instanciator: MI,
122 bridges_instanciator: BI,
123 ) -> Self {
124 Self {
125 tasks_instanciator,
126 monitored_components,
127 culist_component_mapping,
128 #[cfg(feature = "std")]
129 parallel_rt_metadata,
130 monitor_instanciator,
131 bridges_instanciator,
132 _payload: PhantomData,
133 }
134 }
135}
136
137#[doc(hidden)]
138pub struct CuRuntimeBuilder<
139 'cfg,
140 CT,
141 CB,
142 P: CopperListTuple,
143 M: CuMonitor,
144 const NBCL: usize,
145 TI,
146 BI,
147 MI,
148 CLS,
149 KFS,
150> {
151 clock: RobotClock,
152 config: &'cfg CuConfig,
153 mission: &'cfg str,
154 subsystem: Subsystem,
155 instance_id: u32,
156 resources: Option<ResourceManager>,
157 #[cfg(feature = "std")]
158 thread_pools: Option<Vec<Option<Arc<ThreadPool>>>>,
159 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
160 copperlist_sink: CLS,
161 keyframe_sink: KFS,
162 output_requirements: OutputRequirements,
163}
164
165impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLS, KFS>
166 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
167{
168 pub fn new(
169 clock: RobotClock,
170 config: &'cfg CuConfig,
171 mission: &'cfg str,
172 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
173 copperlist_sink: CLS,
174 keyframe_sink: KFS,
175 output_requirements: OutputRequirements,
176 ) -> Self {
177 Self {
178 clock,
179 config,
180 mission,
181 subsystem: Subsystem::new(None, 0),
182 instance_id: 0,
183 resources: None,
184 #[cfg(feature = "std")]
185 thread_pools: None,
186 parts,
187 copperlist_sink,
188 keyframe_sink,
189 output_requirements,
190 }
191 }
192
193 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
194 self.subsystem = subsystem;
195 self
196 }
197
198 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
199 self.instance_id = instance_id;
200 self
201 }
202
203 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
204 self.resources = Some(resources);
205 self
206 }
207
208 pub fn try_with_resources_instantiator(
209 mut self,
210 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
211 ) -> CuResult<Self> {
212 self.resources = Some(resources_instantiator(self.config)?);
213 Ok(self)
214 }
215
216 #[cfg(feature = "std")]
220 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
221 self.thread_pools = Some(pools);
222 self
223 }
224
225 #[cfg(feature = "std")]
226 pub fn try_with_thread_pools_instantiator(
227 mut self,
228 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
229 ) -> CuResult<Self> {
230 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
231 Ok(self)
232 }
233}
234
235#[inline]
246pub fn perf_now(_clock: &RobotClock) -> CuTime {
247 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
248 {
249 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
250 return PERF_CLOCK.get_or_init(RobotClock::new).now();
251 }
252
253 #[allow(unreachable_code)]
254 _clock.now()
255}
256
257#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
258const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
259
260#[inline]
262pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
263 if rate_target_hz == 0 {
264 return Err(CuError::from(
265 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
266 ));
267 }
268
269 if rate_target_hz > MAX_RATE_TARGET_HZ {
270 return Err(CuError::from(format!(
271 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
272 )));
273 }
274
275 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
276}
277
278#[derive(Clone, Copy, Debug, PartialEq, Eq)]
285pub struct LoopRateLimiter {
286 period: CuDuration,
287 next_deadline: CuTime,
288}
289
290impl LoopRateLimiter {
291 #[inline]
292 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
293 let period = rate_target_period(rate_target_hz)?;
294 Ok(Self {
295 period,
296 next_deadline: clock.now() + period,
297 })
298 }
299
300 #[inline]
301 pub fn is_ready(&self, clock: &RobotClock) -> bool {
302 self.remaining(clock).is_none()
303 }
304
305 #[inline]
306 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
307 let now = clock.now();
308 if now < self.next_deadline {
309 Some(self.next_deadline - now)
310 } else {
311 None
312 }
313 }
314
315 #[inline]
316 pub fn wait_until_ready(&self, clock: &RobotClock) {
317 let deadline = self.next_deadline;
318 let Some(remaining) = self.remaining(clock) else {
319 return;
320 };
321
322 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
323 {
324 let spin_window = self.spin_window();
325 if remaining > spin_window {
326 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
327 }
328 while clock.now() < deadline {
329 core::hint::spin_loop();
330 }
331 }
332
333 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
334 {
335 let _ = deadline;
336 std::thread::sleep(std::time::Duration::from(remaining));
337 }
338
339 #[cfg(not(feature = "std"))]
340 {
341 let _ = remaining;
342 while clock.now() < deadline {
343 core::hint::spin_loop();
344 }
345 }
346 }
347
348 #[inline]
349 pub fn mark_tick(&mut self, clock: &RobotClock) {
350 self.advance_from(clock.now());
351 }
352
353 #[inline]
354 pub fn limit(&mut self, clock: &RobotClock) {
355 self.wait_until_ready(clock);
356 self.mark_tick(clock);
357 }
358
359 #[inline]
360 fn advance_from(&mut self, now: CuTime) {
361 let steps = if now < self.next_deadline {
362 1
363 } else {
364 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
365 };
366 self.next_deadline += steps * self.period;
367 }
368
369 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
370 #[inline]
371 fn spin_window(&self) -> CuDuration {
372 let _ = self.period;
373 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
374 }
375
376 #[cfg(test)]
377 #[inline]
378 fn next_deadline(&self) -> CuTime {
379 self.next_deadline
380 }
381}
382
383#[cfg(all(feature = "std", feature = "async-cl-io"))]
384#[doc(hidden)]
385pub trait AsyncCopperListPayload: Send {}
386
387#[cfg(all(feature = "std", feature = "async-cl-io"))]
388impl<T: Send> AsyncCopperListPayload for T {}
389
390#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
391#[doc(hidden)]
392pub trait AsyncCopperListPayload {}
393
394#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
395impl<T> AsyncCopperListPayload for T {}
396
397#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404#[doc(hidden)]
405pub enum ProcessStepOutcome {
406 Continue,
407 AbortCopperList,
408}
409
410#[doc(hidden)]
412pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
413
414#[cfg(feature = "remote-debug")]
415fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
416 cl: &CopperList<P>,
417) -> CuResult<Vec<u8>> {
418 bincode::encode_to_vec(cl, bincode::config::standard())
419 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
420}
421
422#[doc(hidden)]
428pub type SemanticRecordSink<T> = dyn WriteStream<T>;
429
430#[doc(hidden)]
432pub type CompletedCopperListSink<P> = SemanticRecordSink<CopperList<P>>;
433
434#[doc(hidden)]
436pub type CompletedKeyFrameSink = SemanticRecordSink<KeyFrame>;
437
438#[derive(Clone, Copy, Debug, Default)]
440#[doc(hidden)]
441pub struct NullWriteStream;
442
443impl<E: Encode> WriteStream<E> for NullWriteStream {
444 #[inline]
445 fn log(&mut self, _record: &E) -> CuResult<()> {
446 Ok(())
447 }
448}
449
450#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
456#[doc(hidden)]
457pub struct OutputRequirements {
458 completed_copperlists: bool,
459 keyframes: bool,
460}
461
462impl OutputRequirements {
463 #[inline]
464 pub const fn new(completed_copperlists: bool, keyframes: bool) -> Self {
465 Self {
466 completed_copperlists,
467 keyframes,
468 }
469 }
470
471 #[inline]
472 pub const fn completed_copperlists(self) -> bool {
473 self.completed_copperlists
474 }
475
476 #[inline]
477 pub const fn keyframes(self) -> bool {
478 self.keyframes
479 }
480
481 #[inline]
483 pub const fn union(self, other: Self) -> Self {
484 Self::new(
485 self.completed_copperlists || other.completed_copperlists,
486 self.keyframes || other.keyframes,
487 )
488 }
489}
490
491#[doc(hidden)]
493pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
494 inner: Option<CuListsManager<P, NBCL>>,
495 #[cfg(feature = "std")]
496 free_pool: Vec<CuSlotLease<P>>,
497 #[cfg(feature = "std")]
498 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
499 #[cfg(feature = "std")]
500 next_distributed_id: u64,
501 #[cfg(feature = "std")]
502 pub last_keyframe_bytes: u64,
503 sink: Option<Box<CompletedCopperListSink<P>>>,
504 #[cfg(feature = "remote-debug")]
506 last_completed_encoded: Option<Vec<u8>>,
507 pub last_encoded_bytes: u64,
509 pub last_handle_bytes: u64,
511}
512
513impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
514 #[doc(hidden)]
515 pub fn close_output(&mut self) -> CuResult<()> {
516 self.finish_pending()?;
517 if let Some(sink) = &mut self.sink {
518 sink.flush()?;
519 }
520 self.sink.take();
521 #[cfg(feature = "std")]
522 {
523 if let Some(sink) = &mut self.keyframe_sink {
524 sink.flush()?;
525 }
526 self.keyframe_sink.take();
527 }
528 Ok(())
529 }
530
531 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
532 where
533 P: CuListZeroedInit,
534 {
535 Self::new_with_keyframes(sink, None)
536 }
537
538 pub fn new_with_keyframes(
539 sink: Option<Box<CompletedCopperListSink<P>>>,
540 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
541 ) -> CuResult<Self>
542 where
543 P: CuListZeroedInit,
544 {
545 #[cfg(not(feature = "std"))]
546 let _ = keyframe_sink;
547 Ok(Self {
548 inner: (!P::DISTRIBUTED).then(CuListsManager::new),
549 #[cfg(feature = "std")]
550 free_pool: if P::DISTRIBUTED {
551 allocate_slots::<P>(NBCL)
552 } else {
553 Vec::new()
554 },
555 #[cfg(feature = "std")]
556 keyframe_sink,
557 #[cfg(feature = "std")]
558 next_distributed_id: 0,
559 #[cfg(feature = "std")]
560 last_keyframe_bytes: 0,
561 sink,
562 #[cfg(feature = "remote-debug")]
563 last_completed_encoded: None,
564 last_encoded_bytes: 0,
565 last_handle_bytes: 0,
566 })
567 }
568
569 pub fn next_cl_id(&self) -> u64 {
570 #[cfg(feature = "std")]
571 {
572 self.inner
573 .as_ref()
574 .map_or(self.next_distributed_id, CuListsManager::next_cl_id)
575 }
576 #[cfg(not(feature = "std"))]
577 {
578 self.inner.as_ref().expect("serial storage").next_cl_id()
579 }
580 }
581
582 #[cfg(feature = "std")]
583 pub fn note_admission(&mut self, next_id: u64) {
584 self.next_distributed_id = self.next_distributed_id.max(next_id);
585 }
586
587 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
590 if !self.inner.as_ref().is_none_or(CuListsManager::is_empty) {
591 return Err(CuError::from(
592 "Cannot reposition replay with active CopperLists",
593 ));
594 }
595 if let Some(inner) = &mut self.inner {
596 inner.set_next_replay_id(next_id);
597 }
598 #[cfg(feature = "std")]
599 {
600 self.next_distributed_id = next_id;
601 }
602 Ok(())
603 }
604
605 pub fn last_cl_id(&self) -> u64 {
606 self.next_cl_id().saturating_sub(1)
607 }
608
609 pub fn peek(&self) -> Option<&CopperList<P>> {
610 self.inner.as_ref().and_then(CuListsManager::peek)
611 }
612
613 #[cfg(feature = "remote-debug")]
614 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
615 self.last_completed_encoded.as_deref()
616 }
617
618 #[cfg(not(feature = "remote-debug"))]
619 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
620 None
621 }
622
623 #[cfg(feature = "remote-debug")]
624 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
625 self.last_completed_encoded = snapshot;
626 }
627
628 #[cfg(not(feature = "remote-debug"))]
629 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
630
631 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
632 where
633 P: CuListZeroedInit,
634 {
635 self.inner
636 .as_mut()
637 .expect("serial storage")
638 .create()
639 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
640 }
641
642 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
643 #[cfg(debug_assertions)]
644 self.debug_assert_end_of_processing_target(culistid);
645
646 let mut is_top = true;
647 let mut nb_done = 0;
648 self.last_encoded_bytes = 0;
649 self.last_handle_bytes = 0;
650 #[cfg(feature = "remote-debug")]
651 let last_completed_encoded = &mut self.last_completed_encoded;
652 for cl in self.inner.as_mut().expect("serial storage").iter_mut() {
653 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
654 cl.change_state(CopperListState::DoneProcessing);
655 #[cfg(feature = "remote-debug")]
656 {
657 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
658 }
659 }
660 if is_top && cl.get_state() == CopperListState::DoneProcessing {
661 if let Some(sink) = &mut self.sink {
662 cl.change_state(CopperListState::BeingSerialized);
663 sink.log(cl)?;
664 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
665 self.last_handle_bytes = take_last_completed_handle_bytes();
666 }
667 cl.change_state(CopperListState::Free);
668 nb_done += 1;
669 } else {
670 is_top = false;
671 }
672 }
673 for _ in 0..nb_done {
674 let _ = self.inner.as_mut().expect("serial storage").pop();
675 }
676 Ok(())
677 }
678
679 #[doc(hidden)]
681 pub fn discard_current(&mut self) {
682 if let Some(inner) = &mut self.inner
683 && inner.peek().is_some_and(|culist| {
684 matches!(
685 culist.get_state(),
686 CopperListState::Initialized | CopperListState::Processing
687 )
688 })
689 && let Some(culist) = inner.pop()
690 {
691 culist.change_state(CopperListState::Free);
692 }
693 }
694
695 pub fn finish_pending(&mut self) -> CuResult<()> {
696 Ok(())
697 }
698
699 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
700 if let Some(inner) = &self.inner {
701 return Ok(NBCL - inner.len());
702 }
703 #[cfg(feature = "std")]
704 {
705 Ok(self.free_pool.len())
706 }
707 #[cfg(not(feature = "std"))]
708 {
709 Ok(0)
710 }
711 }
712
713 #[inline]
714 pub const fn dropped_copperlists_total(&self) -> u64 {
715 0
716 }
717
718 #[cfg(feature = "std")]
719 pub fn submit_slot(
720 &mut self,
721 mut culist: CuSlotLease<P>,
722 ) -> CuResult<OwnedCopperListSubmission<P>> {
723 #[cfg(debug_assertions)]
724 debug_assert_processing_completion_state(culist.as_ref(), "sync slot completion");
725
726 self.note_admission(culist.id + 1);
727 culist.change_state(CopperListState::DoneProcessing);
728 self.last_keyframe_bytes = 0;
729 if let Some(sink) = &mut self.keyframe_sink
730 && let Some(keyframe) = culist.keyframe().filter(|keyframe| keyframe.is_active())
731 {
732 sink.log(keyframe)?;
733 self.last_keyframe_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
734 }
735 self.last_encoded_bytes = 0;
736 self.last_handle_bytes = 0;
737 if let Some(sink) = &mut self.sink {
738 culist.change_state(CopperListState::BeingSerialized);
739 sink.log(&culist)?;
740 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
741 self.last_handle_bytes = take_last_completed_handle_bytes();
742 }
743 culist.change_state(CopperListState::Free);
744 Ok(OwnedCopperListSubmission::Recycled(culist))
745 }
746
747 #[cfg(feature = "std")]
748 pub fn try_reclaim_slot(&mut self) -> CuResult<Option<CuSlotLease<P>>> {
749 Ok(None)
750 }
751
752 #[cfg(feature = "std")]
753 pub const fn has_pending_slots(&self) -> bool {
754 false
755 }
756
757 #[cfg(feature = "std")]
758 pub fn wait_reclaim_slot(&mut self) -> CuResult<CuSlotLease<P>> {
759 Err(CuError::from(
760 "Synchronous CopperList I/O cannot block waiting for slot completions",
761 ))
762 }
763
764 #[cfg(feature = "std")]
765 pub fn finish_pending_slots(&mut self) -> CuResult<Vec<CuSlotLease<P>>> {
766 Ok(Vec::new())
767 }
768
769 #[cfg(feature = "std")]
770 pub fn finish_pending_with(&mut self, _reclaim: impl FnMut(CuSlotLease<P>)) -> CuResult<()> {
771 Ok(())
772 }
773
774 #[cfg(debug_assertions)]
775 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
776 let mut matches = 0usize;
777 let mut state = None;
778 for cl in self.inner.as_ref().expect("serial storage").iter() {
779 if cl.id == culistid {
780 matches += 1;
781 state = Some(cl.get_state());
782 }
783 }
784
785 assert_eq!(
786 matches, 1,
787 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
788 );
789 assert_eq!(
790 state,
791 Some(CopperListState::Processing),
792 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
793 state
794 );
795 }
796}
797
798#[cfg(feature = "std")]
799impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
800 pub fn acquire_iteration_slot(&mut self) -> CuResult<CuSlotLease<P>> {
801 self.free_pool
802 .pop()
803 .ok_or_else(|| CuError::from("CopperList arena has no free slot"))
804 }
805
806 pub fn prepare_keyframes(&mut self, capacities: &[usize]) {
807 if !capacities.is_empty()
808 && let Some(slot) = self.free_pool.first_mut()
809 {
810 slot.prepare_keyframes(capacities);
811 }
812 }
813
814 pub fn recycle_slot(&mut self, slot: CuSlotLease<P>) {
815 self.free_pool.push(slot);
816 }
817
818 pub fn take_execution_slots(&mut self) -> Vec<CuSlotLease<P>> {
819 core::mem::take(&mut self.free_pool)
820 }
821
822 pub fn return_execution_slots(&mut self, slots: Vec<CuSlotLease<P>>) {
823 self.free_pool = slots;
824 }
825}
826
827#[cfg(feature = "std")]
829#[doc(hidden)]
830pub enum OwnedCopperListSubmission<P: CopperListTuple> {
831 Recycled(CuSlotLease<P>),
833 Pending,
835}
836
837#[cfg(all(feature = "std", feature = "async-cl-io"))]
838struct AsyncCopperListCompletion<P: CopperListTuple> {
839 culist: CuSlotLease<P>,
840 sink_result: CuResult<(u64, u64, u64)>,
841 #[cfg(feature = "remote-debug")]
842 completed_snapshot: CuResult<Vec<u8>>,
843}
844
845#[cfg(all(feature = "std", feature = "async-cl-io"))]
846struct AsyncOutputWorkerRunningGuard(Arc<AtomicBool>);
847
848#[cfg(all(feature = "std", feature = "async-cl-io"))]
849impl Drop for AsyncOutputWorkerRunningGuard {
850 fn drop(&mut self) {
851 self.0.store(false, Ordering::Release);
852 }
853}
854
855#[cfg(all(feature = "std", feature = "async-cl-io"))]
857#[doc(hidden)]
858pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
859 free_pool: Vec<CuSlotLease<P>>,
860 current: Option<CuSlotLease<P>>,
861 #[cfg(feature = "remote-debug")]
862 last_completed_encoded: Option<Vec<u8>>,
863 pending_count: usize,
864 next_cl_id: u64,
865 pending_producer: Option<Producer<CuSlotLease<P>>>,
866 completion_consumer: Option<Consumer<AsyncCopperListCompletion<P>>>,
867 worker_handle: Option<JoinHandle<()>>,
868 worker_thread: Option<Thread>,
869 worker_shutdown: Option<Arc<AtomicBool>>,
870 worker_running: Option<Arc<AtomicBool>>,
871 dropped_copperlists_total: u64,
872 pub last_encoded_bytes: u64,
874 pub last_handle_bytes: u64,
876 pub last_keyframe_bytes: u64,
877 distributed: bool,
878}
879
880#[cfg(all(feature = "std", feature = "async-cl-io"))]
881impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
882 #[doc(hidden)]
883 pub fn close_output(&mut self) -> CuResult<()> {
884 self.shutdown_worker()
885 }
886
887 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
888 where
889 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
890 {
891 Self::new_with_keyframes(sink, None)
892 }
893
894 pub fn new_with_keyframes(
895 sink: Option<Box<CompletedCopperListSink<P>>>,
896 keyframe_sink: Option<Box<CompletedKeyFrameSink>>,
897 ) -> CuResult<Self>
898 where
899 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
900 {
901 let free_pool = allocate_slots::<P>(NBCL);
902
903 if sink.is_some() && !P::DISTRIBUTED && NBCL < 2 {
904 return Err(CuError::from(
905 "async CopperList output requires at least two CopperList slots",
906 ));
907 }
908
909 let (
910 pending_producer,
911 completion_consumer,
912 worker_handle,
913 worker_thread,
914 worker_shutdown,
915 worker_running,
916 ) = if sink.is_some() || keyframe_sink.is_some() {
917 let mut sink = sink;
918 let mut keyframe_sink = keyframe_sink;
919 let handoff_capacity = NBCL.max(1);
920 let (pending_producer, mut pending_consumer) =
921 RingBuffer::<CuSlotLease<P>>::new(handoff_capacity);
922 let (mut completion_producer, completion_consumer) =
923 RingBuffer::<AsyncCopperListCompletion<P>>::new(handoff_capacity);
924 let worker_shutdown = Arc::new(AtomicBool::new(false));
925 let worker_running = Arc::new(AtomicBool::new(true));
926 let shutdown = worker_shutdown.clone();
927 let running = worker_running.clone();
928 let worker_handle = std::thread::Builder::new()
929 .name("cu-async-cl-io".to_string())
930 .spawn(move || {
931 let _running_guard = AsyncOutputWorkerRunningGuard(running);
932 loop {
933 let mut culist = match pending_consumer.pop() {
934 Ok(culist) => culist,
935 Err(PopError::Empty) => {
936 if shutdown.load(Ordering::Acquire) {
937 break;
938 }
939 std::thread::park();
940 continue;
941 }
942 };
943 #[cfg(feature = "remote-debug")]
944 let completed_snapshot = {
945 culist.change_state(CopperListState::DoneProcessing);
948 encode_completed_copperlist_snapshot(&culist)
949 };
950 culist.change_state(CopperListState::BeingSerialized);
951 let sink_result = (|| {
952 let mut keyframe_bytes = 0;
953 if let Some(keyframe_sink) = &mut keyframe_sink
954 && let Some(keyframe) =
955 culist.keyframe().filter(|keyframe| keyframe.is_active())
956 {
957 keyframe_sink.log(keyframe)?;
958 keyframe_bytes = keyframe_sink.last_log_bytes().unwrap_or(0) as u64;
959 }
960 if let Some(sink) = &mut sink {
961 sink.log(&culist)?;
962 Ok((
963 sink.last_log_bytes().unwrap_or(0) as u64,
964 take_last_completed_handle_bytes(),
965 keyframe_bytes,
966 ))
967 } else {
968 Ok((0, 0, keyframe_bytes))
969 }
970 })();
971 let should_stop = sink_result.is_err();
972 #[cfg(feature = "remote-debug")]
973 let should_stop = should_stop || completed_snapshot.is_err();
974 let mut completion = AsyncCopperListCompletion {
975 culist,
976 sink_result,
977 #[cfg(feature = "remote-debug")]
978 completed_snapshot,
979 };
980 loop {
981 match completion_producer.push(completion) {
982 Ok(()) => break,
983 Err(PushError::Full(returned)) => {
984 completion = returned;
985 std::thread::yield_now();
986 }
987 }
988 }
989 if should_stop {
990 break;
991 }
992 }
993 })
994 .map_err(|e| {
995 CuError::from("Failed to spawn async CopperList serializer thread")
996 .add_cause(e.to_string().as_str())
997 })?;
998 let worker_thread = worker_handle.thread().clone();
999 (
1000 Some(pending_producer),
1001 Some(completion_consumer),
1002 Some(worker_handle),
1003 Some(worker_thread),
1004 Some(worker_shutdown),
1005 Some(worker_running),
1006 )
1007 } else {
1008 (None, None, None, None, None, None)
1009 };
1010
1011 Ok(Self {
1012 free_pool,
1013 current: None,
1014 #[cfg(feature = "remote-debug")]
1015 last_completed_encoded: None,
1016 pending_count: 0,
1017 next_cl_id: 0,
1018 pending_producer,
1019 completion_consumer,
1020 worker_handle,
1021 worker_thread,
1022 worker_shutdown,
1023 worker_running,
1024 dropped_copperlists_total: 0,
1025 last_encoded_bytes: 0,
1026 last_handle_bytes: 0,
1027 last_keyframe_bytes: 0,
1028 distributed: P::DISTRIBUTED,
1029 })
1030 }
1031
1032 pub fn acquire_iteration_slot(&mut self) -> CuResult<CuSlotLease<P>> {
1033 self.reclaim_completed()?;
1034 match self.free_pool.pop() {
1035 Some(slot) => Ok(slot),
1036 None => self.wait_reclaim_slot(),
1037 }
1038 }
1039
1040 pub fn prepare_keyframes(&mut self, capacities: &[usize]) {
1041 if !capacities.is_empty()
1042 && let Some(slot) = self.free_pool.first_mut()
1043 {
1044 slot.prepare_keyframes(capacities);
1045 }
1046 }
1047
1048 pub fn recycle_slot(&mut self, slot: CuSlotLease<P>) {
1049 self.free_pool.push(slot);
1050 }
1051
1052 pub fn take_execution_slots(&mut self) -> Vec<CuSlotLease<P>> {
1053 core::mem::take(&mut self.free_pool)
1054 }
1055
1056 pub fn return_execution_slots(&mut self, slots: Vec<CuSlotLease<P>>) {
1057 self.free_pool = slots;
1058 }
1059
1060 pub fn note_admission(&mut self, next_id: u64) {
1061 self.next_cl_id = self.next_cl_id.max(next_id);
1062 }
1063
1064 pub fn next_cl_id(&self) -> u64 {
1065 self.next_cl_id
1066 }
1067
1068 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
1071 self.finish_pending()?;
1072 self.next_cl_id = next_id;
1073 Ok(())
1074 }
1075
1076 pub fn last_cl_id(&self) -> u64 {
1077 self.next_cl_id.saturating_sub(1)
1078 }
1079
1080 pub fn peek(&self) -> Option<&CopperList<P>> {
1081 self.current.as_deref()
1082 }
1083
1084 #[cfg(feature = "remote-debug")]
1085 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
1086 self.last_completed_encoded.as_deref()
1087 }
1088
1089 #[cfg(not(feature = "remote-debug"))]
1090 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
1091 None
1092 }
1093
1094 #[cfg(feature = "remote-debug")]
1095 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
1096 self.last_completed_encoded = snapshot;
1097 }
1098
1099 #[cfg(not(feature = "remote-debug"))]
1100 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
1101
1102 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
1103 where
1104 P: CuListZeroedInit,
1105 {
1106 if self.current.is_some() {
1107 return Err(CuError::from(
1108 "Attempted to create a CopperList while another one is still active",
1109 ));
1110 }
1111
1112 self.reclaim_completed()?;
1113
1114 let culist = self.free_pool.pop().ok_or_else(|| {
1115 CuError::from("CopperList output handoff exhausted the slot reserved for execution")
1116 })?;
1117 self.current = Some(culist);
1118
1119 let current = self
1120 .current
1121 .as_mut()
1122 .expect("current CopperList is missing");
1123 current.reset(self.next_cl_id);
1124 self.next_cl_id += 1;
1125 Ok(current.as_mut())
1126 }
1127
1128 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1129 self.reclaim_completed()?;
1130
1131 let mut culist = self.current.take().ok_or_else(|| {
1132 CuError::from("Attempted to finish processing without an active CopperList")
1133 })?;
1134
1135 if culist.id != culistid {
1136 return Err(CuError::from(format!(
1137 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
1138 culist.id
1139 )));
1140 }
1141 #[cfg(debug_assertions)]
1142 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
1143
1144 culist.change_state(CopperListState::DoneProcessing);
1145 self.last_encoded_bytes = 0;
1146 self.last_handle_bytes = 0;
1147
1148 match self.try_submit(culist)? {
1149 OwnedCopperListSubmission::Recycled(culist) => self.free_pool.push(culist),
1150 OwnedCopperListSubmission::Pending => {}
1151 }
1152
1153 Ok(())
1154 }
1155
1156 #[doc(hidden)]
1158 pub fn discard_current(&mut self) {
1159 if let Some(mut culist) = self.current.take() {
1160 culist.change_state(CopperListState::Free);
1161 self.free_pool.push(culist);
1162 }
1163 }
1164
1165 pub fn finish_pending(&mut self) -> CuResult<()> {
1166 if self.current.is_some() {
1167 return Err(CuError::from(
1168 "Cannot flush CopperList I/O while a CopperList is still active",
1169 ));
1170 }
1171
1172 while self.pending_count > 0 {
1173 self.wait_for_completion()?;
1174 }
1175 Ok(())
1176 }
1177
1178 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
1179 self.reclaim_completed()?;
1180 Ok(self.free_pool.len())
1181 }
1182
1183 #[inline]
1184 pub const fn dropped_copperlists_total(&self) -> u64 {
1185 self.dropped_copperlists_total
1186 }
1187
1188 pub fn submit_slot(
1189 &mut self,
1190 mut culist: CuSlotLease<P>,
1191 ) -> CuResult<OwnedCopperListSubmission<P>> {
1192 #[cfg(debug_assertions)]
1193 debug_assert_processing_completion_state(culist.as_ref(), "async slot completion");
1194 self.next_cl_id = self.next_cl_id.max(culist.id + 1);
1195 culist.change_state(CopperListState::DoneProcessing);
1196 self.last_encoded_bytes = 0;
1197 self.last_handle_bytes = 0;
1198
1199 self.try_submit(culist)
1200 }
1201
1202 fn try_submit(&mut self, mut culist: CuSlotLease<P>) -> CuResult<OwnedCopperListSubmission<P>> {
1203 let Some(pending_producer) = self.pending_producer.as_mut() else {
1204 culist.change_state(CopperListState::Free);
1205 return Ok(OwnedCopperListSubmission::Recycled(culist));
1206 };
1207
1208 if !self.distributed && self.pending_count >= NBCL - 1 {
1212 return Ok(self.drop_copperlist(culist));
1213 }
1214
1215 culist.change_state(CopperListState::QueuedForSerialization);
1216 match pending_producer.push(culist) {
1217 Ok(()) => {
1218 self.pending_count += 1;
1219 if let Some(worker_thread) = self.worker_thread.as_ref() {
1220 worker_thread.unpark();
1221 }
1222 Ok(OwnedCopperListSubmission::Pending)
1223 }
1224 Err(PushError::Full(culist)) => Ok(self.drop_copperlist(culist)),
1225 }
1226 }
1227
1228 fn drop_copperlist(&mut self, mut culist: CuSlotLease<P>) -> OwnedCopperListSubmission<P> {
1229 self.dropped_copperlists_total = self.dropped_copperlists_total.saturating_add(1);
1230 culist.change_state(CopperListState::Free);
1231 OwnedCopperListSubmission::Recycled(culist)
1232 }
1233
1234 pub fn try_reclaim_slot(&mut self) -> CuResult<Option<CuSlotLease<P>>> {
1235 let pop_result = {
1236 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1237 return Ok(None);
1238 };
1239 completion_consumer.pop()
1240 };
1241 match pop_result {
1242 Ok(completion) => self.handle_completion(completion).map(Some),
1243 Err(PopError::Empty) => {
1244 if self.pending_count > 0
1245 && self
1246 .worker_running
1247 .as_ref()
1248 .is_some_and(|running| !running.load(Ordering::Acquire))
1249 {
1250 Err(CuError::from(
1251 "Async CopperList output worker stopped unexpectedly",
1252 ))
1253 } else {
1254 Ok(None)
1255 }
1256 }
1257 }
1258 }
1259
1260 pub fn has_pending_slots(&self) -> bool {
1261 self.pending_count > 0
1262 }
1263
1264 pub fn wait_reclaim_slot(&mut self) -> CuResult<CuSlotLease<P>> {
1265 if self.completion_consumer.is_none() {
1266 return Err(CuError::from(
1267 "No async CopperList output worker is active to return a free slot",
1268 ));
1269 }
1270 loop {
1271 if let Some(culist) = self.try_reclaim_slot()? {
1272 return Ok(culist);
1273 }
1274 std::thread::yield_now();
1275 }
1276 }
1277
1278 pub fn finish_pending_slots(&mut self) -> CuResult<Vec<CuSlotLease<P>>> {
1279 let mut reclaimed = Vec::with_capacity(self.pending_count);
1280 if self.current.is_some() {
1281 return Err(CuError::from(
1282 "Cannot flush CopperList I/O while a CopperList is still active",
1283 ));
1284 }
1285 while self.pending_count > 0 {
1286 reclaimed.push(self.wait_reclaim_slot()?);
1287 }
1288 Ok(reclaimed)
1289 }
1290
1291 pub fn finish_pending_with(&mut self, mut reclaim: impl FnMut(CuSlotLease<P>)) -> CuResult<()> {
1292 if self.current.is_some() {
1293 return Err(CuError::from(
1294 "Cannot flush CopperList I/O while a CopperList is still active",
1295 ));
1296 }
1297 while self.pending_count > 0 {
1298 reclaim(self.wait_reclaim_slot()?);
1299 }
1300 Ok(())
1301 }
1302
1303 fn reclaim_completed(&mut self) -> CuResult<()> {
1304 loop {
1305 let Some(culist) = self.try_reclaim_slot()? else {
1306 break;
1307 };
1308 self.free_pool.push(culist);
1309 }
1310 Ok(())
1311 }
1312
1313 fn wait_for_completion(&mut self) -> CuResult<()> {
1314 let culist = self.wait_reclaim_slot()?;
1315 self.free_pool.push(culist);
1316 Ok(())
1317 }
1318
1319 fn handle_completion(
1320 &mut self,
1321 mut completion: AsyncCopperListCompletion<P>,
1322 ) -> CuResult<CuSlotLease<P>> {
1323 self.pending_count = self.pending_count.saturating_sub(1);
1324 if let Ok((encoded_bytes, handle_bytes, keyframe_bytes)) = completion.sink_result.as_ref() {
1325 self.last_encoded_bytes = *encoded_bytes;
1326 self.last_handle_bytes = *handle_bytes;
1327 self.last_keyframe_bytes = *keyframe_bytes;
1328 }
1329 completion.culist.change_state(CopperListState::Free);
1330 completion.sink_result?;
1331 #[cfg(feature = "remote-debug")]
1332 {
1333 self.last_completed_encoded = Some(completion.completed_snapshot?);
1334 }
1335 Ok(completion.culist)
1336 }
1337
1338 fn shutdown_worker(&mut self) -> CuResult<()> {
1339 self.finish_pending()?;
1340 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1341 shutdown.store(true, Ordering::Release);
1342 }
1343 if let Some(worker_thread) = self.worker_thread.as_ref() {
1344 worker_thread.unpark();
1345 }
1346 if let Some(worker_handle) = self.worker_handle.take() {
1347 worker_handle.join().map_err(|_| {
1348 CuError::from("Async CopperList output worker panicked while joining")
1349 })?;
1350 }
1351 self.pending_producer.take();
1352 self.worker_thread.take();
1353 self.worker_shutdown.take();
1354 self.worker_running.take();
1355 Ok(())
1356 }
1357}
1358
1359#[cfg(all(feature = "std", feature = "async-cl-io"))]
1360impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
1361 fn drop(&mut self) {
1362 let _ = self.shutdown_worker();
1363 }
1364}
1365
1366#[cfg(all(feature = "std", debug_assertions))]
1367fn debug_assert_processing_completion_state<P: CopperListTuple>(
1368 culist: &CopperList<P>,
1369 context: &str,
1370) {
1371 assert_eq!(
1372 culist.get_state(),
1373 CopperListState::Processing,
1374 "{context} expected CopperList #{} to be Processing, found {}",
1375 culist.id,
1376 culist.get_state()
1377 );
1378}
1379
1380#[cfg(all(feature = "std", feature = "async-cl-io"))]
1381#[doc(hidden)]
1382pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
1383
1384#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1385#[doc(hidden)]
1386pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
1387
1388#[cfg(all(feature = "std", feature = "async-cl-io"))]
1389struct AsyncKeyFrameCompletion {
1390 keyframe: Box<KeyFrame>,
1391 sink_result: CuResult<u64>,
1392}
1393
1394pub struct KeyFramesManager {
1396 distributed: bool,
1397 #[doc(hidden)]
1399 pub capture_capacities: Vec<usize>,
1400 inner: Option<KeyFrame>,
1402
1403 forced_timestamp: Option<CuTime>,
1405
1406 locked: bool,
1408
1409 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1411 sink: Option<Box<CompletedKeyFrameSink>>,
1412
1413 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1417 #[allow(clippy::vec_box)]
1418 spares: Vec<Box<KeyFrame>>,
1419 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1420 pending_count: usize,
1421 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1422 pending_producer: Option<Producer<Box<KeyFrame>>>,
1423 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1424 completion_consumer: Option<Consumer<AsyncKeyFrameCompletion>>,
1425 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1426 worker_handle: Option<JoinHandle<()>>,
1427 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1428 worker_thread: Option<Thread>,
1429 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1430 worker_shutdown: Option<Arc<AtomicBool>>,
1431 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1432 worker_running: Option<Arc<AtomicBool>>,
1433 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1434 capture_this_copperlist: Option<u64>,
1435 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1436 dropped_keyframes_total: u64,
1437
1438 keyframe_interval: u32,
1440
1441 pub last_encoded_bytes: u64,
1443
1444 capture_size_hint: usize,
1446}
1447
1448const MIN_KEYFRAME_CAPTURE_CAPACITY: usize = 4 * 1024;
1449#[cfg(all(feature = "std", feature = "async-cl-io"))]
1450const ASYNC_KEYFRAME_HANDOFF_CAPACITY: usize = 2;
1451
1452struct PreallocatedVecWriter<'a>(&'a mut Vec<u8>);
1454
1455impl Writer for PreallocatedVecWriter<'_> {
1456 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
1457 if bytes.len() > self.0.capacity().saturating_sub(self.0.len()) {
1458 return Err(EncodeError::UnexpectedEnd);
1459 }
1460 self.0.extend_from_slice(bytes);
1462 Ok(())
1463 }
1464}
1465
1466impl KeyFramesManager {
1467 fn distributed(enabled: bool, keyframe_interval: u32) -> CuResult<Self> {
1468 if enabled && keyframe_interval == 0 {
1469 return Err(CuError::from("Keyframe interval cannot be zero"));
1470 }
1471 let mut manager = Self::new(None, keyframe_interval)?;
1472 manager.distributed = enabled;
1473 Ok(manager)
1474 }
1475
1476 #[doc(hidden)]
1477 pub fn capture_interval(&self) -> u32 {
1478 self.keyframe_interval
1479 }
1480
1481 #[doc(hidden)]
1482 pub fn new(sink: Option<Box<CompletedKeyFrameSink>>, keyframe_interval: u32) -> CuResult<Self> {
1483 if sink.is_some() && keyframe_interval == 0 {
1484 return Err(CuError::from(
1485 "Keyframe interval cannot be zero when a downstream consumer requires keyframes",
1486 ));
1487 }
1488
1489 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1490 {
1491 let enabled = sink.is_some();
1492 let (
1493 pending_producer,
1494 completion_consumer,
1495 worker_handle,
1496 worker_thread,
1497 worker_shutdown,
1498 worker_running,
1499 ) = if let Some(mut sink) = sink {
1500 let (pending_producer, mut pending_consumer) =
1501 RingBuffer::<Box<KeyFrame>>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1502 let (mut completion_producer, completion_consumer) =
1503 RingBuffer::<AsyncKeyFrameCompletion>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1504 let worker_shutdown = Arc::new(AtomicBool::new(false));
1505 let worker_running = Arc::new(AtomicBool::new(true));
1506 let shutdown = worker_shutdown.clone();
1507 let running = worker_running.clone();
1508 let worker_handle = std::thread::Builder::new()
1509 .name("cu-async-kf-io".to_string())
1510 .spawn(move || {
1511 let _running_guard = AsyncOutputWorkerRunningGuard(running);
1512 loop {
1513 let keyframe = match pending_consumer.pop() {
1514 Ok(keyframe) => keyframe,
1515 Err(PopError::Empty) => {
1516 if shutdown.load(Ordering::Acquire) {
1517 break;
1518 }
1519 std::thread::park();
1520 continue;
1521 }
1522 };
1523 let sink_result = sink
1524 .log(keyframe.as_ref())
1525 .map(|_| sink.last_log_bytes().unwrap_or(0) as u64);
1526 let should_stop = sink_result.is_err();
1527 let mut completion = AsyncKeyFrameCompletion {
1528 keyframe,
1529 sink_result,
1530 };
1531 loop {
1532 match completion_producer.push(completion) {
1533 Ok(()) => break,
1534 Err(PushError::Full(returned)) => {
1535 completion = returned;
1536 std::thread::yield_now();
1537 }
1538 }
1539 }
1540 if should_stop {
1541 break;
1542 }
1543 }
1544 })
1545 .map_err(|error| {
1546 CuError::from("Failed to spawn async keyframe output thread")
1547 .add_cause(error.to_string().as_str())
1548 })?;
1549 let worker_thread = worker_handle.thread().clone();
1550 (
1551 Some(pending_producer),
1552 Some(completion_consumer),
1553 Some(worker_handle),
1554 Some(worker_thread),
1555 Some(worker_shutdown),
1556 Some(worker_running),
1557 )
1558 } else {
1559 (None, None, None, None, None, None)
1560 };
1561
1562 let spares = if enabled {
1563 let mut spares = Vec::with_capacity(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1564 for _ in 0..ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1565 spares.push(Box::new(KeyFrame::new()));
1566 }
1567 spares
1568 } else {
1569 Vec::new()
1570 };
1571 Ok(Self {
1572 distributed: false,
1573 capture_capacities: Vec::new(),
1574 inner: enabled.then(KeyFrame::new),
1575 forced_timestamp: None,
1576 locked: false,
1577 spares,
1578 pending_count: 0,
1579 pending_producer,
1580 completion_consumer,
1581 worker_handle,
1582 worker_thread,
1583 worker_shutdown,
1584 worker_running,
1585 capture_this_copperlist: None,
1586 dropped_keyframes_total: 0,
1587 keyframe_interval,
1588 last_encoded_bytes: 0,
1589 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1590 })
1591 }
1592
1593 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1594 {
1595 let enabled = sink.is_some();
1596 Ok(Self {
1597 distributed: false,
1598 capture_capacities: Vec::new(),
1599 inner: enabled.then(KeyFrame::new),
1600 forced_timestamp: None,
1601 locked: false,
1602 sink,
1603 keyframe_interval,
1604 last_encoded_bytes: 0,
1605 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1606 })
1607 }
1608 }
1609
1610 fn is_keyframe_due(&self, culistid: u64) -> bool {
1611 (self.inner.is_some() || self.distributed)
1612 && culistid.is_multiple_of(self.keyframe_interval as u64)
1613 }
1614
1615 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1616 fn is_capturing(&self, culistid: u64) -> bool {
1617 self.capture_this_copperlist == Some(culistid)
1618 }
1619
1620 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1621 fn is_capturing(&self, culistid: u64) -> bool {
1622 self.is_keyframe_due(culistid)
1623 }
1624
1625 #[inline]
1626 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1627 self.is_keyframe_due(culistid)
1628 }
1629
1630 #[doc(hidden)]
1632 pub fn begin_capture_preallocation(&mut self) {
1633 self.capture_size_hint = KEYFRAME_PAYLOAD_HEADER.len();
1634 self.capture_capacities.clear();
1635 }
1636
1637 #[doc(hidden)]
1639 pub fn include_capture_capacity(&mut self, item: &impl Freezable) -> CuResult<()> {
1640 if self.inner.is_none() && !self.distributed {
1641 return Ok(());
1642 }
1643 let mut sizer = EncoderImpl::new(SizeWriter::default(), bincode::config::standard());
1644 BincodeAdapter(item)
1645 .encode(&mut sizer)
1646 .map_err(|_| CuError::from("Failed to size component keyframe state"))?;
1647 let payload_bytes = sizer.into_writer().bytes_written as usize;
1648 if self.distributed {
1649 self.capture_capacities.push(
1650 (KEYFRAME_FRAME_HEADER_LEN + payload_bytes)
1651 .max(DISTRIBUTED_REGION_ALIGN)
1652 .checked_next_power_of_two()
1653 .ok_or_else(|| CuError::from("Keyframe region size overflow"))?,
1654 );
1655 }
1656 self.capture_size_hint = self
1657 .capture_size_hint
1658 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1659 .and_then(|size| size.checked_add(payload_bytes))
1660 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1661 Ok(())
1662 }
1663
1664 #[doc(hidden)]
1666 pub fn finish_capture_preallocation(&mut self) -> CuResult<()> {
1667 if self.distributed {
1668 return Ok(());
1669 }
1670 if self.inner.is_none() {
1671 return Ok(());
1672 }
1673 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1674 self.reclaim_completed()?;
1675 let requested = self
1676 .capture_size_hint
1677 .max(MIN_KEYFRAME_CAPTURE_CAPACITY)
1678 .checked_next_power_of_two()
1679 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1680 reserve_keyframe_capacity(self.inner.as_mut().unwrap(), requested)?;
1681 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1682 for spare in &mut self.spares {
1683 reserve_keyframe_capacity(spare, requested)?;
1684 }
1685 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1686 if self.spares.len() != ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1687 return Err(CuError::from(
1688 "Keyframe output worker did not return every capture buffer before preallocation",
1689 ));
1690 }
1691 Ok(())
1692 }
1693
1694 #[doc(hidden)]
1697 pub fn try_reset(&mut self, culistid: u64, clock: &RobotClock) -> CuResult<()> {
1698 if self.is_keyframe_due(culistid) {
1699 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1700 {
1701 self.reclaim_completed()?;
1702 if self.spares.is_empty() {
1703 self.capture_this_copperlist = None;
1704 self.dropped_keyframes_total = self.dropped_keyframes_total.saturating_add(1);
1705 self.forced_timestamp = None;
1706 self.locked = false;
1707 return Ok(());
1708 }
1709 self.capture_this_copperlist = Some(culistid);
1710 }
1711 let inner = self.inner.as_mut().unwrap();
1713 if self.locked && inner.culistid == culistid {
1714 return Ok(());
1715 }
1716 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1717 inner.reset(culistid, ts);
1718 self.locked = false;
1719 }
1720 Ok(())
1721 }
1722
1723 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1728 let _ = self.try_reset(culistid, clock);
1729 }
1730
1731 #[cfg(feature = "std")]
1733 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1734 self.forced_timestamp = Some(ts);
1735 }
1736
1737 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1738 if self.is_capturing(culistid) {
1739 if self.locked {
1740 return Ok(0);
1742 }
1743 let inner = self.inner.as_mut().unwrap();
1744 if inner.culistid != culistid {
1745 return Err(CuError::from(format!(
1746 "Freezing task for culistid {} but current keyframe is {}",
1747 culistid, inner.culistid
1748 )));
1749 }
1750 let encoded = inner
1751 .add_frozen_task(task)
1752 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))?;
1753 Ok(encoded)
1754 } else {
1755 Ok(0)
1756 }
1757 }
1758
1759 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1761 self.freeze_task(culistid, item)
1762 }
1763
1764 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1765 if self.is_capturing(culistid) {
1766 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1767 {
1768 let sink = self.sink.as_mut().unwrap();
1769 sink.log(self.inner.as_ref().unwrap())?;
1770 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
1771 }
1772 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1773 {
1774 self.last_encoded_bytes = 0;
1775 let mut completed = self.spares.pop().ok_or_else(|| {
1776 CuError::from("Missing spare keyframe buffer at output handoff")
1777 })?;
1778 core::mem::swap(self.inner.as_mut().unwrap(), completed.as_mut());
1779 let producer = self.pending_producer.as_mut().ok_or_else(|| {
1780 CuError::from("Missing keyframe output producer for active capture")
1781 })?;
1782 match producer.push(completed) {
1783 Ok(()) => {
1784 self.pending_count += 1;
1785 if let Some(worker_thread) = self.worker_thread.as_ref() {
1786 worker_thread.unpark();
1787 }
1788 }
1789 Err(PushError::Full(spare)) => {
1790 self.spares.push(spare);
1791 self.dropped_keyframes_total =
1792 self.dropped_keyframes_total.saturating_add(1);
1793 }
1794 }
1795 self.capture_this_copperlist = None;
1796 }
1797 self.locked = false;
1799 Ok(())
1800 } else {
1801 self.last_encoded_bytes = 0;
1803 Ok(())
1804 }
1805 }
1806
1807 #[cfg(feature = "std")]
1809 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1810 if let Some(inner) = self.inner.as_mut() {
1811 *inner = keyframe.clone();
1812 self.forced_timestamp = Some(keyframe.timestamp);
1813 self.locked = true;
1814 }
1815 }
1816
1817 #[inline]
1818 #[doc(hidden)]
1819 pub const fn dropped_keyframes_total(&self) -> u64 {
1820 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1821 {
1822 self.dropped_keyframes_total
1823 }
1824 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1825 {
1826 0
1827 }
1828 }
1829
1830 #[doc(hidden)]
1831 pub fn close_output(&mut self) -> CuResult<()> {
1832 self.finish_pending()?;
1833 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1834 self.shutdown_worker()?;
1835 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1836 {
1837 if let Some(sink) = &mut self.sink {
1838 sink.flush()?;
1839 }
1840 self.sink.take();
1841 }
1842 Ok(())
1843 }
1844
1845 #[doc(hidden)]
1846 pub fn finish_pending(&mut self) -> CuResult<()> {
1847 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1848 while self.pending_count > 0 {
1849 self.wait_for_completion()?;
1850 }
1851 Ok(())
1852 }
1853
1854 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1855 fn reclaim_completed(&mut self) -> CuResult<()> {
1856 let pop_result = {
1857 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1858 return Ok(());
1859 };
1860 completion_consumer.pop()
1861 };
1862 match pop_result {
1863 Ok(completion) => self.handle_completion(completion),
1864 Err(PopError::Empty) => {
1865 if self.pending_count > 0
1866 && self
1867 .worker_running
1868 .as_ref()
1869 .is_some_and(|running| !running.load(Ordering::Acquire))
1870 {
1871 Err(CuError::from(
1872 "Async keyframe output worker stopped unexpectedly",
1873 ))
1874 } else {
1875 Ok(())
1876 }
1877 }
1878 }
1879 }
1880
1881 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1882 fn wait_for_completion(&mut self) -> CuResult<()> {
1883 loop {
1884 let pending_before = self.pending_count;
1885 self.reclaim_completed()?;
1886 if self.pending_count < pending_before {
1887 return Ok(());
1888 }
1889 std::thread::yield_now();
1890 }
1891 }
1892
1893 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1894 fn handle_completion(&mut self, completion: AsyncKeyFrameCompletion) -> CuResult<()> {
1895 self.pending_count = self.pending_count.saturating_sub(1);
1896 if let Ok(encoded_bytes) = completion.sink_result.as_ref() {
1897 self.last_encoded_bytes = *encoded_bytes;
1898 }
1899 self.spares.push(completion.keyframe);
1900 completion.sink_result.map(|_| ())
1901 }
1902
1903 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1904 fn shutdown_worker(&mut self) -> CuResult<()> {
1905 self.finish_pending()?;
1906 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1907 shutdown.store(true, Ordering::Release);
1908 }
1909 if let Some(worker_thread) = self.worker_thread.as_ref() {
1910 worker_thread.unpark();
1911 }
1912 if let Some(worker_handle) = self.worker_handle.take() {
1913 worker_handle.join().map_err(|_| {
1914 CuError::from("Async keyframe output worker panicked while joining")
1915 })?;
1916 }
1917 self.pending_producer.take();
1918 self.worker_thread.take();
1919 self.worker_shutdown.take();
1920 self.worker_running.take();
1921 Ok(())
1922 }
1923}
1924
1925fn reserve_keyframe_capacity(keyframe: &mut KeyFrame, requested: usize) -> CuResult<()> {
1926 if keyframe.serialized_tasks.capacity() < requested {
1927 let additional = requested.saturating_sub(keyframe.serialized_tasks.len());
1928 keyframe
1929 .serialized_tasks
1930 .try_reserve_exact(additional)
1931 .map_err(|error| {
1932 CuError::from("Failed to preallocate keyframe capture buffer")
1933 .add_cause(&error.to_string())
1934 })?;
1935 }
1936 Ok(())
1937}
1938
1939#[cfg(all(feature = "std", feature = "async-cl-io"))]
1940impl Drop for KeyFramesManager {
1941 fn drop(&mut self) {
1942 let _ = self.shutdown_worker();
1943 }
1944}
1945
1946pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1950 clock: RobotClock,
1952
1953 subsystem_code: u16,
1955
1956 #[doc(hidden)]
1958 pub instance_id: u32,
1959
1960 #[doc(hidden)]
1962 pub tasks: CT,
1963
1964 #[doc(hidden)]
1966 pub bridges: CB,
1967
1968 #[doc(hidden)]
1970 pub resources: ResourceManager,
1971
1972 #[cfg(feature = "std")]
1975 #[doc(hidden)]
1976 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1977
1978 #[doc(hidden)]
1980 pub monitor: M,
1981
1982 #[cfg(feature = "std")]
1988 #[doc(hidden)]
1989 pub execution_probe: ExecutionProbeHandle,
1990 #[cfg(not(feature = "std"))]
1991 #[doc(hidden)]
1992 pub execution_probe: RuntimeExecutionProbe,
1993
1994 #[doc(hidden)]
1996 pub copperlists_manager: CopperListsManager<P, NBCL>,
1997
1998 #[doc(hidden)]
2000 pub keyframes_manager: KeyFramesManager,
2001
2002 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2004 #[doc(hidden)]
2005 pub parallel_rt: ParallelRt<NBCL>,
2006
2007 #[doc(hidden)]
2009 pub runtime_config: RuntimeConfig,
2010}
2011
2012impl<
2014 CT,
2015 CB,
2016 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
2017 M: CuMonitor,
2018 const NBCL: usize,
2019> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
2020{
2021 fn get_clock(&self) -> RobotClock {
2022 self.clock.clone()
2023 }
2024}
2025
2026impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
2027 #[inline]
2029 pub fn clock(&self) -> RobotClock {
2030 self.clock.clone()
2031 }
2032
2033 #[doc(hidden)]
2035 #[inline]
2036 pub fn clock_ref(&self) -> &RobotClock {
2037 &self.clock
2038 }
2039
2040 #[inline]
2042 pub fn subsystem_code(&self) -> u16 {
2043 self.subsystem_code
2044 }
2045
2046 #[inline]
2048 pub fn instance_id(&self) -> u32 {
2049 self.instance_id
2050 }
2051}
2052
2053#[cfg(feature = "std")]
2054impl<
2055 'cfg,
2056 CT,
2057 CB,
2058 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2059 M: CuMonitor,
2060 const NBCL: usize,
2061 TI,
2062 BI,
2063 MI,
2064 CLS,
2065 KFS,
2066> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
2067where
2068 TI: for<'c> Fn(
2069 Vec<Option<&'c ComponentConfig>>,
2070 &mut ResourceManager,
2071 &[Option<Arc<ThreadPool>>],
2072 ) -> CuResult<CT>,
2073 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
2074 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
2075 CLS: WriteStream<CopperList<P>> + 'static,
2076 KFS: WriteStream<KeyFrame> + 'static,
2077{
2078 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
2079 let Self {
2080 clock,
2081 config,
2082 mission,
2083 subsystem,
2084 instance_id,
2085 resources,
2086 thread_pools,
2087 parts,
2088 copperlist_sink,
2089 keyframe_sink,
2090 output_requirements,
2091 } = self;
2092 let mut resources =
2093 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
2094 let thread_pools = thread_pools.unwrap_or_default();
2095
2096 let graph = config.get_graph(Some(mission))?;
2097 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
2098 .get_all_nodes()
2099 .iter()
2100 .map(|(_, node)| node.get_instance_config())
2101 .collect();
2102
2103 let tasks =
2104 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
2105
2106 #[cfg(feature = "std")]
2107 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
2108 #[cfg(not(feature = "std"))]
2109 let execution_probe = RuntimeExecutionProbe::default();
2110 let monitor_metadata = CuMonitoringMetadata::new(
2111 CompactString::from(mission),
2112 parts.monitored_components,
2113 parts.culist_component_mapping,
2114 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
2115 build_monitor_topology(config, mission)?,
2116 None,
2117 )?
2118 .with_subsystem_id(subsystem.id())
2119 .with_instance_id(instance_id);
2120 #[cfg(feature = "std")]
2121 let monitor_runtime =
2122 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
2123 #[cfg(not(feature = "std"))]
2124 let monitor_runtime = CuMonitoringRuntime::unavailable();
2125 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
2126 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
2127
2128 let copperlist_sink = output_requirements
2129 .completed_copperlists()
2130 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
2131 let keyframe_sink = output_requirements
2132 .keyframes()
2133 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
2134 let keyframe_interval = config
2135 .logging
2136 .as_ref()
2137 .and_then(|logging| logging.keyframe_interval)
2138 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
2139
2140 let (copperlists_manager, keyframes_manager) = if P::DISTRIBUTED {
2141 let keyframes_manager =
2142 KeyFramesManager::distributed(keyframe_sink.is_some(), keyframe_interval)?;
2143 (
2144 CopperListsManager::new_with_keyframes(copperlist_sink, keyframe_sink)?,
2145 keyframes_manager,
2146 )
2147 } else {
2148 (
2149 CopperListsManager::new(copperlist_sink)?,
2150 KeyFramesManager::new(keyframe_sink, keyframe_interval)?,
2151 )
2152 };
2153 #[cfg(target_os = "none")]
2154 {
2155 let cl_size = core::mem::size_of::<CopperList<P>>();
2156 let total_bytes = cl_size.saturating_mul(NBCL);
2157 info!(
2158 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
2159 NBCL, cl_size, total_bytes
2160 );
2161 }
2162
2163 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2164 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
2165
2166 let runtime_config = config.runtime.clone().unwrap_or_default();
2167 runtime_config.validate()?;
2168
2169 Ok(CuRuntime {
2170 subsystem_code: subsystem.code(),
2171 instance_id,
2172 tasks,
2173 bridges,
2174 resources,
2175 thread_pools,
2176 monitor,
2177 execution_probe,
2178 clock,
2179 copperlists_manager,
2180 keyframes_manager,
2181 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2182 parallel_rt,
2183 runtime_config,
2184 })
2185 }
2186}
2187
2188#[cfg(not(feature = "std"))]
2189impl<
2190 'cfg,
2191 CT,
2192 CB,
2193 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2194 M: CuMonitor,
2195 const NBCL: usize,
2196 TI,
2197 BI,
2198 MI,
2199 CLS,
2200 KFS,
2201> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
2202where
2203 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
2204 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
2205 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
2206 CLS: WriteStream<CopperList<P>> + 'static,
2207 KFS: WriteStream<KeyFrame> + 'static,
2208{
2209 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
2210 let Self {
2211 clock,
2212 config,
2213 mission,
2214 subsystem,
2215 instance_id,
2216 resources,
2217 parts,
2218 copperlist_sink,
2219 keyframe_sink,
2220 output_requirements,
2221 } = self;
2222 let mut resources =
2223 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
2224
2225 let graph = config.get_graph(Some(mission))?;
2226 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
2227 .get_all_nodes()
2228 .iter()
2229 .map(|(_, node)| node.get_instance_config())
2230 .collect();
2231
2232 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
2233
2234 let execution_probe = RuntimeExecutionProbe::default();
2235 let monitor_metadata = CuMonitoringMetadata::new(
2236 CompactString::from(mission),
2237 parts.monitored_components,
2238 parts.culist_component_mapping,
2239 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
2240 build_monitor_topology(config, mission)?,
2241 None,
2242 )?
2243 .with_subsystem_id(subsystem.id())
2244 .with_instance_id(instance_id);
2245 let monitor_runtime = CuMonitoringRuntime::unavailable();
2246 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
2247 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
2248
2249 let copperlist_sink = output_requirements
2250 .completed_copperlists()
2251 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
2252 let keyframe_sink = output_requirements
2253 .keyframes()
2254 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
2255 let keyframe_interval = config
2256 .logging
2257 .as_ref()
2258 .and_then(|logging| logging.keyframe_interval)
2259 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
2260
2261 let (copperlists_manager, keyframes_manager) = if P::DISTRIBUTED {
2262 let keyframes_manager =
2263 KeyFramesManager::distributed(keyframe_sink.is_some(), keyframe_interval)?;
2264 (
2265 CopperListsManager::new_with_keyframes(copperlist_sink, keyframe_sink)?,
2266 keyframes_manager,
2267 )
2268 } else {
2269 (
2270 CopperListsManager::new(copperlist_sink)?,
2271 KeyFramesManager::new(keyframe_sink, keyframe_interval)?,
2272 )
2273 };
2274 #[cfg(target_os = "none")]
2275 {
2276 let cl_size = core::mem::size_of::<CopperList<P>>();
2277 let total_bytes = cl_size.saturating_mul(NBCL);
2278 info!(
2279 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
2280 NBCL, cl_size, total_bytes
2281 );
2282 }
2283
2284 let runtime_config = config.runtime.clone().unwrap_or_default();
2285 runtime_config.validate()?;
2286
2287 Ok(CuRuntime {
2288 subsystem_code: subsystem.code(),
2289 instance_id,
2290 tasks,
2291 bridges,
2292 resources,
2293 monitor,
2294 execution_probe,
2295 clock,
2296 copperlists_manager,
2297 keyframes_manager,
2298 runtime_config,
2299 })
2300 }
2301}
2302
2303#[derive(Clone, Decode)]
2308pub struct KeyFrame {
2309 pub culistid: u64,
2311 pub timestamp: CuTime,
2313 pub serialized_tasks: Vec<u8>,
2315}
2316
2317const DISTRIBUTED_KEYFRAME_HEADER: &[u8; 5] = b"CUKD\x01";
2318const DISTRIBUTED_ACTIVE_OFFSET: usize = DISTRIBUTED_KEYFRAME_HEADER.len();
2319const DISTRIBUTED_COUNT_OFFSET: usize = DISTRIBUTED_ACTIVE_OFFSET + 1;
2320const DISTRIBUTED_TABLE_OFFSET: usize = DISTRIBUTED_COUNT_OFFSET + 4;
2321const DISTRIBUTED_ENTRY_LEN: usize = 16;
2322const DISTRIBUTED_REGION_LEN: usize = 8;
2323const DISTRIBUTED_REGION_ALIGN: usize = 128;
2324
2325struct RegionWriter<'a> {
2326 bytes: &'a mut [u8],
2327 written: usize,
2328}
2329
2330impl Writer for RegionWriter<'_> {
2331 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
2332 let end = self
2333 .written
2334 .checked_add(bytes.len())
2335 .ok_or(EncodeError::UnexpectedEnd)?;
2336 self.bytes
2337 .get_mut(self.written..end)
2338 .ok_or(EncodeError::UnexpectedEnd)?
2339 .copy_from_slice(bytes);
2340 self.written = end;
2341 Ok(())
2342 }
2343}
2344
2345fn distributed_count(bytes: &[u8]) -> Result<Option<usize>, &'static str> {
2346 if !bytes.starts_with(DISTRIBUTED_KEYFRAME_HEADER) {
2347 return Ok(None);
2348 }
2349 let count_end = DISTRIBUTED_COUNT_OFFSET + 4;
2350 let count = u32::from_le_bytes(
2351 bytes
2352 .get(DISTRIBUTED_COUNT_OFFSET..count_end)
2353 .ok_or("distributed keyframe header is truncated")?
2354 .try_into()
2355 .map_err(|_| "distributed keyframe count is invalid")?,
2356 ) as usize;
2357 let table_end = DISTRIBUTED_TABLE_OFFSET
2358 .checked_add(
2359 count
2360 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2361 .ok_or("distributed keyframe table overflows")?,
2362 )
2363 .ok_or("distributed keyframe table overflows")?;
2364 if table_end > bytes.len() {
2365 return Err("distributed keyframe table is truncated");
2366 }
2367 Ok(Some(count))
2368}
2369
2370fn distributed_region(bytes: &[u8], component: usize) -> Result<(usize, usize), &'static str> {
2371 let count = distributed_count(bytes)?.ok_or("distributed keyframe storage is unavailable")?;
2372 if component >= count {
2373 return Err("keyframe component is out of bounds");
2374 }
2375 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2376 let offset = u64::from_le_bytes(
2377 bytes[entry..entry + 8]
2378 .try_into()
2379 .map_err(|_| "distributed keyframe offset is invalid")?,
2380 );
2381 let capacity = u64::from_le_bytes(
2382 bytes[entry + 8..entry + DISTRIBUTED_ENTRY_LEN]
2383 .try_into()
2384 .map_err(|_| "distributed keyframe capacity is invalid")?,
2385 );
2386 let offset = usize::try_from(offset).map_err(|_| "distributed keyframe offset is too large")?;
2387 let capacity =
2388 usize::try_from(capacity).map_err(|_| "distributed keyframe capacity is too large")?;
2389 let end = offset
2390 .checked_add(DISTRIBUTED_REGION_LEN)
2391 .and_then(|value| value.checked_add(capacity))
2392 .ok_or("distributed keyframe region overflows")?;
2393 if end > bytes.len() {
2394 return Err("distributed keyframe region is truncated");
2395 }
2396 Ok((offset, capacity))
2397}
2398
2399unsafe fn distributed_region_raw(
2400 ptr: *const u8,
2401 allocation_len: usize,
2402 component: usize,
2403) -> Result<(usize, usize), &'static str> {
2404 if allocation_len < DISTRIBUTED_COUNT_OFFSET + 4 {
2405 return Err("distributed keyframe header is truncated");
2406 }
2407 for (index, expected) in DISTRIBUTED_KEYFRAME_HEADER.iter().copied().enumerate() {
2408 if unsafe { ptr.add(index).read() } != expected {
2411 return Err("distributed keyframe storage is unavailable");
2412 }
2413 }
2414 let count = u32::from_le(unsafe {
2417 ptr.add(DISTRIBUTED_COUNT_OFFSET)
2418 .cast::<u32>()
2419 .read_unaligned()
2420 }) as usize;
2421 let table_end = DISTRIBUTED_TABLE_OFFSET
2422 .checked_add(
2423 count
2424 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2425 .ok_or("distributed keyframe table overflows")?,
2426 )
2427 .ok_or("distributed keyframe table overflows")?;
2428 if table_end > allocation_len {
2429 return Err("distributed keyframe table is truncated");
2430 }
2431 if component >= count {
2432 return Err("keyframe component is out of bounds");
2433 }
2434 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2435 let offset = u64::from_le(unsafe { ptr.add(entry).cast::<u64>().read_unaligned() });
2437 let capacity = u64::from_le(unsafe { ptr.add(entry + 8).cast::<u64>().read_unaligned() });
2439 let offset = usize::try_from(offset).map_err(|_| "distributed keyframe offset is too large")?;
2440 let capacity =
2441 usize::try_from(capacity).map_err(|_| "distributed keyframe capacity is too large")?;
2442 let end = offset
2443 .checked_add(DISTRIBUTED_REGION_LEN)
2444 .and_then(|value| value.checked_add(capacity))
2445 .ok_or("distributed keyframe region overflows")?;
2446 if end > allocation_len {
2447 return Err("distributed keyframe region is truncated");
2448 }
2449 Ok((offset, capacity))
2450}
2451
2452fn distributed_frame(bytes: &[u8], component: usize) -> Result<&[u8], &'static str> {
2453 let (offset, capacity) = distributed_region(bytes, component)?;
2454 let frame_len = u64::from_le_bytes(
2455 bytes[offset..offset + DISTRIBUTED_REGION_LEN]
2456 .try_into()
2457 .map_err(|_| "distributed keyframe length is invalid")?,
2458 );
2459 let frame_len =
2460 usize::try_from(frame_len).map_err(|_| "distributed keyframe length is too large")?;
2461 if !(KEYFRAME_FRAME_HEADER_LEN..=capacity).contains(&frame_len) {
2462 return Err("distributed keyframe component is incomplete");
2463 }
2464 let frame_start = offset + DISTRIBUTED_REGION_LEN;
2465 Ok(&bytes[frame_start..frame_start + frame_len])
2466}
2467
2468fn align_up(value: usize, alignment: usize) -> Option<usize> {
2469 value
2470 .checked_add(alignment - 1)
2471 .map(|value| value & !(alignment - 1))
2472}
2473
2474impl Encode for KeyFrame {
2475 fn encode<__E: bincode::enc::Encoder>(&self, encoder: &mut __E) -> Result<(), EncodeError> {
2476 self.culistid.encode(encoder)?;
2477 self.timestamp.encode(encoder)?;
2478 let bytes = self.serialized_tasks.as_slice();
2479 let Some(count) = distributed_count(bytes).map_err(EncodeError::Other)? else {
2480 return self.serialized_tasks.encode(encoder);
2481 };
2482 let mut length = KEYFRAME_PAYLOAD_HEADER.len();
2483 for component in 0..count {
2484 length = length
2485 .checked_add(
2486 distributed_frame(bytes, component)
2487 .map_err(EncodeError::Other)?
2488 .len(),
2489 )
2490 .ok_or(EncodeError::Other("distributed keyframe length overflows"))?;
2491 }
2492 length.encode(encoder)?;
2493 encoder.writer().write(KEYFRAME_PAYLOAD_HEADER)?;
2494 for component in 0..count {
2495 encoder
2496 .writer()
2497 .write(distributed_frame(bytes, component).map_err(EncodeError::Other)?)?;
2498 }
2499 Ok(())
2500 }
2501}
2502
2503impl KeyFrame {
2504 pub(crate) fn from_serialized(
2505 culistid: u64,
2506 timestamp: CuTime,
2507 serialized_tasks: Vec<u8>,
2508 ) -> Self {
2509 Self {
2510 culistid,
2511 timestamp,
2512 serialized_tasks,
2513 }
2514 }
2515
2516 #[inline]
2517 #[cfg(feature = "std")]
2518 pub(crate) fn is_active(&self) -> bool {
2519 self.serialized_tasks
2520 .starts_with(DISTRIBUTED_KEYFRAME_HEADER)
2521 && self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] != 0
2522 }
2523
2524 #[doc(hidden)]
2526 pub fn distributed(capacities: &[usize]) -> Self {
2527 let table_end = DISTRIBUTED_TABLE_OFFSET
2528 .checked_add(
2529 capacities
2530 .len()
2531 .checked_mul(DISTRIBUTED_ENTRY_LEN)
2532 .expect("distributed keyframe region table overflow"),
2533 )
2534 .expect("distributed keyframe region table overflow");
2535 let slab_bytes = capacities.iter().fold(0usize, |total, &capacity| {
2536 let capacity = capacity.max(KEYFRAME_FRAME_HEADER_LEN);
2537 let region_bytes =
2538 align_up(DISTRIBUTED_REGION_LEN + capacity, DISTRIBUTED_REGION_ALIGN)
2539 .expect("distributed keyframe region size overflow");
2540 total
2541 .checked_add(region_bytes)
2542 .expect("distributed keyframe slab size overflow")
2543 });
2544 let maximum = table_end
2545 .checked_add(DISTRIBUTED_REGION_ALIGN - 1)
2546 .and_then(|value| value.checked_add(slab_bytes))
2547 .expect("distributed keyframe allocation size overflow");
2548 let mut serialized_tasks = alloc::vec![0; maximum];
2549 serialized_tasks[..DISTRIBUTED_KEYFRAME_HEADER.len()]
2550 .copy_from_slice(DISTRIBUTED_KEYFRAME_HEADER);
2551 let count = u32::try_from(capacities.len()).expect("too many keyframe components");
2552 serialized_tasks[DISTRIBUTED_COUNT_OFFSET..DISTRIBUTED_COUNT_OFFSET + 4]
2553 .copy_from_slice(&count.to_le_bytes());
2554
2555 let allocation = serialized_tasks.as_ptr() as usize;
2556 let mut region_offset = align_up(
2557 allocation
2558 .checked_add(table_end)
2559 .expect("distributed keyframe address overflow"),
2560 DISTRIBUTED_REGION_ALIGN,
2561 )
2562 .expect("distributed keyframe alignment overflow")
2563 - allocation;
2564 for (component, &capacity) in capacities.iter().enumerate() {
2565 let capacity = capacity.max(KEYFRAME_FRAME_HEADER_LEN);
2566 let entry = DISTRIBUTED_TABLE_OFFSET + component * DISTRIBUTED_ENTRY_LEN;
2567 serialized_tasks[entry..entry + 8]
2568 .copy_from_slice(&(region_offset as u64).to_le_bytes());
2569 serialized_tasks[entry + 8..entry + DISTRIBUTED_ENTRY_LEN]
2570 .copy_from_slice(&(capacity as u64).to_le_bytes());
2571 region_offset += align_up(DISTRIBUTED_REGION_LEN + capacity, DISTRIBUTED_REGION_ALIGN)
2572 .expect("distributed keyframe region size overflow");
2573 }
2574 serialized_tasks.truncate(region_offset);
2575 Self {
2576 culistid: 0,
2577 timestamp: CuTime::default(),
2578 serialized_tasks,
2579 }
2580 }
2581
2582 #[doc(hidden)]
2583 pub fn reset_distributed(&mut self, culistid: u64, timestamp: CuTime, interval: u32) {
2584 self.culistid = culistid;
2585 self.timestamp = timestamp;
2586 let active = interval != 0 && culistid.is_multiple_of(u64::from(interval));
2587 if distributed_count(&self.serialized_tasks)
2588 .ok()
2589 .flatten()
2590 .is_some()
2591 {
2592 self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] = u8::from(active);
2593 }
2594 if active {
2595 let count = distributed_count(&self.serialized_tasks)
2596 .expect("distributed keyframe descriptor")
2597 .expect("distributed keyframe storage");
2598 for component in 0..count {
2599 let (offset, _) = distributed_region(&self.serialized_tasks, component)
2600 .expect("distributed keyframe region");
2601 self.serialized_tasks[offset..offset + DISTRIBUTED_REGION_LEN].fill(0);
2602 }
2603 }
2604 }
2605
2606 #[doc(hidden)]
2607 #[inline]
2608 pub fn discard_distributed(&mut self) {
2609 if distributed_count(&self.serialized_tasks)
2610 .ok()
2611 .flatten()
2612 .is_some()
2613 {
2614 self.serialized_tasks[DISTRIBUTED_ACTIVE_OFFSET] = 0;
2615 }
2616 }
2617
2618 #[doc(hidden)]
2626 pub unsafe fn freeze_region(
2627 ptr: *mut u8,
2628 allocation_len: usize,
2629 component: usize,
2630 task: &impl Freezable,
2631 ) -> CuResult<usize> {
2632 if ptr.is_null() || allocation_len <= DISTRIBUTED_ACTIVE_OFFSET {
2633 return Ok(0);
2634 }
2635 if unsafe { ptr.add(DISTRIBUTED_ACTIVE_OFFSET).read() } == 0 {
2636 return Ok(0);
2637 }
2638 let (offset, capacity) =
2641 unsafe { distributed_region_raw(ptr.cast_const(), allocation_len, component) }
2642 .map_err(CuError::from)?;
2643 let length = u64::from_le(unsafe { ptr.add(offset).cast::<u64>().read_unaligned() });
2646 if length != 0 {
2647 return Err(CuError::from(
2648 "Component captured twice in one distributed keyframe",
2649 ));
2650 }
2651 let frame = unsafe {
2654 core::slice::from_raw_parts_mut(ptr.add(offset + DISTRIBUTED_REGION_LEN), capacity)
2655 };
2656 let mut writer = RegionWriter {
2657 bytes: &mut frame[KEYFRAME_FRAME_HEADER_LEN..],
2658 written: 0,
2659 };
2660 let mut encoder = EncoderImpl::new(&mut writer, bincode::config::standard());
2661 BincodeAdapter(task).encode(&mut encoder).map_err(|error| {
2662 CuError::from(format!("Failed to capture keyframe component: {error}"))
2663 })?;
2664 let payload_len = writer.written;
2665 let payload_len = u32::try_from(payload_len)
2666 .map_err(|_| CuError::from("Keyframe component exceeds u32 size"))?;
2667 frame[..KEYFRAME_FRAME_HEADER_LEN].copy_from_slice(&payload_len.to_le_bytes());
2668 let frame_len = KEYFRAME_FRAME_HEADER_LEN + payload_len as usize;
2669 unsafe {
2670 ptr.add(offset)
2671 .cast::<u64>()
2672 .write_unaligned(frame_len as u64);
2673 }
2674 Ok(frame_len)
2675 }
2676
2677 fn new() -> Self {
2678 Self::from_serialized(0, CuTime::default(), KEYFRAME_PAYLOAD_HEADER.to_vec())
2679 }
2680
2681 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
2683 self.culistid = culistid;
2684 self.timestamp = timestamp;
2685 self.serialized_tasks.clear();
2686 self.serialized_tasks
2687 .extend_from_slice(KEYFRAME_PAYLOAD_HEADER);
2688 }
2689
2690 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
2692 let cfg = bincode::config::standard();
2693 let start = self.serialized_tasks.len();
2694 let payload_offset =
2695 start
2696 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
2697 .ok_or(EncodeError::Other(
2698 "keyframe component frame offset overflow",
2699 ))?;
2700 if payload_offset > self.serialized_tasks.capacity() {
2701 return Err(EncodeError::UnexpectedEnd);
2702 }
2703
2704 self.serialized_tasks.resize(payload_offset, 0);
2705 let length_offset = start;
2706 self.serialized_tasks[length_offset..payload_offset].fill(0);
2707
2708 let mut encoder =
2709 EncoderImpl::<_, _>::new(PreallocatedVecWriter(&mut self.serialized_tasks), cfg);
2710 if let Err(error) = BincodeAdapter(task).encode(&mut encoder) {
2711 self.serialized_tasks.truncate(start);
2712 return Err(error);
2713 }
2714 let payload_len = encoder.into_writer().0.len() - payload_offset;
2715 let payload_len = u32::try_from(payload_len).map_err(|_| {
2716 self.serialized_tasks.truncate(start);
2717 EncodeError::OtherString(
2718 "keyframe component snapshot exceeds the u32 frame limit".to_string(),
2719 )
2720 })?;
2721 self.serialized_tasks
2722 .truncate(payload_offset + payload_len as usize);
2723 self.serialized_tasks[length_offset..payload_offset]
2724 .copy_from_slice(&payload_len.to_le_bytes());
2725 Ok(self.serialized_tasks.len() - start)
2726 }
2727}
2728
2729const KEYFRAME_PAYLOAD_MAGIC: &[u8; 4] = b"CUKF";
2730const KEYFRAME_PAYLOAD_VERSION: u8 = 1;
2731const KEYFRAME_PAYLOAD_HEADER: &[u8; 5] = b"CUKF\x01";
2732const KEYFRAME_FRAME_HEADER_LEN: usize = 4;
2733
2734#[doc(hidden)]
2736pub struct KeyFramePayloadReader<'a> {
2737 remaining: &'a [u8],
2738 distributed: Option<(&'a [u8], usize, usize)>,
2739}
2740
2741impl<'a> KeyFramePayloadReader<'a> {
2742 pub fn new(keyframe: &'a KeyFrame) -> CuResult<Self> {
2744 let payload = keyframe.serialized_tasks.as_slice();
2745 if let Some(count) = distributed_count(payload).map_err(CuError::from)? {
2746 return Ok(Self {
2747 remaining: &[],
2748 distributed: Some((payload, 0, count)),
2749 });
2750 }
2751 if payload.len() < KEYFRAME_PAYLOAD_HEADER.len()
2752 || payload[..KEYFRAME_PAYLOAD_MAGIC.len()] != *KEYFRAME_PAYLOAD_MAGIC
2753 {
2754 return Err(CuError::from(
2755 "Unsupported legacy keyframe payload: expected framed format version 1",
2756 ));
2757 }
2758 let version = payload[KEYFRAME_PAYLOAD_MAGIC.len()];
2759 if version != KEYFRAME_PAYLOAD_VERSION {
2760 return Err(CuError::from(format!(
2761 "Unsupported keyframe payload version {version}; expected {KEYFRAME_PAYLOAD_VERSION}"
2762 )));
2763 }
2764 Ok(Self {
2765 remaining: &payload[KEYFRAME_PAYLOAD_HEADER.len()..],
2766 distributed: None,
2767 })
2768 }
2769
2770 pub fn next_frame(&mut self) -> CuResult<&'a [u8]> {
2772 if let Some((bytes, index, count)) = &mut self.distributed {
2773 if *index >= *count {
2774 return Err(CuError::from("Keyframe ended before next component frame"));
2775 }
2776 let frame = distributed_frame(bytes, *index).map_err(CuError::from)?;
2777 *index += 1;
2778 return Ok(&frame[KEYFRAME_FRAME_HEADER_LEN..]);
2779 }
2780 if self.remaining.len() < KEYFRAME_FRAME_HEADER_LEN {
2781 return Err(CuError::from("Keyframe ended before next component frame"));
2782 }
2783 let payload_len = u32::from_le_bytes(
2784 self.remaining[..KEYFRAME_FRAME_HEADER_LEN]
2785 .try_into()
2786 .map_err(|_| CuError::from("Invalid keyframe component frame length"))?,
2787 ) as usize;
2788 let frame_end = KEYFRAME_FRAME_HEADER_LEN
2789 .checked_add(payload_len)
2790 .ok_or_else(|| CuError::from("Keyframe component frame length overflow"))?;
2791 if frame_end > self.remaining.len() {
2792 return Err(CuError::from("Keyframe component frame is truncated"));
2793 }
2794 let payload = &self.remaining[KEYFRAME_FRAME_HEADER_LEN..frame_end];
2795 self.remaining = &self.remaining[frame_end..];
2796 Ok(payload)
2797 }
2798
2799 pub fn finish(self) -> CuResult<()> {
2801 if let Some((_, index, count)) = self.distributed {
2802 return if index == count {
2803 Ok(())
2804 } else {
2805 Err(CuError::from("Keyframe contains trailing component data"))
2806 };
2807 }
2808 if self.remaining.is_empty() {
2809 Ok(())
2810 } else {
2811 Err(CuError::from("Keyframe contains trailing component data"))
2812 }
2813 }
2814}
2815
2816struct FrameSliceReader<'a> {
2817 remaining: &'a [u8],
2818}
2819
2820impl Reader for FrameSliceReader<'_> {
2821 fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
2822 if bytes.len() > self.remaining.len() {
2823 return Err(DecodeError::UnexpectedEnd {
2824 additional: bytes.len() - self.remaining.len(),
2825 });
2826 }
2827 let (read, remaining) = self.remaining.split_at(bytes.len());
2828 bytes.copy_from_slice(read);
2829 self.remaining = remaining;
2830 Ok(())
2831 }
2832
2833 fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
2834 self.remaining.get(..length)
2835 }
2836
2837 fn consume(&mut self, length: usize) {
2838 self.remaining = self.remaining.get(length..).unwrap_or_default();
2839 }
2840}
2841
2842#[doc(hidden)]
2844pub fn thaw_keyframe_component(item: &mut impl Freezable, frame: &[u8]) -> CuResult<()> {
2845 let reader = FrameSliceReader { remaining: frame };
2846 let mut decoder = DecoderImpl::new(reader, bincode::config::standard(), ());
2847 item.thaw(&mut decoder)
2848 .map_err(|error| CuError::from(format!("Failed to thaw keyframe component: {error}")))?;
2849 let trailing = decoder.reader().remaining.len();
2850 if trailing != 0 {
2851 return Err(CuError::from(format!(
2852 "Keyframe component snapshot has {} trailing bytes",
2853 trailing
2854 )));
2855 }
2856 Ok(())
2857}
2858
2859#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2861pub enum RuntimeLifecycleConfigSource {
2862 ProgrammaticOverride,
2863 ExternalFile,
2864 BundledDefault,
2865}
2866
2867#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2869pub struct RuntimeLifecycleStackInfo {
2870 pub app_name: String,
2871 pub app_version: String,
2872 pub git_commit: Option<String>,
2873 pub git_dirty: Option<bool>,
2874 pub subsystem_id: Option<String>,
2875 pub subsystem_code: u16,
2876 pub instance_id: u32,
2877}
2878
2879#[derive(Clone, Copy, Encode, Decode, Debug, PartialEq, Eq)]
2881pub enum RuntimeStopReason {
2882 Requested,
2883 Completed,
2884 Error,
2885 Panic,
2886}
2887
2888#[derive(Clone, Copy, Encode, Decode, Debug, PartialEq, Eq)]
2890pub enum RuntimeLifecycleOperation {
2891 Start,
2892 Stop,
2893 Iteration,
2894 Shutdown,
2895}
2896
2897#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2899pub enum RuntimeLifecycleEvent {
2900 Instantiated {
2901 config_source: RuntimeLifecycleConfigSource,
2902 },
2903 MissionStarted,
2904 MissionStopped {
2905 reason: RuntimeStopReason,
2906 },
2907 LifecycleFailed {
2908 operation: RuntimeLifecycleOperation,
2909 error: String,
2910 },
2911 Panic {
2912 message: String,
2913 file: Option<String>,
2914 line: Option<u32>,
2915 column: Option<u32>,
2916 },
2917 ShutdownCompleted,
2918 #[cfg(feature = "clock-sync")]
2920 ClockSync(crate::clock_sync::ClockSyncRecord),
2921}
2922
2923impl RuntimeLifecycleEvent {
2924 pub fn clock_sync_record(&self) -> Option<&crate::clock_sync::ClockSyncRecord> {
2926 #[cfg(feature = "clock-sync")]
2927 if let Self::ClockSync(record) = self {
2928 return Some(record);
2929 }
2930 None
2931 }
2932}
2933
2934#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2936pub struct RuntimeLifecycleRecord {
2937 pub timestamp: CuTime,
2938 pub event: RuntimeLifecycleEvent,
2939}
2940
2941#[doc(hidden)]
2943pub type RuntimeLifecycleSink = SemanticRecordSink<RuntimeLifecycleRecord>;
2944
2945impl<
2946 CT,
2947 CB,
2948 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2949 M: CuMonitor,
2950 const NBCL: usize,
2951> CuRuntime<CT, CB, P, M, NBCL>
2952{
2953 #[inline]
2957 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
2958 self.execution_probe.record(marker);
2959 }
2960
2961 #[inline]
2966 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
2967 #[cfg(feature = "std")]
2968 {
2969 self.execution_probe.as_ref()
2970 }
2971
2972 #[cfg(not(feature = "std"))]
2973 {
2974 &self.execution_probe
2975 }
2976 }
2977}
2978
2979#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2984pub enum CuTaskType {
2985 Source,
2986 Regular,
2987 Sink,
2988}
2989
2990impl From<TaskKind> for CuTaskType {
2991 fn from(value: TaskKind) -> Self {
2992 match value {
2993 TaskKind::Source => CuTaskType::Source,
2994 TaskKind::Regular | TaskKind::Stateless => CuTaskType::Regular,
2995 TaskKind::Sink => CuTaskType::Sink,
2996 }
2997 }
2998}
2999
3000#[derive(Debug, Clone)]
3001pub struct CuOutputPack {
3002 pub culist_index: u32,
3003 pub msg_types: Vec<String>,
3004 pub src_channels: Vec<Option<String>>,
3012}
3013
3014#[derive(Debug, Clone)]
3015pub struct CuInputMsg {
3016 pub culist_index: u32,
3017 pub msg_type: String,
3018 pub src_port: usize,
3019 pub edge_id: usize,
3020 pub connection_order: usize,
3021}
3022
3023#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3031pub enum CuStepPhase {
3032 #[default]
3034 Whole,
3035 AnytimeBase,
3038 AnytimeRefine,
3040}
3041
3042pub struct CuExecutionStep {
3044 pub node_id: NodeId,
3046 pub node: Node,
3048 pub task_type: CuTaskType,
3050 pub phase: CuStepPhase,
3053
3054 pub input_msg_indices_types: Vec<CuInputMsg>,
3057
3058 pub output_msg_pack: Option<CuOutputPack>,
3061}
3062
3063impl Debug for CuExecutionStep {
3064 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
3065 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
3066 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
3067 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
3068 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
3069 f.write_str(
3070 format!(
3071 " input_msg_types: {:?}\n",
3072 self.input_msg_indices_types
3073 )
3074 .as_str(),
3075 )?;
3076 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
3077 Ok(())
3078 }
3079}
3080
3081pub struct CuExecutionLoop {
3086 pub steps: Vec<CuExecutionUnit>,
3087 pub loop_count: Option<u32>,
3088}
3089
3090impl Debug for CuExecutionLoop {
3091 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
3092 f.write_str("CuExecutionLoop:\n")?;
3093 for step in &self.steps {
3094 match step {
3095 CuExecutionUnit::Step(step) => {
3096 step.fmt(f)?;
3097 }
3098 CuExecutionUnit::Loop(l) => {
3099 l.fmt(f)?;
3100 }
3101 }
3102 }
3103
3104 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
3105 Ok(())
3106 }
3107}
3108
3109#[derive(Debug)]
3111pub enum CuExecutionUnit {
3112 Step(Box<CuExecutionStep>),
3113 Loop(CuExecutionLoop),
3114}
3115
3116pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
3117 let node = graph
3118 .get_node(node_id)
3119 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
3120
3121 if node.get_flavor() == crate::config::Flavor::Task {
3122 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
3123 }
3124
3125 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
3126 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
3127 Ok(match (has_inputs, has_outputs) {
3128 (false, true) => CuTaskType::Source,
3129 (true, false) => CuTaskType::Sink,
3130 _ => CuTaskType::Regular,
3131 })
3132}
3133
3134pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
3136 let order = topo_bfs_order(graph)?;
3137 check_order(graph, &order)?;
3138 plan_from_order(graph, &order)
3139}
3140
3141pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
3159 loop {
3160 let Some(base_pos) = plan.steps.iter().position(|unit| {
3163 matches!(
3164 unit,
3165 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
3166 && step.node.anytime().is_some()
3167 && !step.node.is_background()
3168 )
3169 }) else {
3170 return Ok(());
3171 };
3172
3173 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
3174 unreachable!("position() only matches steps");
3175 };
3176 let anytime = base_step
3177 .node
3178 .anytime()
3179 .expect("position() only matches anytime nodes");
3180 let Some(max_refines) = anytime.max_refines else {
3183 return Err(CuError::from(format!(
3184 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
3185 base_step.node.get_id()
3186 )));
3187 };
3188 base_step.phase = CuStepPhase::AnytimeBase;
3189 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
3190 CuError::from(format!(
3191 "Task '{}': an anytime task needs an output to refine.",
3192 base_step.node.get_id()
3193 ))
3194 })?;
3195 let output_index = output_pack.culist_index;
3196 let node_id = base_step.node_id;
3197 let node = base_step.node.clone();
3198 let task_type = base_step.task_type;
3199
3200 let refine_step = || {
3201 CuExecutionUnit::Step(Box::new(CuExecutionStep {
3202 node_id,
3203 node: node.clone(),
3204 task_type,
3205 phase: CuStepPhase::AnytimeRefine,
3206 input_msg_indices_types: Vec::new(),
3207 output_msg_pack: Some(output_pack.clone()),
3208 }))
3209 };
3210
3211 let consumer_pos = plan.steps[base_pos + 1..]
3214 .iter()
3215 .position(|unit| {
3216 matches!(
3217 unit,
3218 CuExecutionUnit::Step(step) if step
3219 .input_msg_indices_types
3220 .iter()
3221 .any(|input| input.culist_index == output_index)
3222 )
3223 })
3224 .map(|offset| base_pos + 1 + offset)
3225 .unwrap_or(base_pos + 1);
3226
3227 let mut tail = plan.steps.split_off(base_pos + 1);
3228 let suffix = tail.split_off(consumer_pos - base_pos - 1);
3229 let gap = tail;
3230
3231 let mut remaining = max_refines.max(1);
3233 remaining -= 1;
3234 plan.steps.push(refine_step());
3235 for gap_unit in gap {
3236 plan.steps.push(gap_unit);
3237 if remaining > 0 {
3238 remaining -= 1;
3239 plan.steps.push(refine_step());
3240 }
3241 }
3242 for _ in 0..remaining {
3243 plan.steps.push(refine_step());
3244 }
3245 plan.steps.extend(suffix);
3246 }
3247}
3248
3249#[cfg(test)]
3251mod tests {
3252 use super::*;
3253 use crate::config::Node;
3254 use crate::context::CuContext;
3255 use crate::cutask::CuSinkTask;
3256 use crate::cutask::{CuSrcTask, Freezable};
3257 use crate::monitoring::NoMonitor;
3258 use crate::reflect::Reflect;
3259 use bincode::Encode;
3260 use core::cell::Cell;
3261 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
3262 use serde_derive::{Deserialize, Serialize};
3263 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3264 use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
3265 #[cfg(feature = "std")]
3266 use std::sync::{Arc, Mutex};
3267
3268 struct CountingSnapshot<'a> {
3269 calls: &'a Cell<usize>,
3270 value: u32,
3271 fail: bool,
3272 }
3273
3274 impl Freezable for CountingSnapshot<'_> {
3275 fn freeze<E: bincode::enc::Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
3276 self.calls.set(self.calls.get() + 1);
3277 self.value.encode(encoder)?;
3278 if self.fail {
3279 Err(EncodeError::OtherString(
3280 "intentional freeze failure".to_string(),
3281 ))
3282 } else {
3283 Ok(())
3284 }
3285 }
3286 }
3287
3288 #[derive(Default)]
3289 struct SnapshotValue(u32);
3290
3291 impl Freezable for SnapshotValue {
3292 fn thaw<D: bincode::de::Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
3293 self.0 = u32::decode(decoder)?;
3294 Ok(())
3295 }
3296 }
3297
3298 #[test]
3299 fn keyframe_frames_freeze_once_and_roll_back_only_failed_frame() {
3300 let calls = Cell::new(0);
3301 let mut keyframe = KeyFrame::new();
3302 keyframe
3303 .serialized_tasks
3304 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
3305 .unwrap();
3306 keyframe.reset(7, CuTime::from_nanos(70));
3307 keyframe
3308 .add_frozen_task(&CountingSnapshot {
3309 calls: &calls,
3310 value: 11,
3311 fail: false,
3312 })
3313 .unwrap();
3314 let committed_len = keyframe.serialized_tasks.len();
3315
3316 let failing = CountingSnapshot {
3317 calls: &calls,
3318 value: 99,
3319 fail: true,
3320 };
3321 assert!(keyframe.add_frozen_task(&failing).is_err());
3322 assert_eq!(keyframe.serialized_tasks.len(), committed_len);
3323
3324 keyframe
3325 .add_frozen_task(&CountingSnapshot {
3326 calls: &calls,
3327 value: 22,
3328 fail: false,
3329 })
3330 .unwrap();
3331 assert_eq!(calls.get(), 3, "each append must call freeze exactly once");
3332 let first_payload_len = bincode::encode_to_vec(11u32, bincode::config::standard())
3333 .unwrap()
3334 .len();
3335 let second_payload_len = bincode::encode_to_vec(22u32, bincode::config::standard())
3336 .unwrap()
3337 .len();
3338 assert_eq!(
3339 keyframe.serialized_tasks.len(),
3340 KEYFRAME_PAYLOAD_HEADER.len()
3341 + 2 * KEYFRAME_FRAME_HEADER_LEN
3342 + first_payload_len
3343 + second_payload_len,
3344 "component frames carry only a length prefix"
3345 );
3346
3347 let mut frames = KeyFramePayloadReader::new(&keyframe).unwrap();
3348 let mut first = SnapshotValue::default();
3349 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
3350 let mut second = SnapshotValue::default();
3351 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
3352 frames.finish().unwrap();
3353 assert_eq!((first.0, second.0), (11, 22));
3354 }
3355
3356 #[test]
3357 fn distributed_keyframe_preserves_serial_wire_and_restore_order() {
3358 let serial_calls = Cell::new(0);
3359 let mut serial = KeyFrame::new();
3360 serial.serialized_tasks.reserve(256);
3361 serial.reset(17, CuTime::from_nanos(123));
3362 serial
3363 .add_frozen_task(&CountingSnapshot {
3364 calls: &serial_calls,
3365 value: 11,
3366 fail: false,
3367 })
3368 .unwrap();
3369 serial
3370 .add_frozen_task(&CountingSnapshot {
3371 calls: &serial_calls,
3372 value: 22,
3373 fail: false,
3374 })
3375 .unwrap();
3376
3377 let distributed_calls = Cell::new(0);
3378 let mut distributed = KeyFrame::distributed(&[128, 128]);
3379 distributed.reset_distributed(17, CuTime::from_nanos(123), 1);
3380 for component in 0..2 {
3381 let (offset, _) = distributed_region(&distributed.serialized_tasks, component).unwrap();
3382 assert_eq!(
3383 (distributed.serialized_tasks.as_ptr() as usize + offset)
3384 % DISTRIBUTED_REGION_ALIGN,
3385 0,
3386 );
3387 }
3388 let capture_ptr = distributed.serialized_tasks.as_mut_ptr();
3389 let capture_len = distributed.serialized_tasks.len();
3390 unsafe {
3391 KeyFrame::freeze_region(
3392 capture_ptr,
3393 capture_len,
3394 1,
3395 &CountingSnapshot {
3396 calls: &distributed_calls,
3397 value: 22,
3398 fail: false,
3399 },
3400 )
3401 .unwrap();
3402 KeyFrame::freeze_region(
3403 capture_ptr,
3404 capture_len,
3405 0,
3406 &CountingSnapshot {
3407 calls: &distributed_calls,
3408 value: 11,
3409 fail: false,
3410 },
3411 )
3412 .unwrap();
3413 }
3414
3415 let config = bincode::config::standard();
3416 assert_eq!(
3417 bincode::encode_to_vec(&distributed, config).unwrap(),
3418 bincode::encode_to_vec(&serial, config).unwrap(),
3419 );
3420 let mut frames = KeyFramePayloadReader::new(&distributed).unwrap();
3421 let mut first = SnapshotValue::default();
3422 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
3423 let mut second = SnapshotValue::default();
3424 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
3425 frames.finish().unwrap();
3426 assert_eq!((first.0, second.0), (11, 22));
3427 assert_eq!(distributed_calls.get(), 2);
3428 }
3429
3430 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3431 #[test]
3432 fn arena_reset_and_distributed_capture_do_not_allocate() {
3433 let calls = Cell::new(0);
3434 let mut slots = allocate_slots::<Msgs>(1);
3435 slots[0].prepare_keyframes(&[128]);
3436 let mut slot = slots.pop().unwrap();
3437
3438 let allocations = crate::monitoring::ScopedAllocCounter::new();
3439 slot.reset(3);
3440 slot.keyframe_mut()
3441 .unwrap()
3442 .reset_distributed(3, CuTime::from_nanos(30), 1);
3443 let (capture_ptr, capture_len) = slot.keyframe_capture();
3444 unsafe {
3445 KeyFrame::freeze_region(
3446 capture_ptr,
3447 capture_len,
3448 0,
3449 &CountingSnapshot {
3450 calls: &calls,
3451 value: 42,
3452 fail: false,
3453 },
3454 )
3455 .unwrap();
3456 }
3457
3458 assert_eq!(allocations.allocated(), 0);
3459 assert_eq!(calls.get(), 1);
3460 }
3461
3462 #[cfg(feature = "std")]
3463 #[test]
3464 fn arena_slots_wrap_without_moving_storage() {
3465 let mut slots = allocate_slots::<Msgs>(2);
3466 let pointers = slots
3467 .iter_mut()
3468 .map(CuSlotLease::as_ptr)
3469 .collect::<Vec<_>>();
3470
3471 for generation in 1..=4 {
3472 slots.rotate_left(1);
3473 for slot in &mut slots {
3474 let index = slot.index();
3475 slot.reset((generation * 2 + index) as u64);
3476 assert_eq!(slot.generation(), generation as u64);
3477 assert_eq!(slot.as_ptr(), pointers[index]);
3478 }
3479 }
3480 }
3481
3482 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3483 #[test]
3484 fn preallocated_keyframe_append_does_not_allocate() {
3485 let calls = Cell::new(0);
3486 let mut keyframe = KeyFrame::new();
3487 keyframe
3488 .serialized_tasks
3489 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
3490 .unwrap();
3491 keyframe.reset(3, CuTime::from_nanos(30));
3492
3493 let allocations = crate::monitoring::ScopedAllocCounter::new();
3494 keyframe
3495 .add_frozen_task(&CountingSnapshot {
3496 calls: &calls,
3497 value: 42,
3498 fail: false,
3499 })
3500 .unwrap();
3501
3502 assert_eq!(allocations.allocated(), 0);
3503 assert_eq!(calls.get(), 1);
3504 }
3505
3506 #[test]
3507 fn keyframe_reader_rejects_legacy_payload_clearly() {
3508 let keyframe = KeyFrame::from_serialized(0, CuTime::default(), vec![0, 1, 2]);
3509 let error = match KeyFramePayloadReader::new(&keyframe) {
3510 Ok(_) => panic!("legacy payload unexpectedly accepted"),
3511 Err(error) => error,
3512 };
3513 assert!(error.to_string().contains("legacy keyframe payload"));
3514 }
3515
3516 #[derive(Reflect)]
3517 pub struct TestSource {}
3518
3519 impl Freezable for TestSource {}
3520
3521 impl CuSrcTask for TestSource {
3522 type Resources<'r> = ();
3523 type Output<'m> = ();
3524 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
3525 where
3526 Self: Sized,
3527 {
3528 Ok(Self {})
3529 }
3530
3531 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
3532 Ok(())
3533 }
3534 }
3535
3536 #[derive(Reflect)]
3537 pub struct TestSink {}
3538
3539 impl Freezable for TestSink {}
3540
3541 impl CuSinkTask for TestSink {
3542 type Resources<'r> = ();
3543 type Input<'m> = ();
3544
3545 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
3546 where
3547 Self: Sized,
3548 {
3549 Ok(Self {})
3550 }
3551
3552 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
3553 Ok(())
3554 }
3555 }
3556
3557 type Tasks = (TestSource, TestSink);
3559 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
3560 const TEST_NBCL: usize = 2;
3561
3562 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3563 struct Msgs(());
3564
3565 impl ErasedCuStampedDataSet for Msgs {
3566 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3567 Vec::new()
3568 }
3569 }
3570
3571 impl MatchingTasks for Msgs {
3572 fn get_all_task_ids() -> &'static [&'static str] {
3573 &[]
3574 }
3575 }
3576
3577 impl CuListZeroedInit for Msgs {
3578 fn init_zeroed(&mut self) {}
3579 }
3580
3581 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3582 struct IntMsgs(i32);
3583
3584 impl ErasedCuStampedDataSet for IntMsgs {
3585 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3586 Vec::new()
3587 }
3588 }
3589
3590 impl MatchingTasks for IntMsgs {
3591 fn get_all_task_ids() -> &'static [&'static str] {
3592 &[]
3593 }
3594 }
3595
3596 impl CuListZeroedInit for IntMsgs {
3597 fn init_zeroed(&mut self) {}
3598 }
3599
3600 #[cfg(not(feature = "async-cl-io"))]
3601 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
3602 struct DistributedIntMsgs(i32);
3603
3604 #[cfg(not(feature = "async-cl-io"))]
3605 impl ErasedCuStampedDataSet for DistributedIntMsgs {
3606 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
3607 Vec::new()
3608 }
3609 }
3610
3611 #[cfg(not(feature = "async-cl-io"))]
3612 impl MatchingTasks for DistributedIntMsgs {
3613 fn get_all_task_ids() -> &'static [&'static str] {
3614 &[]
3615 }
3616 }
3617
3618 #[cfg(not(feature = "async-cl-io"))]
3619 impl CuListZeroedInit for DistributedIntMsgs {
3620 const DISTRIBUTED: bool = true;
3621
3622 fn init_zeroed(&mut self) {}
3623 }
3624
3625 #[cfg(feature = "std")]
3626 fn tasks_instanciator(
3627 all_instances_configs: Vec<Option<&ComponentConfig>>,
3628 _resources: &mut ResourceManager,
3629 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
3630 ) -> CuResult<Tasks> {
3631 Ok((
3632 TestSource::new(all_instances_configs[0], ())?,
3633 TestSink::new(all_instances_configs[1], ())?,
3634 ))
3635 }
3636
3637 #[cfg(not(feature = "std"))]
3638 fn tasks_instanciator(
3639 all_instances_configs: Vec<Option<&ComponentConfig>>,
3640 _resources: &mut ResourceManager,
3641 ) -> CuResult<Tasks> {
3642 Ok((
3643 TestSource::new(all_instances_configs[0], ())?,
3644 TestSink::new(all_instances_configs[1], ())?,
3645 ))
3646 }
3647
3648 fn monitor_instanciator(
3649 _config: &CuConfig,
3650 metadata: CuMonitoringMetadata,
3651 runtime: CuMonitoringRuntime,
3652 ) -> NoMonitor {
3653 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
3654 }
3655
3656 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
3657 Ok(())
3658 }
3659
3660 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
3661 Ok(ResourceManager::new(&[]))
3662 }
3663
3664 #[derive(Debug)]
3665 struct FakeWriter {}
3666
3667 impl<E: Encode> WriteStream<E> for FakeWriter {
3668 fn log(&mut self, _obj: &E) -> CuResult<()> {
3669 Ok(())
3670 }
3671 }
3672
3673 #[cfg(not(feature = "async-cl-io"))]
3674 #[derive(Debug)]
3675 struct RecordingSyncWriter {
3676 ids: Arc<Mutex<Vec<u64>>>,
3677 last_log_bytes: usize,
3678 fail_on: Option<u64>,
3679 }
3680
3681 #[cfg(not(feature = "async-cl-io"))]
3682 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
3683 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
3684 self.ids.lock().unwrap().push(culist.id);
3685 if self.fail_on == Some(culist.id) {
3686 return Err(CuError::from(format!(
3687 "logger failed for CopperList #{}",
3688 culist.id
3689 )));
3690 }
3691 Ok(())
3692 }
3693
3694 fn last_log_bytes(&self) -> Option<usize> {
3695 Some(self.last_log_bytes)
3696 }
3697 }
3698
3699 #[cfg(feature = "std")]
3700 #[derive(Debug)]
3701 struct RecordingSemanticSink {
3702 ids: Arc<Mutex<Vec<u64>>>,
3703 }
3704
3705 #[cfg(feature = "std")]
3706 impl WriteStream<CopperList<IntMsgs>> for RecordingSemanticSink {
3707 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
3708 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3709 self.ids.lock().unwrap().push(culist.id);
3710 Ok(())
3711 }
3712
3713 fn last_log_bytes(&self) -> Option<usize> {
3714 Some(23)
3715 }
3716 }
3717
3718 #[cfg(feature = "std")]
3719 impl WriteStream<CopperList<Msgs>> for RecordingSemanticSink {
3720 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3721 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3722 self.ids.lock().unwrap().push(culist.id);
3723 Ok(())
3724 }
3725
3726 fn last_log_bytes(&self) -> Option<usize> {
3727 Some(23)
3728 }
3729 }
3730
3731 #[cfg(feature = "std")]
3732 #[derive(Debug)]
3733 struct RecordingKeyFrameSink {
3734 ids: Arc<Mutex<Vec<u64>>>,
3735 }
3736
3737 #[cfg(feature = "std")]
3738 impl WriteStream<KeyFrame> for RecordingKeyFrameSink {
3739 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
3740 self.ids.lock().unwrap().push(keyframe.culistid);
3741 Ok(())
3742 }
3743
3744 fn last_log_bytes(&self) -> Option<usize> {
3745 Some(29)
3746 }
3747 }
3748
3749 #[test]
3750 fn test_runtime_instantiation() {
3751 let mut config = CuConfig::default();
3752 let graph = config.get_graph_mut(None).unwrap();
3753 graph.add_node(Node::new("a", "TestSource")).unwrap();
3754 graph.add_node(Node::new("b", "TestSink")).unwrap();
3755 graph.connect(0, 1, "()").unwrap();
3756 let runtime: CuResult<TestRuntime> =
3757 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3758 RobotClock::default(),
3759 &config,
3760 crate::config::DEFAULT_MISSION_ID,
3761 CuRuntimeParts::new(
3762 tasks_instanciator,
3763 &[],
3764 &[],
3765 #[cfg(feature = "std")]
3766 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3767 monitor_instanciator,
3768 bridges_instanciator,
3769 ),
3770 FakeWriter {},
3771 FakeWriter {},
3772 OutputRequirements::new(true, true),
3773 )
3774 .try_with_resources_instantiator(resources_instanciator)
3775 .and_then(|builder| builder.build());
3776 assert!(runtime.is_ok());
3777 }
3778
3779 #[cfg(feature = "std")]
3780 #[test]
3781 fn downstream_requirements_are_independent_from_local_logging() {
3782 let mut config = CuConfig::default();
3783 config.logging = Some(crate::config::LoggingConfig {
3784 enable_task_logging: false,
3785 enable_keyframe_logging: false,
3786 keyframe_interval: Some(1),
3787 ..Default::default()
3788 });
3789 let graph = config.get_graph_mut(None).unwrap();
3790 graph.add_node(Node::new("a", "TestSource")).unwrap();
3791 graph.add_node(Node::new("b", "TestSink")).unwrap();
3792 graph.connect(0, 1, "()").unwrap();
3793
3794 let copperlist_ids = Arc::new(Mutex::new(Vec::new()));
3795 let keyframe_ids = Arc::new(Mutex::new(Vec::new()));
3796 let mut runtime: TestRuntime =
3797 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3798 RobotClock::default(),
3799 &config,
3800 crate::config::DEFAULT_MISSION_ID,
3801 CuRuntimeParts::new(
3802 tasks_instanciator,
3803 &[],
3804 &[],
3805 #[cfg(feature = "std")]
3806 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3807 monitor_instanciator,
3808 bridges_instanciator,
3809 ),
3810 RecordingSemanticSink {
3811 ids: copperlist_ids.clone(),
3812 },
3813 RecordingKeyFrameSink {
3814 ids: keyframe_ids.clone(),
3815 },
3816 OutputRequirements::new(true, false).union(OutputRequirements::new(false, true)),
3817 )
3818 .try_with_resources_instantiator(resources_instanciator)
3819 .and_then(|builder| builder.build())
3820 .unwrap();
3821
3822 let copperlist = runtime.copperlists_manager.create().unwrap();
3823 copperlist.change_state(CopperListState::Processing);
3824 runtime.copperlists_manager.end_of_processing(0).unwrap();
3825 runtime.copperlists_manager.finish_pending().unwrap();
3826
3827 runtime
3828 .keyframes_manager
3829 .try_reset(0, &runtime.clock)
3830 .unwrap();
3831 runtime.keyframes_manager.end_of_processing(0).unwrap();
3832 runtime.keyframes_manager.finish_pending().unwrap();
3833
3834 assert_eq!(*copperlist_ids.lock().unwrap(), vec![0]);
3835 assert_eq!(*keyframe_ids.lock().unwrap(), vec![0]);
3836 }
3837
3838 #[test]
3839 fn test_rate_target_period_rejects_zero() {
3840 let err = rate_target_period(0).expect_err("zero rate target should fail");
3841 assert!(
3842 err.to_string()
3843 .contains("Runtime rate target cannot be zero"),
3844 "unexpected error: {err}"
3845 );
3846 }
3847
3848 #[test]
3849 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
3850 let (clock, mock) = RobotClock::mock();
3851 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3852 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
3853
3854 mock.set_value(10_000_000);
3855 limiter.mark_tick(&clock);
3856
3857 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
3858 }
3859
3860 #[test]
3861 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
3862 let (clock, mock) = RobotClock::mock();
3863 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3864
3865 mock.set_value(35_000_000);
3866 limiter.mark_tick(&clock);
3867
3868 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
3869 }
3870
3871 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
3872 #[test]
3873 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
3874 let (clock, _) = RobotClock::mock();
3875 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
3876 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
3877
3878 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
3879 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
3880 }
3881
3882 #[cfg(not(feature = "async-cl-io"))]
3883 #[test]
3884 fn test_copperlists_manager_lifecycle() {
3885 let mut config = CuConfig::default();
3886 let graph = config.get_graph_mut(None).unwrap();
3887 graph.add_node(Node::new("a", "TestSource")).unwrap();
3888 graph.add_node(Node::new("b", "TestSink")).unwrap();
3889 graph.connect(0, 1, "()").unwrap();
3890
3891 let mut runtime: TestRuntime =
3892 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3893 RobotClock::default(),
3894 &config,
3895 crate::config::DEFAULT_MISSION_ID,
3896 CuRuntimeParts::new(
3897 tasks_instanciator,
3898 &[],
3899 &[],
3900 #[cfg(feature = "std")]
3901 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3902 monitor_instanciator,
3903 bridges_instanciator,
3904 ),
3905 FakeWriter {},
3906 FakeWriter {},
3907 OutputRequirements::new(true, true),
3908 )
3909 .try_with_resources_instantiator(resources_instanciator)
3910 .and_then(|builder| builder.build())
3911 .unwrap();
3912
3913 {
3915 let copperlists = &mut runtime.copperlists_manager;
3916 let culist0 = copperlists
3917 .create()
3918 .expect("Ran out of space for copper lists");
3919 let id = culist0.id;
3920 assert_eq!(id, 0);
3921 culist0.change_state(CopperListState::Processing);
3922 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3923 }
3924
3925 {
3926 let copperlists = &mut runtime.copperlists_manager;
3927 let culist1 = copperlists
3928 .create()
3929 .expect("Ran out of space for copper lists");
3930 let id = culist1.id;
3931 assert_eq!(id, 1);
3932 culist1.change_state(CopperListState::Processing);
3933 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3934 }
3935
3936 {
3937 let copperlists = &mut runtime.copperlists_manager;
3938 let culist2 = copperlists.create();
3939 assert!(culist2.is_err());
3940 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3941 let _ = copperlists.end_of_processing(1);
3943 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3944 }
3945
3946 {
3948 let copperlists = &mut runtime.copperlists_manager;
3949 let culist2 = copperlists
3950 .create()
3951 .expect("Ran out of space for copper lists");
3952 let id = culist2.id;
3953 assert_eq!(id, 2);
3954 culist2.change_state(CopperListState::Processing);
3955 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3956 let _ = copperlists.end_of_processing(0);
3958 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3960
3961 let _ = copperlists.end_of_processing(2);
3963 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3966 }
3967 }
3968
3969 #[cfg(not(feature = "async-cl-io"))]
3970 #[test]
3971 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
3972 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3973
3974 assert_eq!(copperlists.next_cl_id(), 0);
3975 assert_eq!(copperlists.last_cl_id(), 0);
3976 assert!(copperlists.peek().is_none());
3977
3978 {
3979 let culist = copperlists.create().unwrap();
3980 culist.msgs.0 = 11;
3981 assert_eq!(culist.id, 0);
3982 assert_eq!(culist.get_state(), CopperListState::Initialized);
3983 }
3984
3985 assert_eq!(copperlists.next_cl_id(), 1);
3986 assert_eq!(copperlists.last_cl_id(), 0);
3987 let peeked = copperlists.peek().unwrap();
3988 assert_eq!(peeked.id, 0);
3989 assert_eq!(peeked.msgs.0, 11);
3990 assert_eq!(peeked.get_state(), CopperListState::Initialized);
3991 }
3992
3993 #[cfg(not(feature = "async-cl-io"))]
3994 #[test]
3995 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
3996 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3997
3998 {
3999 let culist = copperlists.create().unwrap();
4000 culist.msgs.0 = 41;
4001 culist.change_state(CopperListState::Processing);
4002 assert_eq!(culist.id, 0);
4003 }
4004
4005 copperlists.end_of_processing(0).unwrap();
4006 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4007
4008 let reused = copperlists.create().unwrap();
4009 assert_eq!(reused.id, 1);
4010 assert_eq!(reused.get_state(), CopperListState::Initialized);
4011 assert_eq!(reused.msgs.0, 41);
4012 }
4013
4014 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
4015 #[test]
4016 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
4017 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
4018 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
4019
4020 {
4021 let culist = copperlists.create().unwrap();
4022 culist.msgs.0 = 10;
4023 culist.change_state(CopperListState::Processing);
4024 }
4025 {
4026 let culist = copperlists.create().unwrap();
4027 culist.msgs.0 = 20;
4028 culist.change_state(CopperListState::Processing);
4029 }
4030
4031 let _ = copperlists.end_of_processing(99);
4032 }
4033
4034 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
4035 #[test]
4036 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
4037 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
4038 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4039
4040 {
4041 let culist = copperlists.create().unwrap();
4042 culist.msgs.0 = 10;
4043 assert_eq!(culist.get_state(), CopperListState::Initialized);
4044 }
4045
4046 let _ = copperlists.end_of_processing(0);
4047 }
4048
4049 #[cfg(not(feature = "async-cl-io"))]
4050 #[test]
4051 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
4052 let ids = Arc::new(Mutex::new(Vec::new()));
4053 let mut copperlists =
4054 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
4055 ids: ids.clone(),
4056 last_log_bytes: 17,
4057 fail_on: None,
4058 })))
4059 .unwrap();
4060
4061 {
4062 let culist = copperlists.create().unwrap();
4063 culist.msgs.0 = 10;
4064 culist.change_state(CopperListState::Processing);
4065 }
4066 {
4067 let culist = copperlists.create().unwrap();
4068 culist.msgs.0 = 20;
4069 culist.change_state(CopperListState::Processing);
4070 }
4071
4072 copperlists.end_of_processing(0).unwrap();
4073 assert!(ids.lock().unwrap().is_empty());
4074 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
4075
4076 copperlists.end_of_processing(1).unwrap();
4077
4078 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
4079 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
4080 }
4081
4082 #[cfg(not(feature = "async-cl-io"))]
4083 #[test]
4084 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
4085 let ids = Arc::new(Mutex::new(Vec::new()));
4086 let mut copperlists =
4087 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
4088 ids: ids.clone(),
4089 last_log_bytes: 17,
4090 fail_on: None,
4091 })))
4092 .unwrap();
4093 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
4094
4095 {
4096 let culist = copperlists.create().unwrap();
4097 culist.msgs.0 = 10;
4098 culist.change_state(CopperListState::Processing);
4099 }
4100
4101 {
4102 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
4103 capture.select_slot(0);
4104 crate::monitoring::record_payload_handle_bytes(32);
4105 }
4106
4107 copperlists.end_of_processing(0).unwrap();
4108
4109 assert_eq!(*ids.lock().unwrap(), vec![0]);
4110 assert_eq!(copperlists.last_encoded_bytes, 17);
4111 assert_eq!(copperlists.last_handle_bytes, 32);
4112 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4113 }
4114
4115 #[cfg(feature = "std")]
4116 #[test]
4117 fn test_sync_manager_accepts_nonserializing_semantic_sink() {
4118 let ids = Arc::new(Mutex::new(Vec::new()));
4119 let mut copperlists =
4120 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSemanticSink {
4121 ids: ids.clone(),
4122 })))
4123 .unwrap();
4124
4125 let culist = copperlists.create().unwrap();
4126 culist.change_state(CopperListState::Processing);
4127 copperlists.end_of_processing(0).unwrap();
4128
4129 assert_eq!(*ids.lock().unwrap(), vec![0]);
4130 assert_eq!(copperlists.last_encoded_bytes, 23);
4131 assert_eq!(copperlists.last_handle_bytes, 0);
4132 }
4133
4134 #[cfg(feature = "std")]
4135 #[test]
4136 fn test_keyframe_manager_accepts_nonserializing_semantic_sink() {
4137 let ids = Arc::new(Mutex::new(Vec::new()));
4138 let mut keyframes = KeyFramesManager::new(
4139 Some(Box::new(RecordingKeyFrameSink { ids: ids.clone() })),
4140 1,
4141 )
4142 .unwrap();
4143
4144 keyframes.try_reset(7, &RobotClock::default()).unwrap();
4145 keyframes.end_of_processing(7).unwrap();
4146 keyframes.finish_pending().unwrap();
4147
4148 assert_eq!(*ids.lock().unwrap(), vec![7]);
4149 assert_eq!(keyframes.last_encoded_bytes, 29);
4150 }
4151
4152 #[cfg(not(feature = "async-cl-io"))]
4153 #[test]
4154 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
4155 let ids = Arc::new(Mutex::new(Vec::new()));
4156 let mut copperlists =
4157 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
4158 ids: ids.clone(),
4159 last_log_bytes: 17,
4160 fail_on: Some(0),
4161 })))
4162 .unwrap();
4163
4164 {
4165 let culist = copperlists.create().unwrap();
4166 culist.change_state(CopperListState::Processing);
4167 }
4168
4169 let err = copperlists.end_of_processing(0).unwrap_err();
4170
4171 assert!(
4172 err.to_string().contains("logger failed for CopperList #0"),
4173 "unexpected error: {err}"
4174 );
4175 assert_eq!(*ids.lock().unwrap(), vec![0]);
4176 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
4177 assert_eq!(copperlists.last_encoded_bytes, 0);
4178 assert_eq!(copperlists.last_handle_bytes, 0);
4179
4180 let peeked = copperlists.peek().unwrap();
4181 assert_eq!(peeked.id, 0);
4182 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
4183 }
4184
4185 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
4186 #[test]
4187 #[should_panic(expected = "sync slot completion expected CopperList #7 to be Processing")]
4188 fn test_sync_submit_slot_wrong_state_panics_in_debug() {
4189 let mut copperlists = SyncCopperListsManager::<DistributedIntMsgs, 1>::new(None).unwrap();
4190 let mut culist = copperlists.acquire_iteration_slot().unwrap();
4191 culist.reset(7);
4192
4193 let _ = copperlists.submit_slot(culist);
4194 }
4195
4196 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4197 #[derive(Debug, Default)]
4198 struct RecordingWriter {
4199 ids: Arc<Mutex<Vec<u64>>>,
4200 }
4201
4202 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4203 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
4204 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
4205 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
4206 self.ids.lock().unwrap().push(culist.id);
4207 std::thread::sleep(std::time::Duration::from_millis(2));
4208 Ok(())
4209 }
4210 }
4211
4212 #[cfg(feature = "std")]
4213 #[derive(Debug)]
4214 struct OrderedCopperListSink {
4215 events: Arc<Mutex<Vec<&'static str>>>,
4216 }
4217
4218 #[cfg(feature = "std")]
4219 impl WriteStream<CopperList<Msgs>> for OrderedCopperListSink {
4220 fn log(&mut self, _culist: &CopperList<Msgs>) -> CuResult<()> {
4221 self.events.lock().unwrap().push("copperlist");
4222 Ok(())
4223 }
4224 }
4225
4226 #[cfg(feature = "std")]
4227 #[derive(Debug)]
4228 struct OrderedKeyFrameSink {
4229 events: Arc<Mutex<Vec<&'static str>>>,
4230 }
4231
4232 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4233 #[derive(Debug)]
4234 struct BlockingKeyFrameSink {
4235 events: Arc<Mutex<Vec<&'static str>>>,
4236 started: SyncSender<()>,
4237 release: Arc<Mutex<Receiver<()>>>,
4238 }
4239
4240 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4241 impl WriteStream<KeyFrame> for BlockingKeyFrameSink {
4242 fn log(&mut self, _keyframe: &KeyFrame) -> CuResult<()> {
4243 self.events.lock().unwrap().push("keyframe");
4244 self.started
4245 .send(())
4246 .map_err(|_| CuError::from("failed to signal keyframe writer start"))?;
4247 self.release
4248 .lock()
4249 .unwrap()
4250 .recv()
4251 .map_err(|_| CuError::from("failed to release keyframe writer"))
4252 }
4253 }
4254
4255 #[cfg(feature = "std")]
4256 impl WriteStream<KeyFrame> for OrderedKeyFrameSink {
4257 fn log(&mut self, _keyframe: &KeyFrame) -> CuResult<()> {
4258 self.events.lock().unwrap().push("keyframe");
4259 Ok(())
4260 }
4261 }
4262
4263 #[cfg(feature = "std")]
4264 #[test]
4265 fn slot_output_writes_keyframe_then_copperlist_before_recycling() {
4266 let events = Arc::new(Mutex::new(Vec::new()));
4267 let mut manager = SyncCopperListsManager::<Msgs, 1>::new_with_keyframes(
4268 Some(Box::new(OrderedCopperListSink {
4269 events: Arc::clone(&events),
4270 })),
4271 Some(Box::new(OrderedKeyFrameSink {
4272 events: Arc::clone(&events),
4273 })),
4274 )
4275 .unwrap();
4276 let mut slot = allocate_slots::<Msgs>(1).pop().unwrap();
4277 slot.prepare_keyframes(&[128]);
4278 slot.reset(0);
4279 slot.keyframe_mut()
4280 .unwrap()
4281 .reset_distributed(0, CuTime::default(), 1);
4282 let (capture_ptr, capture_len) = slot.keyframe_capture();
4283 unsafe {
4284 KeyFrame::freeze_region(capture_ptr, capture_len, 0, &SnapshotValue(42)).unwrap();
4285 }
4286 slot.change_state(CopperListState::Processing);
4287 let index = slot.index();
4288 let generation = slot.generation();
4289
4290 let recycled = match manager.submit_slot(slot).unwrap() {
4291 OwnedCopperListSubmission::Recycled(slot) => slot,
4292 OwnedCopperListSubmission::Pending => panic!("sync output retained slot"),
4293 };
4294
4295 assert_eq!(*events.lock().unwrap(), vec!["keyframe", "copperlist"]);
4296 assert_eq!(recycled.index(), index);
4297 assert_eq!(recycled.generation(), generation);
4298 assert_eq!(recycled.get_state(), CopperListState::Free);
4299 }
4300
4301 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4302 #[test]
4303 fn async_slot_is_reclaimed_only_after_keyframe_and_copperlist_output() {
4304 let events = Arc::new(Mutex::new(Vec::new()));
4305 let (started_tx, started_rx) = sync_channel(1);
4306 let (release_tx, release_rx) = sync_channel(1);
4307 let mut manager = AsyncCopperListsManager::<Msgs, 2>::new_with_keyframes(
4308 Some(Box::new(OrderedCopperListSink {
4309 events: Arc::clone(&events),
4310 })),
4311 Some(Box::new(BlockingKeyFrameSink {
4312 events: Arc::clone(&events),
4313 started: started_tx,
4314 release: Arc::new(Mutex::new(release_rx)),
4315 })),
4316 )
4317 .unwrap();
4318 manager.prepare_keyframes(&[128]);
4319 let mut slot = manager.acquire_iteration_slot().unwrap();
4320 slot.reset(0);
4321 slot.keyframe_mut()
4322 .unwrap()
4323 .reset_distributed(0, CuTime::default(), 1);
4324 let (capture_ptr, capture_len) = slot.keyframe_capture();
4325 unsafe {
4326 KeyFrame::freeze_region(capture_ptr, capture_len, 0, &SnapshotValue(42)).unwrap();
4327 }
4328 slot.change_state(CopperListState::Processing);
4329 let index = slot.index();
4330 let generation = slot.generation();
4331
4332 assert!(matches!(
4333 manager.submit_slot(slot).unwrap(),
4334 OwnedCopperListSubmission::Pending
4335 ));
4336 started_rx
4337 .recv_timeout(std::time::Duration::from_secs(1))
4338 .unwrap();
4339 assert!(manager.try_reclaim_slot().unwrap().is_none());
4340 assert_eq!(*events.lock().unwrap(), vec!["keyframe"]);
4341
4342 release_tx.send(()).unwrap();
4343 let recycled = manager.wait_reclaim_slot().unwrap();
4344 assert_eq!(*events.lock().unwrap(), vec!["keyframe", "copperlist"]);
4345 assert_eq!(recycled.index(), index);
4346 assert_eq!(recycled.generation(), generation);
4347 assert_eq!(recycled.get_state(), CopperListState::Free);
4348 }
4349
4350 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4351 #[derive(Debug)]
4352 struct BlockingWriter {
4353 ids: Arc<Mutex<Vec<u64>>>,
4354 started: SyncSender<()>,
4355 release: Arc<Mutex<Receiver<()>>>,
4356 }
4357
4358 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4359 impl WriteStream<CopperList<Msgs>> for BlockingWriter {
4360 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
4361 self.ids.lock().unwrap().push(culist.id);
4362 self.started
4363 .send(())
4364 .map_err(|_| CuError::from("failed to signal blocking writer start"))?;
4365 self.release
4366 .lock()
4367 .unwrap()
4368 .recv()
4369 .map_err(|_| CuError::from("failed to release blocking writer"))
4370 }
4371 }
4372
4373 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4374 #[derive(Debug)]
4375 struct BlockingKeyFrameWriter {
4376 ids: Arc<Mutex<Vec<u64>>>,
4377 started: SyncSender<()>,
4378 release: Arc<Mutex<Receiver<()>>>,
4379 }
4380
4381 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4382 impl WriteStream<KeyFrame> for BlockingKeyFrameWriter {
4383 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
4384 self.ids.lock().unwrap().push(keyframe.culistid);
4385 self.started
4386 .send(())
4387 .map_err(|_| CuError::from("failed to signal blocking keyframe writer start"))?;
4388 self.release
4389 .lock()
4390 .unwrap()
4391 .recv()
4392 .map_err(|_| CuError::from("failed to release blocking keyframe writer"))
4393 }
4394 }
4395
4396 #[test]
4397 fn disabled_keyframes_allocate_no_capture_buffers() {
4398 let keyframes = KeyFramesManager::new(None, 1).unwrap();
4399
4400 assert!(keyframes.inner.is_none());
4401 assert!(!keyframes.captures_keyframe(0));
4402 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4403 {
4404 assert!(keyframes.spares.is_empty());
4405 assert!(keyframes.pending_producer.is_none());
4406 assert!(keyframes.worker_handle.is_none());
4407 }
4408 }
4409
4410 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
4411 #[test]
4412 fn disabled_keyframe_manager_allocates_nothing() {
4413 let allocations = crate::monitoring::ScopedAllocCounter::new();
4414 let keyframes = KeyFramesManager::new(None, 1).unwrap();
4415
4416 assert_eq!(allocations.allocated(), 0);
4417 assert!(keyframes.inner.is_none());
4418 }
4419
4420 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4421 #[test]
4422 fn saturated_keyframe_handoff_skips_capture_before_freezing() {
4423 let ids = Arc::new(Mutex::new(Vec::new()));
4424 let (started_tx, started_rx) = sync_channel(1);
4425 let (release_tx, release_rx) = sync_channel(1);
4426 let mut keyframes = KeyFramesManager::new(
4427 Some(Box::new(BlockingKeyFrameWriter {
4428 ids: ids.clone(),
4429 started: started_tx,
4430 release: Arc::new(Mutex::new(release_rx)),
4431 })),
4432 1,
4433 )
4434 .unwrap();
4435 let freeze_calls = Cell::new(0);
4436 let snapshot = CountingSnapshot {
4437 calls: &freeze_calls,
4438 value: 42,
4439 fail: false,
4440 };
4441 keyframes.begin_capture_preallocation();
4442 keyframes.include_capture_capacity(&snapshot).unwrap();
4443 keyframes.finish_capture_preallocation().unwrap();
4444 freeze_calls.set(0);
4445
4446 keyframes.try_reset(0, &RobotClock::default()).unwrap();
4447 assert_ne!(keyframes.freeze_task(0, &snapshot).unwrap(), 0);
4448 keyframes.end_of_processing(0).unwrap();
4449 started_rx
4450 .recv_timeout(std::time::Duration::from_secs(1))
4451 .unwrap();
4452
4453 keyframes.try_reset(1, &RobotClock::default()).unwrap();
4454 assert_ne!(keyframes.freeze_task(1, &snapshot).unwrap(), 0);
4455 keyframes.end_of_processing(1).unwrap();
4456
4457 keyframes.try_reset(2, &RobotClock::default()).unwrap();
4458 assert_eq!(keyframes.freeze_task(2, &snapshot).unwrap(), 0);
4459 keyframes.end_of_processing(2).unwrap();
4460
4461 assert_eq!(freeze_calls.get(), 2);
4462 assert_eq!(keyframes.dropped_keyframes_total(), 1);
4463 assert_eq!(*ids.lock().unwrap(), vec![0]);
4464
4465 release_tx.send(()).unwrap();
4466 started_rx
4467 .recv_timeout(std::time::Duration::from_secs(1))
4468 .unwrap();
4469 release_tx.send(()).unwrap();
4470 keyframes.finish_pending().unwrap();
4471 assert_eq!(*ids.lock().unwrap(), vec![0, 1]);
4472 }
4473
4474 #[cfg(feature = "std")]
4475 #[test]
4476 fn test_discard_current_preserves_completed_output_and_reuses_slot() {
4477 let events = Arc::new(Mutex::new(Vec::new()));
4478 let mut copperlists =
4479 CopperListsManager::<Msgs, 3>::new(Some(Box::new(OrderedCopperListSink {
4480 events: events.clone(),
4481 })))
4482 .unwrap();
4483
4484 let culist = copperlists.create().unwrap();
4485 culist.change_state(CopperListState::Processing);
4486 copperlists.end_of_processing(0).unwrap();
4487
4488 let culist = copperlists.create().unwrap();
4489 assert_eq!(culist.id, 1);
4490 culist.change_state(CopperListState::Processing);
4491 copperlists.discard_current();
4492 copperlists.discard_current();
4493 copperlists.finish_pending().unwrap();
4494 assert!(copperlists.peek().is_none());
4495 assert_eq!(copperlists.available_copper_lists().unwrap(), 3);
4496 assert_eq!(*events.lock().unwrap(), vec!["copperlist"]);
4497
4498 let culist = copperlists.create().unwrap();
4500 assert_eq!(culist.id, 2);
4501 culist.change_state(CopperListState::Processing);
4502 copperlists.end_of_processing(2).unwrap();
4503 copperlists.finish_pending().unwrap();
4504 assert_eq!(copperlists.available_copper_lists().unwrap(), 3);
4505 assert_eq!(*events.lock().unwrap(), vec!["copperlist", "copperlist"]);
4506 }
4507
4508 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4509 #[test]
4510 fn test_async_copperlists_manager_flushes_in_order() {
4511 let ids = Arc::new(Mutex::new(Vec::new()));
4512 let mut copperlists = CopperListsManager::<Msgs, 5>::new(Some(Box::new(RecordingWriter {
4513 ids: ids.clone(),
4514 })))
4515 .unwrap();
4516
4517 for expected_id in 0..4 {
4518 let culist = copperlists.create().unwrap();
4519 assert_eq!(culist.id, expected_id);
4520 culist.change_state(CopperListState::Processing);
4521 copperlists.end_of_processing(expected_id).unwrap();
4522 }
4523
4524 copperlists.finish_pending().unwrap();
4525 assert_eq!(copperlists.available_copper_lists().unwrap(), 5);
4526 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
4527 assert_eq!(copperlists.dropped_copperlists_total(), 0);
4528 }
4529
4530 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4531 #[test]
4532 fn test_async_handoff_drops_without_exhausting_execution_slot() {
4533 let ids = Arc::new(Mutex::new(Vec::new()));
4534 let (started_tx, started_rx) = sync_channel(1);
4535 let (release_tx, release_rx) = sync_channel(1);
4536 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
4537 ids: ids.clone(),
4538 started: started_tx,
4539 release: Arc::new(Mutex::new(release_rx)),
4540 })))
4541 .unwrap();
4542
4543 let first = copperlists.create().unwrap();
4544 first.change_state(CopperListState::Processing);
4545 copperlists.end_of_processing(0).unwrap();
4546 started_rx
4547 .recv_timeout(std::time::Duration::from_secs(1))
4548 .unwrap();
4549
4550 let second = copperlists.create().unwrap();
4551 second.change_state(CopperListState::Processing);
4552 copperlists.end_of_processing(1).unwrap();
4553
4554 assert_eq!(copperlists.dropped_copperlists_total(), 1);
4555 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4556 assert_eq!(*ids.lock().unwrap(), vec![0]);
4557
4558 release_tx.send(()).unwrap();
4559 copperlists.finish_pending().unwrap();
4560 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
4561 }
4562
4563 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4564 #[test]
4565 fn test_async_slot_handoff_returns_dropped_copperlist_to_caller() {
4566 let ids = Arc::new(Mutex::new(Vec::new()));
4567 let (started_tx, started_rx) = sync_channel(1);
4568 let (release_tx, release_rx) = sync_channel(1);
4569 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
4570 ids: ids.clone(),
4571 started: started_tx,
4572 release: Arc::new(Mutex::new(release_rx)),
4573 })))
4574 .unwrap();
4575
4576 let mut first = copperlists.acquire_iteration_slot().unwrap();
4577 first.reset(0);
4578 first.change_state(CopperListState::Processing);
4579 assert!(matches!(
4580 copperlists.submit_slot(first).unwrap(),
4581 OwnedCopperListSubmission::Pending
4582 ));
4583 started_rx
4584 .recv_timeout(std::time::Duration::from_secs(1))
4585 .unwrap();
4586
4587 let mut second = copperlists.acquire_iteration_slot().unwrap();
4588 second.reset(1);
4589 second.change_state(CopperListState::Processing);
4590 let recycled = match copperlists.submit_slot(second).unwrap() {
4591 OwnedCopperListSubmission::Recycled(culist) => culist,
4592 OwnedCopperListSubmission::Pending => panic!("saturated handoff accepted CopperList"),
4593 };
4594
4595 assert_eq!(recycled.id, 1);
4596 assert_eq!(recycled.get_state(), CopperListState::Free);
4597 assert_eq!(copperlists.dropped_copperlists_total(), 1);
4598 assert_eq!(*ids.lock().unwrap(), vec![0]);
4599
4600 release_tx.send(()).unwrap();
4601 let completed = copperlists.finish_pending_slots().unwrap();
4602 assert_eq!(completed.len(), 1);
4603 assert_eq!(completed[0].id, 0);
4604 }
4605
4606 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4607 #[test]
4608 fn test_async_output_requires_a_spare_execution_slot() {
4609 let error =
4610 match CopperListsManager::<Msgs, 1>::new(Some(Box::new(RecordingWriter::default()))) {
4611 Ok(_) => panic!("async output unexpectedly accepted a single CopperList slot"),
4612 Err(error) => error,
4613 };
4614
4615 assert!(error.to_string().contains("at least two CopperList slots"));
4616 }
4617
4618 #[cfg(all(feature = "std", feature = "async-cl-io"))]
4619 #[test]
4620 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
4621 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4622
4623 {
4624 let culist = copperlists.create().unwrap();
4625 assert_eq!(culist.id, 0);
4626 assert_eq!(culist.get_state(), CopperListState::Initialized);
4627 culist.msgs.0 = 41;
4628 culist.change_state(CopperListState::Processing);
4629 }
4630
4631 copperlists.end_of_processing(0).unwrap();
4632 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
4633
4634 let reused = copperlists.create().unwrap();
4635 assert_eq!(reused.id, 1);
4636 assert_eq!(reused.get_state(), CopperListState::Initialized);
4637 assert_eq!(reused.msgs.0, 41);
4638 }
4639
4640 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
4641 #[test]
4642 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
4643 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
4644 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
4645
4646 let culist = copperlists.create().unwrap();
4647 assert_eq!(culist.id, 0);
4648 assert_eq!(culist.get_state(), CopperListState::Initialized);
4649
4650 let _ = copperlists.end_of_processing(0);
4651 }
4652
4653 #[test]
4654 fn test_runtime_task_input_order() {
4655 let mut config = CuConfig::default();
4656 let graph = config.get_graph_mut(None).unwrap();
4657 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
4658 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
4659 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
4660
4661 assert_eq!(src1_id, 0);
4662 assert_eq!(src2_id, 1);
4663
4664 let src1_type = "src1_type";
4666 let src2_type = "src2_type";
4667 graph.connect(src2_id, sink_id, src2_type).unwrap();
4668 graph.connect(src1_id, sink_id, src1_type).unwrap();
4669
4670 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
4671 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
4672 assert_eq!(src1_edge_id, 1);
4675 assert_eq!(src2_edge_id, 0);
4676
4677 let runtime = compute_runtime_plan(graph).unwrap();
4678 let sink_step = runtime
4679 .steps
4680 .iter()
4681 .find_map(|step| match step {
4682 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
4683 _ => None,
4684 })
4685 .unwrap();
4686
4687 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
4690 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
4691 }
4692
4693 #[test]
4694 fn test_runtime_output_ports_unique_ordered() {
4695 let mut config = CuConfig::default();
4696 let graph = config.get_graph_mut(None).unwrap();
4697 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4698 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
4699 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
4700 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
4701 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
4702
4703 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
4704 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
4705 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
4706 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
4707
4708 let runtime = compute_runtime_plan(graph).unwrap();
4709 let src_step = runtime
4710 .steps
4711 .iter()
4712 .find_map(|step| match step {
4713 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4714 _ => None,
4715 })
4716 .unwrap();
4717
4718 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4719 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
4720
4721 let dst_a_step = runtime
4722 .steps
4723 .iter()
4724 .find_map(|step| match step {
4725 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
4726 _ => None,
4727 })
4728 .unwrap();
4729 let dst_b_step = runtime
4730 .steps
4731 .iter()
4732 .find_map(|step| match step {
4733 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
4734 _ => None,
4735 })
4736 .unwrap();
4737 let dst_a2_step = runtime
4738 .steps
4739 .iter()
4740 .find_map(|step| match step {
4741 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
4742 _ => None,
4743 })
4744 .unwrap();
4745 let dst_c_step = runtime
4746 .steps
4747 .iter()
4748 .find_map(|step| match step {
4749 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
4750 _ => None,
4751 })
4752 .unwrap();
4753
4754 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
4755 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
4756 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
4757 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
4758 }
4759
4760 #[test]
4761 fn test_runtime_plan_distinguishes_channel_distinct_outputs() {
4762 let mut config = CuConfig::default();
4763 let graph = config.get_graph_mut(None).unwrap();
4764 let src_id = graph.add_node(Node::new("cam", "Cam")).unwrap();
4765 let sink_id = graph.add_node(Node::new("sink", "Sink")).unwrap();
4766
4767 graph
4771 .connect_ext(
4772 src_id,
4773 sink_id,
4774 "msg::Image",
4775 None,
4776 Some("left".to_string()),
4777 None,
4778 )
4779 .unwrap();
4780 graph
4781 .connect_ext(
4782 src_id,
4783 sink_id,
4784 "msg::Image",
4785 None,
4786 Some("right".to_string()),
4787 None,
4788 )
4789 .unwrap();
4790
4791 let runtime = compute_runtime_plan(graph).unwrap();
4792
4793 let src_step = runtime
4794 .steps
4795 .iter()
4796 .find_map(|step| match step {
4797 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4798 _ => None,
4799 })
4800 .unwrap();
4801 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4802 assert_eq!(output_pack.msg_types, vec!["msg::Image", "msg::Image"]);
4803 assert_eq!(
4804 output_pack.src_channels,
4805 vec![Some("left".to_string()), Some("right".to_string())]
4806 );
4807
4808 let sink_step = runtime
4809 .steps
4810 .iter()
4811 .find_map(|step| match step {
4812 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
4813 _ => None,
4814 })
4815 .unwrap();
4816
4817 assert_eq!(sink_step.input_msg_indices_types.len(), 2);
4818 let ports: Vec<usize> = sink_step
4819 .input_msg_indices_types
4820 .iter()
4821 .map(|input| input.src_port)
4822 .collect();
4823 assert_eq!(ports, vec![0, 1]);
4824 }
4825
4826 #[test]
4827 fn test_runtime_output_ports_fanout_single() {
4828 let mut config = CuConfig::default();
4829 let graph = config.get_graph_mut(None).unwrap();
4830 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4831 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
4832 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
4833
4834 graph.connect(src_id, dst_a_id, "i32").unwrap();
4835 graph.connect(src_id, dst_b_id, "i32").unwrap();
4836
4837 let runtime = compute_runtime_plan(graph).unwrap();
4838 let src_step = runtime
4839 .steps
4840 .iter()
4841 .find_map(|step| match step {
4842 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4843 _ => None,
4844 })
4845 .unwrap();
4846
4847 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4848 assert_eq!(output_pack.msg_types, vec!["i32"]);
4849 }
4850
4851 #[test]
4852 fn test_runtime_output_ports_include_nc_outputs() {
4853 let mut config = CuConfig::default();
4854 let graph = config.get_graph_mut(None).unwrap();
4855 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
4856 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
4857 graph.connect(src_id, dst_id, "msg::A").unwrap();
4858 graph
4859 .get_node_mut(src_id)
4860 .expect("missing source node")
4861 .add_nc_output("msg::B", usize::MAX);
4862
4863 let runtime = compute_runtime_plan(graph).unwrap();
4864 let src_step = runtime
4865 .steps
4866 .iter()
4867 .find_map(|step| match step {
4868 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4869 _ => None,
4870 })
4871 .unwrap();
4872 let dst_step = runtime
4873 .steps
4874 .iter()
4875 .find_map(|step| match step {
4876 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4877 _ => None,
4878 })
4879 .unwrap();
4880
4881 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4882 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4883 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
4884 }
4885
4886 #[test]
4887 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
4888 let txt = r#"(
4889 tasks: [
4890 (id: "src", type: "a"),
4891 (id: "regular", type: "b"),
4892 ],
4893 cnx: [
4894 (src: "src", dst: "regular", msg: "msg::A"),
4895 (src: "regular", dst: "__nc__", msg: "msg::B"),
4896 ]
4897 )"#;
4898 let config = CuConfig::deserialize_ron(txt).unwrap();
4899 let graph = config.get_graph(None).unwrap();
4900 let regular_id = graph.get_node_id_by_name("regular").unwrap();
4901
4902 let runtime = compute_runtime_plan(graph).unwrap();
4903 let regular_step = runtime
4904 .steps
4905 .iter()
4906 .find_map(|step| match step {
4907 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
4908 _ => None,
4909 })
4910 .unwrap();
4911
4912 assert_eq!(regular_step.task_type, CuTaskType::Regular);
4913 assert_eq!(
4914 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
4915 vec!["msg::B"]
4916 );
4917 }
4918
4919 #[test]
4920 fn test_runtime_output_ports_respect_connection_order_with_nc() {
4921 let txt = r#"(
4922 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4923 cnx: [
4924 (src: "src", dst: "__nc__", msg: "msg::A"),
4925 (src: "src", dst: "sink", msg: "msg::B"),
4926 ]
4927 )"#;
4928 let config = CuConfig::deserialize_ron(txt).unwrap();
4929 let graph = config.get_graph(None).unwrap();
4930 let src_id = graph.get_node_id_by_name("src").unwrap();
4931 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4932
4933 let runtime = compute_runtime_plan(graph).unwrap();
4934 let src_step = runtime
4935 .steps
4936 .iter()
4937 .find_map(|step| match step {
4938 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4939 _ => None,
4940 })
4941 .unwrap();
4942 let dst_step = runtime
4943 .steps
4944 .iter()
4945 .find_map(|step| match step {
4946 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4947 _ => None,
4948 })
4949 .unwrap();
4950
4951 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4952 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4953 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4954 }
4955
4956 #[cfg(feature = "std")]
4957 #[test]
4958 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
4959 let txt = r#"(
4960 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4961 cnx: [
4962 (src: "src", dst: "__nc__", msg: "msg::A"),
4963 (src: "src", dst: "sink", msg: "msg::B"),
4964 ]
4965 )"#;
4966 let tmp = tempfile::NamedTempFile::new().unwrap();
4967 std::fs::write(tmp.path(), txt).unwrap();
4968 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
4969 let graph = config.get_graph(None).unwrap();
4970 let src_id = graph.get_node_id_by_name("src").unwrap();
4971 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4972
4973 let runtime = compute_runtime_plan(graph).unwrap();
4974 let src_step = runtime
4975 .steps
4976 .iter()
4977 .find_map(|step| match step {
4978 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4979 _ => None,
4980 })
4981 .unwrap();
4982 let dst_step = runtime
4983 .steps
4984 .iter()
4985 .find_map(|step| match step {
4986 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4987 _ => None,
4988 })
4989 .unwrap();
4990
4991 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4992 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
4993 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4994 }
4995
4996 #[test]
4997 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
4998 let txt = r#"(
4999 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
5000 cnx: [
5001 (src: "src", dst: "__nc__", msg: "i32"),
5002 (src: "src", dst: "sink", msg: "bool"),
5003 ]
5004 )"#;
5005 let config = CuConfig::deserialize_ron(txt).unwrap();
5006 let graph = config.get_graph(None).unwrap();
5007 let src_id = graph.get_node_id_by_name("src").unwrap();
5008 let dst_id = graph.get_node_id_by_name("sink").unwrap();
5009
5010 let runtime = compute_runtime_plan(graph).unwrap();
5011 let src_step = runtime
5012 .steps
5013 .iter()
5014 .find_map(|step| match step {
5015 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
5016 _ => None,
5017 })
5018 .unwrap();
5019 let dst_step = runtime
5020 .steps
5021 .iter()
5022 .find_map(|step| match step {
5023 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
5024 _ => None,
5025 })
5026 .unwrap();
5027
5028 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
5029 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
5030 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
5031 }
5032
5033 #[test]
5034 fn test_runtime_plan_diamond_case1() {
5035 let mut config = CuConfig::default();
5037 let graph = config.get_graph_mut(None).unwrap();
5038 let cam0_id = graph
5039 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
5040 .unwrap();
5041 let inf0_id = graph
5042 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
5043 .unwrap();
5044 let broadcast_id = graph
5045 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
5046 .unwrap();
5047
5048 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
5050 graph.connect(cam0_id, inf0_id, "i32").unwrap();
5051 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
5052
5053 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
5054 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
5055
5056 assert_eq!(edge_cam0_to_inf0, 0);
5057 assert_eq!(edge_cam0_to_broadcast, 1);
5058
5059 let runtime = compute_runtime_plan(graph).unwrap();
5060 let broadcast_step = runtime
5061 .steps
5062 .iter()
5063 .find_map(|step| match step {
5064 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
5065 _ => None,
5066 })
5067 .unwrap();
5068
5069 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
5070 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
5071 }
5072
5073 #[test]
5074 fn test_runtime_plan_diamond_case2() {
5075 let mut config = CuConfig::default();
5077 let graph = config.get_graph_mut(None).unwrap();
5078 let cam0_id = graph
5079 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
5080 .unwrap();
5081 let inf0_id = graph
5082 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
5083 .unwrap();
5084 let broadcast_id = graph
5085 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
5086 .unwrap();
5087
5088 graph.connect(cam0_id, inf0_id, "i32").unwrap();
5090 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
5091 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
5092
5093 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
5094 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
5095
5096 assert_eq!(edge_cam0_to_broadcast, 0);
5097 assert_eq!(edge_cam0_to_inf0, 1);
5098
5099 let runtime = compute_runtime_plan(graph).unwrap();
5100 let broadcast_step = runtime
5101 .steps
5102 .iter()
5103 .find_map(|step| match step {
5104 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
5105 _ => None,
5106 })
5107 .unwrap();
5108
5109 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
5110 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
5111 }
5112
5113 use crate::config::AnytimeConfig;
5116
5117 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
5118 let mut node = Node::new(id, "tasks::AnytimeTask");
5119 node.set_anytime(Some(AnytimeConfig {
5120 max_refines,
5121 ..Default::default()
5122 }));
5123 node
5124 }
5125
5126 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
5128 plan.steps
5129 .iter()
5130 .map(|unit| match unit {
5131 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
5132 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
5133 })
5134 .collect()
5135 }
5136
5137 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
5140 CuExecutionUnit::Step(Box::new(CuExecutionStep {
5141 node_id,
5142 node,
5143 task_type: CuTaskType::Regular,
5144 phase: CuStepPhase::default(),
5145 input_msg_indices_types: inputs
5146 .iter()
5147 .map(|&culist_index| CuInputMsg {
5148 culist_index,
5149 msg_type: "msg::A".to_string(),
5150 src_port: 0,
5151 edge_id: 0,
5152 connection_order: 0,
5153 })
5154 .collect(),
5155 output_msg_pack: Some(CuOutputPack {
5156 culist_index: output,
5157 msg_types: vec!["msg::A".to_string()],
5158 src_channels: vec![None],
5159 }),
5160 }))
5161 }
5162
5163 #[test]
5164 fn test_anytime_expansion_contiguous_without_gap() {
5165 let mut config = CuConfig::default();
5168 let graph = config.get_graph_mut(None).unwrap();
5169 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
5170 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
5171 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
5172 graph.connect(src_id, any_id, "msg::A").unwrap();
5173 graph.connect(any_id, sink_id, "msg::B").unwrap();
5174
5175 let mut plan = compute_runtime_plan(graph).unwrap();
5176 expand_anytime_steps(&mut plan).unwrap();
5177
5178 assert_eq!(
5179 plan_shape(&plan),
5180 vec![
5181 (src_id, CuStepPhase::Whole),
5182 (any_id, CuStepPhase::AnytimeBase),
5183 (any_id, CuStepPhase::AnytimeRefine),
5184 (any_id, CuStepPhase::AnytimeRefine),
5185 (any_id, CuStepPhase::AnytimeRefine),
5186 (sink_id, CuStepPhase::Whole),
5187 ]
5188 );
5189
5190 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
5192 let mut base_pack = None;
5193 let mut refines = Vec::new();
5194 for unit in &plan.steps {
5195 if let CuExecutionUnit::Step(step) = unit {
5196 match step.phase {
5197 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
5198 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
5199 CuStepPhase::Whole => {}
5200 }
5201 }
5202 }
5203 (base_pack, refines)
5204 };
5205 let base_pack = base_pack.unwrap();
5206 for refine in refine_steps {
5207 assert!(refine.input_msg_indices_types.is_empty());
5208 let pack = refine.output_msg_pack.as_ref().unwrap();
5209 assert_eq!(pack.culist_index, base_pack.culist_index);
5210 }
5211 }
5212
5213 #[test]
5214 fn test_anytime_expansion_interleaves_with_gap_steps() {
5215 let mut plan = CuExecutionLoop {
5218 steps: vec![
5219 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
5220 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
5221 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
5222 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
5223 ],
5224 loop_count: None,
5225 };
5226 expand_anytime_steps(&mut plan).unwrap();
5227 assert_eq!(
5228 plan_shape(&plan),
5229 vec![
5230 (0, CuStepPhase::AnytimeBase),
5231 (0, CuStepPhase::AnytimeRefine),
5232 (1, CuStepPhase::Whole),
5233 (0, CuStepPhase::AnytimeRefine),
5234 (2, CuStepPhase::Whole),
5235 (0, CuStepPhase::AnytimeRefine),
5236 (0, CuStepPhase::AnytimeRefine),
5237 (3, CuStepPhase::Whole),
5238 ]
5239 );
5240 }
5241
5242 #[test]
5243 fn test_anytime_expansion_fewer_refines_than_gaps() {
5244 let mut plan = CuExecutionLoop {
5246 steps: vec![
5247 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
5248 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
5249 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
5250 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
5251 ],
5252 loop_count: None,
5253 };
5254 expand_anytime_steps(&mut plan).unwrap();
5255 assert_eq!(
5256 plan_shape(&plan),
5257 vec![
5258 (0, CuStepPhase::AnytimeBase),
5259 (0, CuStepPhase::AnytimeRefine),
5260 (1, CuStepPhase::Whole),
5261 (0, CuStepPhase::AnytimeRefine),
5262 (2, CuStepPhase::Whole),
5263 (3, CuStepPhase::Whole),
5264 ]
5265 );
5266 }
5267
5268 #[test]
5269 fn test_anytime_expansion_without_consumer() {
5270 let mut plan = CuExecutionLoop {
5272 steps: vec![
5273 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
5274 manual_step(Node::new("other", "t"), 1, &[], 1),
5275 ],
5276 loop_count: None,
5277 };
5278 expand_anytime_steps(&mut plan).unwrap();
5279 assert_eq!(
5280 plan_shape(&plan),
5281 vec![
5282 (0, CuStepPhase::AnytimeBase),
5283 (0, CuStepPhase::AnytimeRefine),
5284 (0, CuStepPhase::AnytimeRefine),
5285 (1, CuStepPhase::Whole),
5286 ]
5287 );
5288 }
5289
5290 #[test]
5291 fn test_anytime_expansion_two_nodes_interleave() {
5292 let mut plan = CuExecutionLoop {
5295 steps: vec![
5296 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
5297 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
5298 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
5299 ],
5300 loop_count: None,
5301 };
5302 expand_anytime_steps(&mut plan).unwrap();
5303 assert_eq!(
5306 plan_shape(&plan),
5307 vec![
5308 (0, CuStepPhase::AnytimeBase),
5309 (0, CuStepPhase::AnytimeRefine),
5310 (1, CuStepPhase::AnytimeBase),
5311 (1, CuStepPhase::AnytimeRefine),
5312 (0, CuStepPhase::AnytimeRefine),
5313 (1, CuStepPhase::AnytimeRefine),
5314 (2, CuStepPhase::Whole),
5315 ]
5316 );
5317 }
5318
5319 #[test]
5320 fn test_anytime_expansion_requires_max_refines() {
5321 let mut plan = CuExecutionLoop {
5322 steps: vec![
5323 manual_step(anytime_node("any", None), 0, &[], 0),
5324 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
5325 ],
5326 loop_count: None,
5327 };
5328 let err = expand_anytime_steps(&mut plan).unwrap_err();
5329 assert!(err.to_string().contains("needs anytime.max_refines"));
5330 }
5331}