1use crate::app::Subsystem;
6use crate::config::{ComponentConfig, DEFAULT_KEYFRAME_INTERVAL, Node, TaskKind};
7use crate::config::{
8 CuConfig, CuGraph, MAX_RATE_TARGET_HZ, NodeId, RuntimeConfig, resolve_task_kind_for_id,
9};
10use crate::copperlist::{CopperList, CopperListState, CuListZeroedInit, CuListsManager};
11use crate::cutask::{BincodeAdapter, Freezable};
12#[cfg(feature = "std")]
13use crate::monitoring::ExecutionProbeHandle;
14#[cfg(feature = "std")]
15use crate::monitoring::MonitorExecutionProbe;
16use crate::monitoring::{
17 ComponentId, CopperListInfo, CuMonitor, CuMonitoringMetadata, CuMonitoringRuntime,
18 ExecutionMarker, MonitorComponentMetadata, RuntimeExecutionProbe, build_monitor_topology,
19 take_last_completed_handle_bytes,
20};
21#[cfg(all(feature = "std", feature = "parallel-rt"))]
22use crate::parallel_rt::{ParallelRt, ParallelRtMetadata};
23use crate::planner::{CuPlanner, Linearity, check_order, plan_from_order};
24use crate::resource::ResourceManager;
25#[cfg(feature = "std")]
26use alloc::sync::Arc;
27use compact_str::CompactString;
28use cu29_clock::{ClockProvider, CuDuration, CuTime, RobotClock};
29use cu29_traits::CuResult;
30use cu29_traits::WriteStream;
31use cu29_traits::{CopperListTuple, CuError};
32#[cfg(feature = "std")]
33use rayon::ThreadPool;
34
35#[cfg(target_os = "none")]
36#[allow(unused_imports)]
37use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
38#[cfg(target_os = "none")]
39#[allow(unused_imports)]
40use cu29_log_derive::info;
41#[cfg(target_os = "none")]
42#[allow(unused_imports)]
43use cu29_log_runtime::log;
44#[cfg(all(target_os = "none", debug_assertions))]
45#[allow(unused_imports)]
46use cu29_log_runtime::log_debug_mode;
47#[cfg(target_os = "none")]
48#[allow(unused_imports)]
49use cu29_value::to_value;
50
51#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
52use alloc::alloc::{alloc_zeroed, handle_alloc_error};
53use alloc::boxed::Box;
54use alloc::format;
55use alloc::string::{String, ToString};
56use alloc::vec::Vec;
57use bincode::de::read::Reader;
58use bincode::de::{Decoder, DecoderImpl};
59use bincode::enc::EncoderImpl;
60use bincode::enc::write::{SizeWriter, Writer};
61use bincode::error::{DecodeError, EncodeError};
62use bincode::{Decode, Encode};
63#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
64use core::alloc::Layout;
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(all(feature = "std", feature = "parallel-rt"))]
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(all(feature = "std", feature = "parallel-rt"))]
121 parallel_rt_metadata: &'static ParallelRtMetadata,
122 monitor_instanciator: MI,
123 bridges_instanciator: BI,
124 ) -> Self {
125 Self {
126 tasks_instanciator,
127 monitored_components,
128 culist_component_mapping,
129 #[cfg(all(feature = "std", feature = "parallel-rt"))]
130 parallel_rt_metadata,
131 monitor_instanciator,
132 bridges_instanciator,
133 _payload: PhantomData,
134 }
135 }
136}
137
138#[doc(hidden)]
139pub struct CuRuntimeBuilder<
140 'cfg,
141 CT,
142 CB,
143 P: CopperListTuple,
144 M: CuMonitor,
145 const NBCL: usize,
146 TI,
147 BI,
148 MI,
149 CLS,
150 KFS,
151> {
152 clock: RobotClock,
153 config: &'cfg CuConfig,
154 mission: &'cfg str,
155 subsystem: Subsystem,
156 instance_id: u32,
157 resources: Option<ResourceManager>,
158 #[cfg(feature = "std")]
159 thread_pools: Option<Vec<Option<Arc<ThreadPool>>>>,
160 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
161 copperlist_sink: CLS,
162 keyframe_sink: KFS,
163 output_requirements: OutputRequirements,
164}
165
166impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLS, KFS>
167 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
168{
169 pub fn new(
170 clock: RobotClock,
171 config: &'cfg CuConfig,
172 mission: &'cfg str,
173 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
174 copperlist_sink: CLS,
175 keyframe_sink: KFS,
176 output_requirements: OutputRequirements,
177 ) -> Self {
178 Self {
179 clock,
180 config,
181 mission,
182 subsystem: Subsystem::new(None, 0),
183 instance_id: 0,
184 resources: None,
185 #[cfg(feature = "std")]
186 thread_pools: None,
187 parts,
188 copperlist_sink,
189 keyframe_sink,
190 output_requirements,
191 }
192 }
193
194 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
195 self.subsystem = subsystem;
196 self
197 }
198
199 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
200 self.instance_id = instance_id;
201 self
202 }
203
204 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
205 self.resources = Some(resources);
206 self
207 }
208
209 pub fn try_with_resources_instantiator(
210 mut self,
211 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
212 ) -> CuResult<Self> {
213 self.resources = Some(resources_instantiator(self.config)?);
214 Ok(self)
215 }
216
217 #[cfg(feature = "std")]
221 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
222 self.thread_pools = Some(pools);
223 self
224 }
225
226 #[cfg(feature = "std")]
227 pub fn try_with_thread_pools_instantiator(
228 mut self,
229 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
230 ) -> CuResult<Self> {
231 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
232 Ok(self)
233 }
234}
235
236#[inline]
247pub fn perf_now(_clock: &RobotClock) -> CuTime {
248 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
249 {
250 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
251 return PERF_CLOCK.get_or_init(RobotClock::new).now();
252 }
253
254 #[allow(unreachable_code)]
255 _clock.now()
256}
257
258#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
259const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
260
261#[inline]
263pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
264 if rate_target_hz == 0 {
265 return Err(CuError::from(
266 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
267 ));
268 }
269
270 if rate_target_hz > MAX_RATE_TARGET_HZ {
271 return Err(CuError::from(format!(
272 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
273 )));
274 }
275
276 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
277}
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
286pub struct LoopRateLimiter {
287 period: CuDuration,
288 next_deadline: CuTime,
289}
290
291impl LoopRateLimiter {
292 #[inline]
293 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
294 let period = rate_target_period(rate_target_hz)?;
295 Ok(Self {
296 period,
297 next_deadline: clock.now() + period,
298 })
299 }
300
301 #[inline]
302 pub fn is_ready(&self, clock: &RobotClock) -> bool {
303 self.remaining(clock).is_none()
304 }
305
306 #[inline]
307 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
308 let now = clock.now();
309 if now < self.next_deadline {
310 Some(self.next_deadline - now)
311 } else {
312 None
313 }
314 }
315
316 #[inline]
317 pub fn wait_until_ready(&self, clock: &RobotClock) {
318 let deadline = self.next_deadline;
319 let Some(remaining) = self.remaining(clock) else {
320 return;
321 };
322
323 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
324 {
325 let spin_window = self.spin_window();
326 if remaining > spin_window {
327 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
328 }
329 while clock.now() < deadline {
330 core::hint::spin_loop();
331 }
332 }
333
334 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
335 {
336 let _ = deadline;
337 std::thread::sleep(std::time::Duration::from(remaining));
338 }
339
340 #[cfg(not(feature = "std"))]
341 {
342 let _ = remaining;
343 while clock.now() < deadline {
344 core::hint::spin_loop();
345 }
346 }
347 }
348
349 #[inline]
350 pub fn mark_tick(&mut self, clock: &RobotClock) {
351 self.advance_from(clock.now());
352 }
353
354 #[inline]
355 pub fn limit(&mut self, clock: &RobotClock) {
356 self.wait_until_ready(clock);
357 self.mark_tick(clock);
358 }
359
360 #[inline]
361 fn advance_from(&mut self, now: CuTime) {
362 let steps = if now < self.next_deadline {
363 1
364 } else {
365 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
366 };
367 self.next_deadline += steps * self.period;
368 }
369
370 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
371 #[inline]
372 fn spin_window(&self) -> CuDuration {
373 let _ = self.period;
374 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
375 }
376
377 #[cfg(test)]
378 #[inline]
379 fn next_deadline(&self) -> CuTime {
380 self.next_deadline
381 }
382}
383
384#[cfg(all(feature = "std", feature = "async-cl-io"))]
385#[doc(hidden)]
386pub trait AsyncCopperListPayload: Send {}
387
388#[cfg(all(feature = "std", feature = "async-cl-io"))]
389impl<T: Send> AsyncCopperListPayload for T {}
390
391#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
392#[doc(hidden)]
393pub trait AsyncCopperListPayload {}
394
395#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
396impl<T> AsyncCopperListPayload for T {}
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
405#[doc(hidden)]
406pub enum ProcessStepOutcome {
407 Continue,
408 AbortCopperList,
409}
410
411#[doc(hidden)]
413pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
414
415#[cfg(feature = "remote-debug")]
416fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
417 cl: &CopperList<P>,
418) -> CuResult<Vec<u8>> {
419 bincode::encode_to_vec(cl, bincode::config::standard())
420 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
421}
422
423#[doc(hidden)]
429pub type SemanticRecordSink<T> = dyn WriteStream<T>;
430
431#[doc(hidden)]
433pub type CompletedCopperListSink<P> = SemanticRecordSink<CopperList<P>>;
434
435#[doc(hidden)]
437pub type CompletedKeyFrameSink = SemanticRecordSink<KeyFrame>;
438
439#[derive(Clone, Copy, Debug, Default)]
441#[doc(hidden)]
442pub struct NullWriteStream;
443
444impl<E: Encode> WriteStream<E> for NullWriteStream {
445 #[inline]
446 fn log(&mut self, _record: &E) -> CuResult<()> {
447 Ok(())
448 }
449}
450
451#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
457#[doc(hidden)]
458pub struct OutputRequirements {
459 completed_copperlists: bool,
460 keyframes: bool,
461}
462
463impl OutputRequirements {
464 #[inline]
465 pub const fn new(completed_copperlists: bool, keyframes: bool) -> Self {
466 Self {
467 completed_copperlists,
468 keyframes,
469 }
470 }
471
472 #[inline]
473 pub const fn completed_copperlists(self) -> bool {
474 self.completed_copperlists
475 }
476
477 #[inline]
478 pub const fn keyframes(self) -> bool {
479 self.keyframes
480 }
481
482 #[inline]
484 pub const fn union(self, other: Self) -> Self {
485 Self::new(
486 self.completed_copperlists || other.completed_copperlists,
487 self.keyframes || other.keyframes,
488 )
489 }
490}
491
492#[doc(hidden)]
494pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
495 inner: CuListsManager<P, NBCL>,
496 sink: Option<Box<CompletedCopperListSink<P>>>,
497 #[cfg(feature = "remote-debug")]
499 last_completed_encoded: Option<Vec<u8>>,
500 pub last_encoded_bytes: u64,
502 pub last_handle_bytes: u64,
504}
505
506impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
507 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
508 where
509 P: CuListZeroedInit,
510 {
511 Ok(Self {
512 inner: CuListsManager::new(),
513 sink,
514 #[cfg(feature = "remote-debug")]
515 last_completed_encoded: None,
516 last_encoded_bytes: 0,
517 last_handle_bytes: 0,
518 })
519 }
520
521 pub fn next_cl_id(&self) -> u64 {
522 self.inner.next_cl_id()
523 }
524
525 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
528 if !self.inner.is_empty() {
529 return Err(CuError::from(
530 "Cannot reposition replay with active CopperLists",
531 ));
532 }
533 self.inner.set_next_replay_id(next_id);
534 Ok(())
535 }
536
537 pub fn last_cl_id(&self) -> u64 {
538 self.inner.last_cl_id()
539 }
540
541 pub fn peek(&self) -> Option<&CopperList<P>> {
542 self.inner.peek()
543 }
544
545 #[cfg(feature = "remote-debug")]
546 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
547 self.last_completed_encoded.as_deref()
548 }
549
550 #[cfg(not(feature = "remote-debug"))]
551 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
552 None
553 }
554
555 #[cfg(feature = "remote-debug")]
556 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
557 self.last_completed_encoded = snapshot;
558 }
559
560 #[cfg(not(feature = "remote-debug"))]
561 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
562
563 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
564 where
565 P: CuListZeroedInit,
566 {
567 self.inner
568 .create()
569 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
570 }
571
572 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
573 #[cfg(debug_assertions)]
574 self.debug_assert_end_of_processing_target(culistid);
575
576 let mut is_top = true;
577 let mut nb_done = 0;
578 self.last_encoded_bytes = 0;
579 self.last_handle_bytes = 0;
580 #[cfg(feature = "remote-debug")]
581 let last_completed_encoded = &mut self.last_completed_encoded;
582 for cl in self.inner.iter_mut() {
583 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
584 cl.change_state(CopperListState::DoneProcessing);
585 #[cfg(feature = "remote-debug")]
586 {
587 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
588 }
589 }
590 if is_top && cl.get_state() == CopperListState::DoneProcessing {
591 if let Some(sink) = &mut self.sink {
592 cl.change_state(CopperListState::BeingSerialized);
593 sink.log(cl)?;
594 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
595 self.last_handle_bytes = take_last_completed_handle_bytes();
596 }
597 cl.change_state(CopperListState::Free);
598 nb_done += 1;
599 } else {
600 is_top = false;
601 }
602 }
603 for _ in 0..nb_done {
604 let _ = self.inner.pop();
605 }
606 Ok(())
607 }
608
609 pub fn finish_pending(&mut self) -> CuResult<()> {
610 Ok(())
611 }
612
613 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
614 Ok(NBCL - self.inner.len())
615 }
616
617 #[inline]
618 pub const fn dropped_copperlists_total(&self) -> u64 {
619 0
620 }
621
622 #[cfg(feature = "std")]
623 pub fn end_of_processing_boxed(
624 &mut self,
625 mut culist: Box<CopperList<P>>,
626 ) -> CuResult<OwnedCopperListSubmission<P>> {
627 #[cfg(debug_assertions)]
628 debug_assert_processing_completion_state(culist.as_ref(), "sync boxed end_of_processing");
629
630 culist.change_state(CopperListState::DoneProcessing);
631 self.last_encoded_bytes = 0;
632 self.last_handle_bytes = 0;
633 if let Some(sink) = &mut self.sink {
634 culist.change_state(CopperListState::BeingSerialized);
635 sink.log(&culist)?;
636 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
637 self.last_handle_bytes = take_last_completed_handle_bytes();
638 }
639 culist.change_state(CopperListState::Free);
640 Ok(OwnedCopperListSubmission::Recycled(culist))
641 }
642
643 #[cfg(feature = "std")]
644 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
645 Ok(None)
646 }
647
648 #[cfg(feature = "std")]
649 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
650 Err(CuError::from(
651 "Synchronous CopperList I/O cannot block waiting for boxed completions",
652 ))
653 }
654
655 #[cfg(feature = "std")]
656 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
657 Ok(Vec::new())
658 }
659
660 #[cfg(debug_assertions)]
661 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
662 let mut matches = 0usize;
663 let mut state = None;
664 for cl in self.inner.iter() {
665 if cl.id == culistid {
666 matches += 1;
667 state = Some(cl.get_state());
668 }
669 }
670
671 assert_eq!(
672 matches, 1,
673 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
674 );
675 assert_eq!(
676 state,
677 Some(CopperListState::Processing),
678 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
679 state
680 );
681 }
682}
683
684#[cfg(feature = "std")]
686#[doc(hidden)]
687pub enum OwnedCopperListSubmission<P: CopperListTuple> {
688 Recycled(Box<CopperList<P>>),
690 Pending,
692}
693
694#[cfg(all(feature = "std", feature = "async-cl-io"))]
695struct AsyncCopperListCompletion<P: CopperListTuple> {
696 culist: Box<CopperList<P>>,
697 sink_result: CuResult<(u64, u64)>,
698 #[cfg(feature = "remote-debug")]
699 completed_snapshot: CuResult<Vec<u8>>,
700}
701
702#[cfg(all(feature = "std", feature = "async-cl-io"))]
703struct AsyncOutputWorkerRunningGuard(Arc<AtomicBool>);
704
705#[cfg(all(feature = "std", feature = "async-cl-io"))]
706impl Drop for AsyncOutputWorkerRunningGuard {
707 fn drop(&mut self) {
708 self.0.store(false, Ordering::Release);
709 }
710}
711
712#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
713fn allocate_zeroed_copperlist<P>() -> Box<CopperList<P>>
714where
715 P: CopperListTuple + CuListZeroedInit,
716{
717 let mut culist = unsafe {
719 let layout = Layout::new::<CopperList<P>>();
720 let ptr = alloc_zeroed(layout) as *mut CopperList<P>;
721 if ptr.is_null() {
722 handle_alloc_error(layout);
723 }
724 Box::from_raw(ptr)
725 };
726 culist.msgs.init_zeroed();
727 culist
728}
729
730#[cfg(all(feature = "std", feature = "parallel-rt"))]
731pub fn allocate_boxed_copperlists<P, const NBCL: usize>() -> Vec<Box<CopperList<P>>>
732where
733 P: CopperListTuple + CuListZeroedInit,
734{
735 let mut free_pool = Vec::with_capacity(NBCL);
736 for _ in 0..NBCL {
737 free_pool.push(allocate_zeroed_copperlist::<P>());
738 }
739 free_pool
740}
741
742#[cfg(all(feature = "std", feature = "async-cl-io"))]
744#[doc(hidden)]
745pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
746 free_pool: Vec<Box<CopperList<P>>>,
747 current: Option<Box<CopperList<P>>>,
748 #[cfg(feature = "remote-debug")]
749 last_completed_encoded: Option<Vec<u8>>,
750 pending_count: usize,
751 next_cl_id: u64,
752 pending_producer: Option<Producer<Box<CopperList<P>>>>,
753 completion_consumer: Option<Consumer<AsyncCopperListCompletion<P>>>,
754 worker_handle: Option<JoinHandle<()>>,
755 worker_thread: Option<Thread>,
756 worker_shutdown: Option<Arc<AtomicBool>>,
757 worker_running: Option<Arc<AtomicBool>>,
758 dropped_copperlists_total: u64,
759 pub last_encoded_bytes: u64,
761 pub last_handle_bytes: u64,
763}
764
765#[cfg(all(feature = "std", feature = "async-cl-io"))]
766impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
767 pub fn new(sink: Option<Box<CompletedCopperListSink<P>>>) -> CuResult<Self>
768 where
769 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
770 {
771 let mut free_pool = Vec::with_capacity(NBCL);
772 for _ in 0..NBCL {
773 free_pool.push(allocate_zeroed_copperlist::<P>());
774 }
775
776 if sink.is_some() && NBCL < 2 {
777 return Err(CuError::from(
778 "async CopperList output requires at least two CopperList slots",
779 ));
780 }
781
782 let (
783 pending_producer,
784 completion_consumer,
785 worker_handle,
786 worker_thread,
787 worker_shutdown,
788 worker_running,
789 ) = if let Some(mut sink) = sink {
790 let handoff_capacity = NBCL - 1;
791 let (pending_producer, mut pending_consumer) =
792 RingBuffer::<Box<CopperList<P>>>::new(handoff_capacity);
793 let (mut completion_producer, completion_consumer) =
794 RingBuffer::<AsyncCopperListCompletion<P>>::new(handoff_capacity);
795 let worker_shutdown = Arc::new(AtomicBool::new(false));
796 let worker_running = Arc::new(AtomicBool::new(true));
797 let shutdown = worker_shutdown.clone();
798 let running = worker_running.clone();
799 let worker_handle = std::thread::Builder::new()
800 .name("cu-async-cl-io".to_string())
801 .spawn(move || {
802 let _running_guard = AsyncOutputWorkerRunningGuard(running);
803 loop {
804 let mut culist = match pending_consumer.pop() {
805 Ok(culist) => culist,
806 Err(PopError::Empty) => {
807 if shutdown.load(Ordering::Acquire) {
808 break;
809 }
810 std::thread::park();
811 continue;
812 }
813 };
814 #[cfg(feature = "remote-debug")]
815 let completed_snapshot = {
816 culist.change_state(CopperListState::DoneProcessing);
819 encode_completed_copperlist_snapshot(&culist)
820 };
821 culist.change_state(CopperListState::BeingSerialized);
822 let sink_result = sink.log(&culist).map(|_| {
823 (
824 sink.last_log_bytes().unwrap_or(0) as u64,
825 take_last_completed_handle_bytes(),
826 )
827 });
828 let should_stop = sink_result.is_err();
829 #[cfg(feature = "remote-debug")]
830 let should_stop = should_stop || completed_snapshot.is_err();
831 let mut completion = AsyncCopperListCompletion {
832 culist,
833 sink_result,
834 #[cfg(feature = "remote-debug")]
835 completed_snapshot,
836 };
837 loop {
838 match completion_producer.push(completion) {
839 Ok(()) => break,
840 Err(PushError::Full(returned)) => {
841 completion = returned;
842 std::thread::yield_now();
843 }
844 }
845 }
846 if should_stop {
847 break;
848 }
849 }
850 })
851 .map_err(|e| {
852 CuError::from("Failed to spawn async CopperList serializer thread")
853 .add_cause(e.to_string().as_str())
854 })?;
855 let worker_thread = worker_handle.thread().clone();
856 (
857 Some(pending_producer),
858 Some(completion_consumer),
859 Some(worker_handle),
860 Some(worker_thread),
861 Some(worker_shutdown),
862 Some(worker_running),
863 )
864 } else {
865 (None, None, None, None, None, None)
866 };
867
868 Ok(Self {
869 free_pool,
870 current: None,
871 #[cfg(feature = "remote-debug")]
872 last_completed_encoded: None,
873 pending_count: 0,
874 next_cl_id: 0,
875 pending_producer,
876 completion_consumer,
877 worker_handle,
878 worker_thread,
879 worker_shutdown,
880 worker_running,
881 dropped_copperlists_total: 0,
882 last_encoded_bytes: 0,
883 last_handle_bytes: 0,
884 })
885 }
886
887 pub fn next_cl_id(&self) -> u64 {
888 self.next_cl_id
889 }
890
891 pub fn prepare_recorded_replay(&mut self, next_id: u64) -> CuResult<()> {
894 self.finish_pending()?;
895 self.next_cl_id = next_id;
896 Ok(())
897 }
898
899 pub fn last_cl_id(&self) -> u64 {
900 self.next_cl_id.saturating_sub(1)
901 }
902
903 pub fn peek(&self) -> Option<&CopperList<P>> {
904 self.current.as_deref()
905 }
906
907 #[cfg(feature = "remote-debug")]
908 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
909 self.last_completed_encoded.as_deref()
910 }
911
912 #[cfg(not(feature = "remote-debug"))]
913 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
914 None
915 }
916
917 #[cfg(feature = "remote-debug")]
918 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
919 self.last_completed_encoded = snapshot;
920 }
921
922 #[cfg(not(feature = "remote-debug"))]
923 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
924
925 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
926 where
927 P: CuListZeroedInit,
928 {
929 if self.current.is_some() {
930 return Err(CuError::from(
931 "Attempted to create a CopperList while another one is still active",
932 ));
933 }
934
935 self.reclaim_completed()?;
936
937 let culist = self.free_pool.pop().ok_or_else(|| {
938 CuError::from("CopperList output handoff exhausted the slot reserved for execution")
939 })?;
940 self.current = Some(culist);
941
942 let current = self
943 .current
944 .as_mut()
945 .expect("current CopperList is missing");
946 current.reset_for_runtime_use(self.next_cl_id);
947 self.next_cl_id += 1;
948 Ok(current.as_mut())
949 }
950
951 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
952 self.reclaim_completed()?;
953
954 let mut culist = self.current.take().ok_or_else(|| {
955 CuError::from("Attempted to finish processing without an active CopperList")
956 })?;
957
958 if culist.id != culistid {
959 return Err(CuError::from(format!(
960 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
961 culist.id
962 )));
963 }
964 #[cfg(debug_assertions)]
965 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
966
967 culist.change_state(CopperListState::DoneProcessing);
968 self.last_encoded_bytes = 0;
969 self.last_handle_bytes = 0;
970
971 match self.try_submit(culist)? {
972 OwnedCopperListSubmission::Recycled(culist) => self.free_pool.push(culist),
973 OwnedCopperListSubmission::Pending => {}
974 }
975
976 Ok(())
977 }
978
979 pub fn finish_pending(&mut self) -> CuResult<()> {
980 if self.current.is_some() {
981 return Err(CuError::from(
982 "Cannot flush CopperList I/O while a CopperList is still active",
983 ));
984 }
985
986 while self.pending_count > 0 {
987 self.wait_for_completion()?;
988 }
989 Ok(())
990 }
991
992 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
993 self.reclaim_completed()?;
994 Ok(self.free_pool.len())
995 }
996
997 #[inline]
998 pub const fn dropped_copperlists_total(&self) -> u64 {
999 self.dropped_copperlists_total
1000 }
1001
1002 pub fn end_of_processing_boxed(
1003 &mut self,
1004 mut culist: Box<CopperList<P>>,
1005 ) -> CuResult<OwnedCopperListSubmission<P>> {
1006 #[cfg(debug_assertions)]
1007 debug_assert_processing_completion_state(culist.as_ref(), "async boxed end_of_processing");
1008 culist.change_state(CopperListState::DoneProcessing);
1009 self.last_encoded_bytes = 0;
1010 self.last_handle_bytes = 0;
1011
1012 self.try_submit(culist)
1013 }
1014
1015 fn try_submit(
1016 &mut self,
1017 mut culist: Box<CopperList<P>>,
1018 ) -> CuResult<OwnedCopperListSubmission<P>> {
1019 let Some(pending_producer) = self.pending_producer.as_mut() else {
1020 culist.change_state(CopperListState::Free);
1021 return Ok(OwnedCopperListSubmission::Recycled(culist));
1022 };
1023
1024 if self.pending_count >= NBCL - 1 {
1028 return Ok(self.drop_copperlist(culist));
1029 }
1030
1031 culist.change_state(CopperListState::QueuedForSerialization);
1032 match pending_producer.push(culist) {
1033 Ok(()) => {
1034 self.pending_count += 1;
1035 if let Some(worker_thread) = self.worker_thread.as_ref() {
1036 worker_thread.unpark();
1037 }
1038 Ok(OwnedCopperListSubmission::Pending)
1039 }
1040 Err(PushError::Full(culist)) => Ok(self.drop_copperlist(culist)),
1041 }
1042 }
1043
1044 fn drop_copperlist(&mut self, mut culist: Box<CopperList<P>>) -> OwnedCopperListSubmission<P> {
1045 self.dropped_copperlists_total = self.dropped_copperlists_total.saturating_add(1);
1046 culist.change_state(CopperListState::Free);
1047 OwnedCopperListSubmission::Recycled(culist)
1048 }
1049
1050 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
1051 let pop_result = {
1052 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1053 return Ok(None);
1054 };
1055 completion_consumer.pop()
1056 };
1057 match pop_result {
1058 Ok(completion) => self.handle_completion(completion).map(Some),
1059 Err(PopError::Empty) => {
1060 if self.pending_count > 0
1061 && self
1062 .worker_running
1063 .as_ref()
1064 .is_some_and(|running| !running.load(Ordering::Acquire))
1065 {
1066 Err(CuError::from(
1067 "Async CopperList output worker stopped unexpectedly",
1068 ))
1069 } else {
1070 Ok(None)
1071 }
1072 }
1073 }
1074 }
1075
1076 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
1077 if self.completion_consumer.is_none() {
1078 return Err(CuError::from(
1079 "No async CopperList output worker is active to return a free slot",
1080 ));
1081 }
1082 loop {
1083 if let Some(culist) = self.try_reclaim_boxed()? {
1084 return Ok(culist);
1085 }
1086 std::thread::yield_now();
1087 }
1088 }
1089
1090 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
1091 let mut reclaimed = Vec::with_capacity(self.pending_count);
1092 if self.current.is_some() {
1093 return Err(CuError::from(
1094 "Cannot flush CopperList I/O while a CopperList is still active",
1095 ));
1096 }
1097 while self.pending_count > 0 {
1098 reclaimed.push(self.wait_reclaim_boxed()?);
1099 }
1100 Ok(reclaimed)
1101 }
1102
1103 fn reclaim_completed(&mut self) -> CuResult<()> {
1104 loop {
1105 let Some(culist) = self.try_reclaim_boxed()? else {
1106 break;
1107 };
1108 self.free_pool.push(culist);
1109 }
1110 Ok(())
1111 }
1112
1113 fn wait_for_completion(&mut self) -> CuResult<()> {
1114 let culist = self.wait_reclaim_boxed()?;
1115 self.free_pool.push(culist);
1116 Ok(())
1117 }
1118
1119 fn handle_completion(
1120 &mut self,
1121 mut completion: AsyncCopperListCompletion<P>,
1122 ) -> CuResult<Box<CopperList<P>>> {
1123 self.pending_count = self.pending_count.saturating_sub(1);
1124 if let Ok((encoded_bytes, handle_bytes)) = completion.sink_result.as_ref() {
1125 self.last_encoded_bytes = *encoded_bytes;
1126 self.last_handle_bytes = *handle_bytes;
1127 }
1128 completion.culist.change_state(CopperListState::Free);
1129 completion.sink_result?;
1130 #[cfg(feature = "remote-debug")]
1131 {
1132 self.last_completed_encoded = Some(completion.completed_snapshot?);
1133 }
1134 Ok(completion.culist)
1135 }
1136
1137 fn shutdown_worker(&mut self) -> CuResult<()> {
1138 self.finish_pending()?;
1139 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1140 shutdown.store(true, Ordering::Release);
1141 }
1142 if let Some(worker_thread) = self.worker_thread.as_ref() {
1143 worker_thread.unpark();
1144 }
1145 if let Some(worker_handle) = self.worker_handle.take() {
1146 worker_handle.join().map_err(|_| {
1147 CuError::from("Async CopperList output worker panicked while joining")
1148 })?;
1149 }
1150 self.pending_producer.take();
1151 self.worker_thread.take();
1152 self.worker_shutdown.take();
1153 self.worker_running.take();
1154 Ok(())
1155 }
1156}
1157
1158#[cfg(all(feature = "std", feature = "async-cl-io"))]
1159impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
1160 fn drop(&mut self) {
1161 let _ = self.shutdown_worker();
1162 }
1163}
1164
1165#[cfg(all(feature = "std", debug_assertions))]
1166fn debug_assert_processing_completion_state<P: CopperListTuple>(
1167 culist: &CopperList<P>,
1168 context: &str,
1169) {
1170 assert_eq!(
1171 culist.get_state(),
1172 CopperListState::Processing,
1173 "{context} expected CopperList #{} to be Processing, found {}",
1174 culist.id,
1175 culist.get_state()
1176 );
1177}
1178
1179#[cfg(all(feature = "std", feature = "async-cl-io"))]
1180#[doc(hidden)]
1181pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
1182
1183#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1184#[doc(hidden)]
1185pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
1186
1187#[cfg(all(feature = "std", feature = "async-cl-io"))]
1188struct AsyncKeyFrameCompletion {
1189 keyframe: Box<KeyFrame>,
1190 sink_result: CuResult<u64>,
1191}
1192
1193pub struct KeyFramesManager {
1195 inner: Option<KeyFrame>,
1197
1198 forced_timestamp: Option<CuTime>,
1200
1201 locked: bool,
1203
1204 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1206 sink: Option<Box<CompletedKeyFrameSink>>,
1207
1208 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1212 #[allow(clippy::vec_box)]
1213 spares: Vec<Box<KeyFrame>>,
1214 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1215 pending_count: usize,
1216 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1217 pending_producer: Option<Producer<Box<KeyFrame>>>,
1218 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1219 completion_consumer: Option<Consumer<AsyncKeyFrameCompletion>>,
1220 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1221 worker_handle: Option<JoinHandle<()>>,
1222 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1223 worker_thread: Option<Thread>,
1224 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1225 worker_shutdown: Option<Arc<AtomicBool>>,
1226 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1227 worker_running: Option<Arc<AtomicBool>>,
1228 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1229 capture_this_copperlist: Option<u64>,
1230 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1231 dropped_keyframes_total: u64,
1232
1233 keyframe_interval: u32,
1235
1236 pub last_encoded_bytes: u64,
1238
1239 capture_size_hint: usize,
1241}
1242
1243const MIN_KEYFRAME_CAPTURE_CAPACITY: usize = 4 * 1024;
1244#[cfg(all(feature = "std", feature = "async-cl-io"))]
1245const ASYNC_KEYFRAME_HANDOFF_CAPACITY: usize = 2;
1246
1247struct PreallocatedVecWriter<'a>(&'a mut Vec<u8>);
1249
1250impl Writer for PreallocatedVecWriter<'_> {
1251 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
1252 if bytes.len() > self.0.capacity().saturating_sub(self.0.len()) {
1253 return Err(EncodeError::UnexpectedEnd);
1254 }
1255 self.0.extend_from_slice(bytes);
1257 Ok(())
1258 }
1259}
1260
1261impl KeyFramesManager {
1262 #[doc(hidden)]
1263 pub fn new(sink: Option<Box<CompletedKeyFrameSink>>, keyframe_interval: u32) -> CuResult<Self> {
1264 if sink.is_some() && keyframe_interval == 0 {
1265 return Err(CuError::from(
1266 "Keyframe interval cannot be zero when a downstream consumer requires keyframes",
1267 ));
1268 }
1269
1270 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1271 {
1272 let enabled = sink.is_some();
1273 let (
1274 pending_producer,
1275 completion_consumer,
1276 worker_handle,
1277 worker_thread,
1278 worker_shutdown,
1279 worker_running,
1280 ) = if let Some(mut sink) = sink {
1281 let (pending_producer, mut pending_consumer) =
1282 RingBuffer::<Box<KeyFrame>>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1283 let (mut completion_producer, completion_consumer) =
1284 RingBuffer::<AsyncKeyFrameCompletion>::new(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1285 let worker_shutdown = Arc::new(AtomicBool::new(false));
1286 let worker_running = Arc::new(AtomicBool::new(true));
1287 let shutdown = worker_shutdown.clone();
1288 let running = worker_running.clone();
1289 let worker_handle = std::thread::Builder::new()
1290 .name("cu-async-kf-io".to_string())
1291 .spawn(move || {
1292 let _running_guard = AsyncOutputWorkerRunningGuard(running);
1293 loop {
1294 let keyframe = match pending_consumer.pop() {
1295 Ok(keyframe) => keyframe,
1296 Err(PopError::Empty) => {
1297 if shutdown.load(Ordering::Acquire) {
1298 break;
1299 }
1300 std::thread::park();
1301 continue;
1302 }
1303 };
1304 let sink_result = sink
1305 .log(keyframe.as_ref())
1306 .map(|_| sink.last_log_bytes().unwrap_or(0) as u64);
1307 let should_stop = sink_result.is_err();
1308 let mut completion = AsyncKeyFrameCompletion {
1309 keyframe,
1310 sink_result,
1311 };
1312 loop {
1313 match completion_producer.push(completion) {
1314 Ok(()) => break,
1315 Err(PushError::Full(returned)) => {
1316 completion = returned;
1317 std::thread::yield_now();
1318 }
1319 }
1320 }
1321 if should_stop {
1322 break;
1323 }
1324 }
1325 })
1326 .map_err(|error| {
1327 CuError::from("Failed to spawn async keyframe output thread")
1328 .add_cause(error.to_string().as_str())
1329 })?;
1330 let worker_thread = worker_handle.thread().clone();
1331 (
1332 Some(pending_producer),
1333 Some(completion_consumer),
1334 Some(worker_handle),
1335 Some(worker_thread),
1336 Some(worker_shutdown),
1337 Some(worker_running),
1338 )
1339 } else {
1340 (None, None, None, None, None, None)
1341 };
1342
1343 let spares = if enabled {
1344 let mut spares = Vec::with_capacity(ASYNC_KEYFRAME_HANDOFF_CAPACITY);
1345 for _ in 0..ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1346 spares.push(Box::new(KeyFrame::new()));
1347 }
1348 spares
1349 } else {
1350 Vec::new()
1351 };
1352 Ok(Self {
1353 inner: enabled.then(KeyFrame::new),
1354 forced_timestamp: None,
1355 locked: false,
1356 spares,
1357 pending_count: 0,
1358 pending_producer,
1359 completion_consumer,
1360 worker_handle,
1361 worker_thread,
1362 worker_shutdown,
1363 worker_running,
1364 capture_this_copperlist: None,
1365 dropped_keyframes_total: 0,
1366 keyframe_interval,
1367 last_encoded_bytes: 0,
1368 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1369 })
1370 }
1371
1372 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1373 {
1374 let enabled = sink.is_some();
1375 Ok(Self {
1376 inner: enabled.then(KeyFrame::new),
1377 forced_timestamp: None,
1378 locked: false,
1379 sink,
1380 keyframe_interval,
1381 last_encoded_bytes: 0,
1382 capture_size_hint: KEYFRAME_PAYLOAD_HEADER.len(),
1383 })
1384 }
1385 }
1386
1387 fn is_keyframe_due(&self, culistid: u64) -> bool {
1388 self.inner.is_some() && culistid.is_multiple_of(self.keyframe_interval as u64)
1389 }
1390
1391 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1392 fn is_capturing(&self, culistid: u64) -> bool {
1393 self.capture_this_copperlist == Some(culistid)
1394 }
1395
1396 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1397 fn is_capturing(&self, culistid: u64) -> bool {
1398 self.is_keyframe_due(culistid)
1399 }
1400
1401 #[inline]
1402 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1403 self.is_keyframe_due(culistid)
1404 }
1405
1406 #[doc(hidden)]
1408 pub fn begin_capture_preallocation(&mut self) {
1409 self.capture_size_hint = KEYFRAME_PAYLOAD_HEADER.len();
1410 }
1411
1412 #[doc(hidden)]
1414 pub fn include_capture_capacity(&mut self, item: &impl Freezable) -> CuResult<()> {
1415 if self.inner.is_none() {
1416 return Ok(());
1417 }
1418 let mut sizer = EncoderImpl::new(SizeWriter::default(), bincode::config::standard());
1419 BincodeAdapter(item)
1420 .encode(&mut sizer)
1421 .map_err(|_| CuError::from("Failed to size component keyframe state"))?;
1422 let payload_bytes = sizer.into_writer().bytes_written as usize;
1423 self.capture_size_hint = self
1424 .capture_size_hint
1425 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
1426 .and_then(|size| size.checked_add(payload_bytes))
1427 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1428 Ok(())
1429 }
1430
1431 #[doc(hidden)]
1433 pub fn finish_capture_preallocation(&mut self) -> CuResult<()> {
1434 if self.inner.is_none() {
1435 return Ok(());
1436 }
1437 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1438 self.reclaim_completed()?;
1439 let requested = self
1440 .capture_size_hint
1441 .max(MIN_KEYFRAME_CAPTURE_CAPACITY)
1442 .checked_next_power_of_two()
1443 .ok_or_else(|| CuError::from("Keyframe capture capacity overflow"))?;
1444 reserve_keyframe_capacity(self.inner.as_mut().unwrap(), requested)?;
1445 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1446 for spare in &mut self.spares {
1447 reserve_keyframe_capacity(spare, requested)?;
1448 }
1449 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1450 if self.spares.len() != ASYNC_KEYFRAME_HANDOFF_CAPACITY {
1451 return Err(CuError::from(
1452 "Keyframe output worker did not return every capture buffer before preallocation",
1453 ));
1454 }
1455 Ok(())
1456 }
1457
1458 #[doc(hidden)]
1461 pub fn try_reset(&mut self, culistid: u64, clock: &RobotClock) -> CuResult<()> {
1462 if self.is_keyframe_due(culistid) {
1463 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1464 {
1465 self.reclaim_completed()?;
1466 if self.spares.is_empty() {
1467 self.capture_this_copperlist = None;
1468 self.dropped_keyframes_total = self.dropped_keyframes_total.saturating_add(1);
1469 self.forced_timestamp = None;
1470 self.locked = false;
1471 return Ok(());
1472 }
1473 self.capture_this_copperlist = Some(culistid);
1474 }
1475 let inner = self.inner.as_mut().unwrap();
1477 if self.locked && inner.culistid == culistid {
1478 return Ok(());
1479 }
1480 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1481 inner.reset(culistid, ts);
1482 self.locked = false;
1483 }
1484 Ok(())
1485 }
1486
1487 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1492 let _ = self.try_reset(culistid, clock);
1493 }
1494
1495 #[cfg(feature = "std")]
1497 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1498 self.forced_timestamp = Some(ts);
1499 }
1500
1501 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1502 if self.is_capturing(culistid) {
1503 if self.locked {
1504 return Ok(0);
1506 }
1507 let inner = self.inner.as_mut().unwrap();
1508 if inner.culistid != culistid {
1509 return Err(CuError::from(format!(
1510 "Freezing task for culistid {} but current keyframe is {}",
1511 culistid, inner.culistid
1512 )));
1513 }
1514 let encoded = inner
1515 .add_frozen_task(task)
1516 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))?;
1517 Ok(encoded)
1518 } else {
1519 Ok(0)
1520 }
1521 }
1522
1523 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1525 self.freeze_task(culistid, item)
1526 }
1527
1528 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1529 if self.is_capturing(culistid) {
1530 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1531 {
1532 let sink = self.sink.as_mut().unwrap();
1533 sink.log(self.inner.as_ref().unwrap())?;
1534 self.last_encoded_bytes = sink.last_log_bytes().unwrap_or(0) as u64;
1535 }
1536 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1537 {
1538 self.last_encoded_bytes = 0;
1539 let mut completed = self.spares.pop().ok_or_else(|| {
1540 CuError::from("Missing spare keyframe buffer at output handoff")
1541 })?;
1542 core::mem::swap(self.inner.as_mut().unwrap(), completed.as_mut());
1543 let producer = self.pending_producer.as_mut().ok_or_else(|| {
1544 CuError::from("Missing keyframe output producer for active capture")
1545 })?;
1546 match producer.push(completed) {
1547 Ok(()) => {
1548 self.pending_count += 1;
1549 if let Some(worker_thread) = self.worker_thread.as_ref() {
1550 worker_thread.unpark();
1551 }
1552 }
1553 Err(PushError::Full(spare)) => {
1554 self.spares.push(spare);
1555 self.dropped_keyframes_total =
1556 self.dropped_keyframes_total.saturating_add(1);
1557 }
1558 }
1559 self.capture_this_copperlist = None;
1560 }
1561 self.locked = false;
1563 Ok(())
1564 } else {
1565 self.last_encoded_bytes = 0;
1567 Ok(())
1568 }
1569 }
1570
1571 #[cfg(feature = "std")]
1573 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1574 if let Some(inner) = self.inner.as_mut() {
1575 *inner = keyframe.clone();
1576 self.forced_timestamp = Some(keyframe.timestamp);
1577 self.locked = true;
1578 }
1579 }
1580
1581 #[inline]
1582 #[doc(hidden)]
1583 pub const fn dropped_keyframes_total(&self) -> u64 {
1584 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1585 {
1586 self.dropped_keyframes_total
1587 }
1588 #[cfg(not(all(feature = "std", feature = "async-cl-io")))]
1589 {
1590 0
1591 }
1592 }
1593
1594 #[doc(hidden)]
1595 pub fn finish_pending(&mut self) -> CuResult<()> {
1596 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1597 while self.pending_count > 0 {
1598 self.wait_for_completion()?;
1599 }
1600 Ok(())
1601 }
1602
1603 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1604 fn reclaim_completed(&mut self) -> CuResult<()> {
1605 let pop_result = {
1606 let Some(completion_consumer) = self.completion_consumer.as_mut() else {
1607 return Ok(());
1608 };
1609 completion_consumer.pop()
1610 };
1611 match pop_result {
1612 Ok(completion) => self.handle_completion(completion),
1613 Err(PopError::Empty) => {
1614 if self.pending_count > 0
1615 && self
1616 .worker_running
1617 .as_ref()
1618 .is_some_and(|running| !running.load(Ordering::Acquire))
1619 {
1620 Err(CuError::from(
1621 "Async keyframe output worker stopped unexpectedly",
1622 ))
1623 } else {
1624 Ok(())
1625 }
1626 }
1627 }
1628 }
1629
1630 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1631 fn wait_for_completion(&mut self) -> CuResult<()> {
1632 loop {
1633 let pending_before = self.pending_count;
1634 self.reclaim_completed()?;
1635 if self.pending_count < pending_before {
1636 return Ok(());
1637 }
1638 std::thread::yield_now();
1639 }
1640 }
1641
1642 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1643 fn handle_completion(&mut self, completion: AsyncKeyFrameCompletion) -> CuResult<()> {
1644 self.pending_count = self.pending_count.saturating_sub(1);
1645 if let Ok(encoded_bytes) = completion.sink_result.as_ref() {
1646 self.last_encoded_bytes = *encoded_bytes;
1647 }
1648 self.spares.push(completion.keyframe);
1649 completion.sink_result.map(|_| ())
1650 }
1651
1652 #[cfg(all(feature = "std", feature = "async-cl-io"))]
1653 fn shutdown_worker(&mut self) -> CuResult<()> {
1654 self.finish_pending()?;
1655 if let Some(shutdown) = self.worker_shutdown.as_ref() {
1656 shutdown.store(true, Ordering::Release);
1657 }
1658 if let Some(worker_thread) = self.worker_thread.as_ref() {
1659 worker_thread.unpark();
1660 }
1661 if let Some(worker_handle) = self.worker_handle.take() {
1662 worker_handle.join().map_err(|_| {
1663 CuError::from("Async keyframe output worker panicked while joining")
1664 })?;
1665 }
1666 self.pending_producer.take();
1667 self.worker_thread.take();
1668 self.worker_shutdown.take();
1669 self.worker_running.take();
1670 Ok(())
1671 }
1672}
1673
1674fn reserve_keyframe_capacity(keyframe: &mut KeyFrame, requested: usize) -> CuResult<()> {
1675 if keyframe.serialized_tasks.capacity() < requested {
1676 let additional = requested.saturating_sub(keyframe.serialized_tasks.len());
1677 keyframe
1678 .serialized_tasks
1679 .try_reserve_exact(additional)
1680 .map_err(|error| {
1681 CuError::from("Failed to preallocate keyframe capture buffer")
1682 .add_cause(&error.to_string())
1683 })?;
1684 }
1685 Ok(())
1686}
1687
1688#[cfg(all(feature = "std", feature = "async-cl-io"))]
1689impl Drop for KeyFramesManager {
1690 fn drop(&mut self) {
1691 let _ = self.shutdown_worker();
1692 }
1693}
1694
1695pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1699 clock: RobotClock,
1701
1702 subsystem_code: u16,
1704
1705 #[doc(hidden)]
1707 pub instance_id: u32,
1708
1709 #[doc(hidden)]
1711 pub tasks: CT,
1712
1713 #[doc(hidden)]
1715 pub bridges: CB,
1716
1717 #[doc(hidden)]
1719 pub resources: ResourceManager,
1720
1721 #[cfg(feature = "std")]
1724 #[doc(hidden)]
1725 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1726
1727 #[doc(hidden)]
1729 pub monitor: M,
1730
1731 #[cfg(feature = "std")]
1737 #[doc(hidden)]
1738 pub execution_probe: ExecutionProbeHandle,
1739 #[cfg(not(feature = "std"))]
1740 #[doc(hidden)]
1741 pub execution_probe: RuntimeExecutionProbe,
1742
1743 #[doc(hidden)]
1745 pub copperlists_manager: CopperListsManager<P, NBCL>,
1746
1747 #[doc(hidden)]
1749 pub keyframes_manager: KeyFramesManager,
1750
1751 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1753 #[doc(hidden)]
1754 pub parallel_rt: ParallelRt<NBCL>,
1755
1756 #[doc(hidden)]
1758 pub runtime_config: RuntimeConfig,
1759}
1760
1761impl<
1763 CT,
1764 CB,
1765 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
1766 M: CuMonitor,
1767 const NBCL: usize,
1768> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
1769{
1770 fn get_clock(&self) -> RobotClock {
1771 self.clock.clone()
1772 }
1773}
1774
1775impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
1776 #[inline]
1778 pub fn clock(&self) -> RobotClock {
1779 self.clock.clone()
1780 }
1781
1782 #[doc(hidden)]
1784 #[inline]
1785 pub fn clock_ref(&self) -> &RobotClock {
1786 &self.clock
1787 }
1788
1789 #[inline]
1791 pub fn subsystem_code(&self) -> u16 {
1792 self.subsystem_code
1793 }
1794
1795 #[inline]
1797 pub fn instance_id(&self) -> u32 {
1798 self.instance_id
1799 }
1800}
1801
1802#[cfg(feature = "std")]
1803impl<
1804 'cfg,
1805 CT,
1806 CB,
1807 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1808 M: CuMonitor,
1809 const NBCL: usize,
1810 TI,
1811 BI,
1812 MI,
1813 CLS,
1814 KFS,
1815> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
1816where
1817 TI: for<'c> Fn(
1818 Vec<Option<&'c ComponentConfig>>,
1819 &mut ResourceManager,
1820 &[Option<Arc<ThreadPool>>],
1821 ) -> CuResult<CT>,
1822 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1823 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1824 CLS: WriteStream<CopperList<P>> + 'static,
1825 KFS: WriteStream<KeyFrame> + 'static,
1826{
1827 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1828 let Self {
1829 clock,
1830 config,
1831 mission,
1832 subsystem,
1833 instance_id,
1834 resources,
1835 thread_pools,
1836 parts,
1837 copperlist_sink,
1838 keyframe_sink,
1839 output_requirements,
1840 } = self;
1841 let mut resources =
1842 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1843 let thread_pools = thread_pools.unwrap_or_default();
1844
1845 let graph = config.get_graph(Some(mission))?;
1846 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1847 .get_all_nodes()
1848 .iter()
1849 .map(|(_, node)| node.get_instance_config())
1850 .collect();
1851
1852 let tasks =
1853 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
1854
1855 #[cfg(feature = "std")]
1856 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
1857 #[cfg(not(feature = "std"))]
1858 let execution_probe = RuntimeExecutionProbe::default();
1859 let monitor_metadata = CuMonitoringMetadata::new(
1860 CompactString::from(mission),
1861 parts.monitored_components,
1862 parts.culist_component_mapping,
1863 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1864 build_monitor_topology(config, mission)?,
1865 None,
1866 )?
1867 .with_subsystem_id(subsystem.id())
1868 .with_instance_id(instance_id);
1869 #[cfg(feature = "std")]
1870 let monitor_runtime =
1871 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
1872 #[cfg(not(feature = "std"))]
1873 let monitor_runtime = CuMonitoringRuntime::unavailable();
1874 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1875 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1876
1877 let copperlist_sink = output_requirements
1878 .completed_copperlists()
1879 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
1880 let keyframe_sink = output_requirements
1881 .keyframes()
1882 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
1883 let keyframe_interval = config
1884 .logging
1885 .as_ref()
1886 .and_then(|logging| logging.keyframe_interval)
1887 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
1888
1889 let copperlists_manager = CopperListsManager::new(copperlist_sink)?;
1890 #[cfg(target_os = "none")]
1891 {
1892 let cl_size = core::mem::size_of::<CopperList<P>>();
1893 let total_bytes = cl_size.saturating_mul(NBCL);
1894 info!(
1895 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1896 NBCL, cl_size, total_bytes
1897 );
1898 }
1899
1900 let keyframes_manager = KeyFramesManager::new(keyframe_sink, keyframe_interval)?;
1901 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1902 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
1903
1904 let runtime_config = config.runtime.clone().unwrap_or_default();
1905 runtime_config.validate()?;
1906
1907 Ok(CuRuntime {
1908 subsystem_code: subsystem.code(),
1909 instance_id,
1910 tasks,
1911 bridges,
1912 resources,
1913 thread_pools,
1914 monitor,
1915 execution_probe,
1916 clock,
1917 copperlists_manager,
1918 keyframes_manager,
1919 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1920 parallel_rt,
1921 runtime_config,
1922 })
1923 }
1924}
1925
1926#[cfg(not(feature = "std"))]
1927impl<
1928 'cfg,
1929 CT,
1930 CB,
1931 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1932 M: CuMonitor,
1933 const NBCL: usize,
1934 TI,
1935 BI,
1936 MI,
1937 CLS,
1938 KFS,
1939> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLS, KFS>
1940where
1941 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
1942 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1943 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1944 CLS: WriteStream<CopperList<P>> + 'static,
1945 KFS: WriteStream<KeyFrame> + 'static,
1946{
1947 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1948 let Self {
1949 clock,
1950 config,
1951 mission,
1952 subsystem,
1953 instance_id,
1954 resources,
1955 parts,
1956 copperlist_sink,
1957 keyframe_sink,
1958 output_requirements,
1959 } = self;
1960 let mut resources =
1961 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1962
1963 let graph = config.get_graph(Some(mission))?;
1964 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1965 .get_all_nodes()
1966 .iter()
1967 .map(|(_, node)| node.get_instance_config())
1968 .collect();
1969
1970 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
1971
1972 let execution_probe = RuntimeExecutionProbe::default();
1973 let monitor_metadata = CuMonitoringMetadata::new(
1974 CompactString::from(mission),
1975 parts.monitored_components,
1976 parts.culist_component_mapping,
1977 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1978 build_monitor_topology(config, mission)?,
1979 None,
1980 )?
1981 .with_subsystem_id(subsystem.id())
1982 .with_instance_id(instance_id);
1983 let monitor_runtime = CuMonitoringRuntime::unavailable();
1984 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1985 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1986
1987 let copperlist_sink = output_requirements
1988 .completed_copperlists()
1989 .then(|| Box::new(copperlist_sink) as Box<CompletedCopperListSink<P>>);
1990 let keyframe_sink = output_requirements
1991 .keyframes()
1992 .then(|| Box::new(keyframe_sink) as Box<CompletedKeyFrameSink>);
1993 let keyframe_interval = config
1994 .logging
1995 .as_ref()
1996 .and_then(|logging| logging.keyframe_interval)
1997 .unwrap_or(DEFAULT_KEYFRAME_INTERVAL);
1998
1999 let copperlists_manager = CopperListsManager::new(copperlist_sink)?;
2000 #[cfg(target_os = "none")]
2001 {
2002 let cl_size = core::mem::size_of::<CopperList<P>>();
2003 let total_bytes = cl_size.saturating_mul(NBCL);
2004 info!(
2005 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
2006 NBCL, cl_size, total_bytes
2007 );
2008 }
2009
2010 let keyframes_manager = KeyFramesManager::new(keyframe_sink, keyframe_interval)?;
2011
2012 let runtime_config = config.runtime.clone().unwrap_or_default();
2013 runtime_config.validate()?;
2014
2015 Ok(CuRuntime {
2016 subsystem_code: subsystem.code(),
2017 instance_id,
2018 tasks,
2019 bridges,
2020 resources,
2021 monitor,
2022 execution_probe,
2023 clock,
2024 copperlists_manager,
2025 keyframes_manager,
2026 runtime_config,
2027 })
2028 }
2029}
2030
2031#[derive(Clone, Encode, Decode)]
2036pub struct KeyFrame {
2037 pub culistid: u64,
2039 pub timestamp: CuTime,
2041 pub serialized_tasks: Vec<u8>,
2043}
2044
2045impl KeyFrame {
2046 fn new() -> Self {
2047 KeyFrame {
2048 culistid: 0,
2049 timestamp: CuTime::default(),
2050 serialized_tasks: KEYFRAME_PAYLOAD_HEADER.to_vec(),
2051 }
2052 }
2053
2054 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
2056 self.culistid = culistid;
2057 self.timestamp = timestamp;
2058 self.serialized_tasks.clear();
2059 self.serialized_tasks
2060 .extend_from_slice(KEYFRAME_PAYLOAD_HEADER);
2061 }
2062
2063 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
2065 let cfg = bincode::config::standard();
2066 let start = self.serialized_tasks.len();
2067 let payload_offset =
2068 start
2069 .checked_add(KEYFRAME_FRAME_HEADER_LEN)
2070 .ok_or(EncodeError::Other(
2071 "keyframe component frame offset overflow",
2072 ))?;
2073 if payload_offset > self.serialized_tasks.capacity() {
2074 return Err(EncodeError::UnexpectedEnd);
2075 }
2076
2077 self.serialized_tasks.resize(payload_offset, 0);
2078 let length_offset = start;
2079 self.serialized_tasks[length_offset..payload_offset].fill(0);
2080
2081 let mut encoder =
2082 EncoderImpl::<_, _>::new(PreallocatedVecWriter(&mut self.serialized_tasks), cfg);
2083 if let Err(error) = BincodeAdapter(task).encode(&mut encoder) {
2084 self.serialized_tasks.truncate(start);
2085 return Err(error);
2086 }
2087 let payload_len = encoder.into_writer().0.len() - payload_offset;
2088 let payload_len = u32::try_from(payload_len).map_err(|_| {
2089 self.serialized_tasks.truncate(start);
2090 EncodeError::OtherString(
2091 "keyframe component snapshot exceeds the u32 frame limit".to_string(),
2092 )
2093 })?;
2094 self.serialized_tasks
2095 .truncate(payload_offset + payload_len as usize);
2096 self.serialized_tasks[length_offset..payload_offset]
2097 .copy_from_slice(&payload_len.to_le_bytes());
2098 Ok(self.serialized_tasks.len() - start)
2099 }
2100}
2101
2102const KEYFRAME_PAYLOAD_MAGIC: &[u8; 4] = b"CUKF";
2103const KEYFRAME_PAYLOAD_VERSION: u8 = 1;
2104const KEYFRAME_PAYLOAD_HEADER: &[u8; 5] = b"CUKF\x01";
2105const KEYFRAME_FRAME_HEADER_LEN: usize = 4;
2106
2107#[doc(hidden)]
2109pub struct KeyFramePayloadReader<'a> {
2110 remaining: &'a [u8],
2111}
2112
2113impl<'a> KeyFramePayloadReader<'a> {
2114 pub fn new(keyframe: &'a KeyFrame) -> CuResult<Self> {
2116 let payload = keyframe.serialized_tasks.as_slice();
2117 if payload.len() < KEYFRAME_PAYLOAD_HEADER.len()
2118 || payload[..KEYFRAME_PAYLOAD_MAGIC.len()] != *KEYFRAME_PAYLOAD_MAGIC
2119 {
2120 return Err(CuError::from(
2121 "Unsupported legacy keyframe payload: expected framed format version 1",
2122 ));
2123 }
2124 let version = payload[KEYFRAME_PAYLOAD_MAGIC.len()];
2125 if version != KEYFRAME_PAYLOAD_VERSION {
2126 return Err(CuError::from(format!(
2127 "Unsupported keyframe payload version {version}; expected {KEYFRAME_PAYLOAD_VERSION}"
2128 )));
2129 }
2130 Ok(Self {
2131 remaining: &payload[KEYFRAME_PAYLOAD_HEADER.len()..],
2132 })
2133 }
2134
2135 pub fn next_frame(&mut self) -> CuResult<&'a [u8]> {
2137 if self.remaining.len() < KEYFRAME_FRAME_HEADER_LEN {
2138 return Err(CuError::from("Keyframe ended before next component frame"));
2139 }
2140 let payload_len = u32::from_le_bytes(
2141 self.remaining[..KEYFRAME_FRAME_HEADER_LEN]
2142 .try_into()
2143 .map_err(|_| CuError::from("Invalid keyframe component frame length"))?,
2144 ) as usize;
2145 let frame_end = KEYFRAME_FRAME_HEADER_LEN
2146 .checked_add(payload_len)
2147 .ok_or_else(|| CuError::from("Keyframe component frame length overflow"))?;
2148 if frame_end > self.remaining.len() {
2149 return Err(CuError::from("Keyframe component frame is truncated"));
2150 }
2151 let payload = &self.remaining[KEYFRAME_FRAME_HEADER_LEN..frame_end];
2152 self.remaining = &self.remaining[frame_end..];
2153 Ok(payload)
2154 }
2155
2156 pub fn finish(self) -> CuResult<()> {
2158 if self.remaining.is_empty() {
2159 Ok(())
2160 } else {
2161 Err(CuError::from("Keyframe contains trailing component data"))
2162 }
2163 }
2164}
2165
2166struct FrameSliceReader<'a> {
2167 remaining: &'a [u8],
2168}
2169
2170impl Reader for FrameSliceReader<'_> {
2171 fn read(&mut self, bytes: &mut [u8]) -> Result<(), DecodeError> {
2172 if bytes.len() > self.remaining.len() {
2173 return Err(DecodeError::UnexpectedEnd {
2174 additional: bytes.len() - self.remaining.len(),
2175 });
2176 }
2177 let (read, remaining) = self.remaining.split_at(bytes.len());
2178 bytes.copy_from_slice(read);
2179 self.remaining = remaining;
2180 Ok(())
2181 }
2182
2183 fn peek_read(&mut self, length: usize) -> Option<&[u8]> {
2184 self.remaining.get(..length)
2185 }
2186
2187 fn consume(&mut self, length: usize) {
2188 self.remaining = self.remaining.get(length..).unwrap_or_default();
2189 }
2190}
2191
2192#[doc(hidden)]
2194pub fn thaw_keyframe_component(item: &mut impl Freezable, frame: &[u8]) -> CuResult<()> {
2195 let reader = FrameSliceReader { remaining: frame };
2196 let mut decoder = DecoderImpl::new(reader, bincode::config::standard(), ());
2197 item.thaw(&mut decoder)
2198 .map_err(|error| CuError::from(format!("Failed to thaw keyframe component: {error}")))?;
2199 let trailing = decoder.reader().remaining.len();
2200 if trailing != 0 {
2201 return Err(CuError::from(format!(
2202 "Keyframe component snapshot has {} trailing bytes",
2203 trailing
2204 )));
2205 }
2206 Ok(())
2207}
2208
2209#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2211pub enum RuntimeLifecycleConfigSource {
2212 ProgrammaticOverride,
2213 ExternalFile,
2214 BundledDefault,
2215}
2216
2217#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2219pub struct RuntimeLifecycleStackInfo {
2220 pub app_name: String,
2221 pub app_version: String,
2222 pub git_commit: Option<String>,
2223 pub git_dirty: Option<bool>,
2224 pub subsystem_id: Option<String>,
2225 pub subsystem_code: u16,
2226 pub instance_id: u32,
2227}
2228
2229#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2231pub enum RuntimeLifecycleEvent {
2232 Instantiated {
2233 config_source: RuntimeLifecycleConfigSource,
2234 effective_config_ron: String,
2235 stack: RuntimeLifecycleStackInfo,
2236 },
2237 MissionStarted {
2238 mission: String,
2239 },
2240 MissionStopped {
2241 mission: String,
2242 reason: String,
2245 },
2246 Panic {
2248 message: String,
2249 file: Option<String>,
2250 line: Option<u32>,
2251 column: Option<u32>,
2252 },
2253 ShutdownCompleted,
2254}
2255
2256#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
2258pub struct RuntimeLifecycleRecord {
2259 pub timestamp: CuTime,
2260 pub event: RuntimeLifecycleEvent,
2261}
2262
2263#[doc(hidden)]
2265pub type RuntimeLifecycleSink = SemanticRecordSink<RuntimeLifecycleRecord>;
2266
2267impl<
2268 CT,
2269 CB,
2270 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
2271 M: CuMonitor,
2272 const NBCL: usize,
2273> CuRuntime<CT, CB, P, M, NBCL>
2274{
2275 #[inline]
2279 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
2280 self.execution_probe.record(marker);
2281 }
2282
2283 #[inline]
2288 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
2289 #[cfg(feature = "std")]
2290 {
2291 self.execution_probe.as_ref()
2292 }
2293
2294 #[cfg(not(feature = "std"))]
2295 {
2296 &self.execution_probe
2297 }
2298 }
2299}
2300
2301#[derive(Debug, PartialEq, Eq, Clone, Copy)]
2306pub enum CuTaskType {
2307 Source,
2308 Regular,
2309 Sink,
2310}
2311
2312impl From<TaskKind> for CuTaskType {
2313 fn from(value: TaskKind) -> Self {
2314 match value {
2315 TaskKind::Source => CuTaskType::Source,
2316 TaskKind::Regular => CuTaskType::Regular,
2317 TaskKind::Sink => CuTaskType::Sink,
2318 }
2319 }
2320}
2321
2322#[derive(Debug, Clone)]
2323pub struct CuOutputPack {
2324 pub culist_index: u32,
2325 pub msg_types: Vec<String>,
2326 pub src_channels: Vec<Option<String>>,
2334}
2335
2336#[derive(Debug, Clone)]
2337pub struct CuInputMsg {
2338 pub culist_index: u32,
2339 pub msg_type: String,
2340 pub src_port: usize,
2341 pub edge_id: usize,
2342 pub connection_order: usize,
2343}
2344
2345#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2353pub enum CuStepPhase {
2354 #[default]
2356 Whole,
2357 AnytimeBase,
2360 AnytimeRefine,
2362}
2363
2364pub struct CuExecutionStep {
2366 pub node_id: NodeId,
2368 pub node: Node,
2370 pub task_type: CuTaskType,
2372 pub phase: CuStepPhase,
2375
2376 pub input_msg_indices_types: Vec<CuInputMsg>,
2379
2380 pub output_msg_pack: Option<CuOutputPack>,
2383}
2384
2385impl Debug for CuExecutionStep {
2386 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
2387 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
2388 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
2389 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
2390 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
2391 f.write_str(
2392 format!(
2393 " input_msg_types: {:?}\n",
2394 self.input_msg_indices_types
2395 )
2396 .as_str(),
2397 )?;
2398 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
2399 Ok(())
2400 }
2401}
2402
2403pub struct CuExecutionLoop {
2408 pub steps: Vec<CuExecutionUnit>,
2409 pub loop_count: Option<u32>,
2410}
2411
2412impl Debug for CuExecutionLoop {
2413 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
2414 f.write_str("CuExecutionLoop:\n")?;
2415 for step in &self.steps {
2416 match step {
2417 CuExecutionUnit::Step(step) => {
2418 step.fmt(f)?;
2419 }
2420 CuExecutionUnit::Loop(l) => {
2421 l.fmt(f)?;
2422 }
2423 }
2424 }
2425
2426 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
2427 Ok(())
2428 }
2429}
2430
2431#[derive(Debug)]
2433pub enum CuExecutionUnit {
2434 Step(Box<CuExecutionStep>),
2435 Loop(CuExecutionLoop),
2436}
2437
2438pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
2439 let node = graph
2440 .get_node(node_id)
2441 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
2442
2443 if node.get_flavor() == crate::config::Flavor::Task {
2444 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
2445 }
2446
2447 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
2448 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
2449 Ok(match (has_inputs, has_outputs) {
2450 (false, true) => CuTaskType::Source,
2451 (true, false) => CuTaskType::Sink,
2452 _ => CuTaskType::Regular,
2453 })
2454}
2455
2456pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
2462 let order = Linearity.plan(graph)?;
2463 check_order(graph, &order)?;
2464 plan_from_order(graph, &order)
2465}
2466
2467pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
2485 loop {
2486 let Some(base_pos) = plan.steps.iter().position(|unit| {
2489 matches!(
2490 unit,
2491 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
2492 && step.node.anytime().is_some()
2493 && !step.node.is_background()
2494 )
2495 }) else {
2496 return Ok(());
2497 };
2498
2499 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
2500 unreachable!("position() only matches steps");
2501 };
2502 let anytime = base_step
2503 .node
2504 .anytime()
2505 .expect("position() only matches anytime nodes");
2506 let Some(max_refines) = anytime.max_refines else {
2509 return Err(CuError::from(format!(
2510 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
2511 base_step.node.get_id()
2512 )));
2513 };
2514 base_step.phase = CuStepPhase::AnytimeBase;
2515 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
2516 CuError::from(format!(
2517 "Task '{}': an anytime task needs an output to refine.",
2518 base_step.node.get_id()
2519 ))
2520 })?;
2521 let output_index = output_pack.culist_index;
2522 let node_id = base_step.node_id;
2523 let node = base_step.node.clone();
2524 let task_type = base_step.task_type;
2525
2526 let refine_step = || {
2527 CuExecutionUnit::Step(Box::new(CuExecutionStep {
2528 node_id,
2529 node: node.clone(),
2530 task_type,
2531 phase: CuStepPhase::AnytimeRefine,
2532 input_msg_indices_types: Vec::new(),
2533 output_msg_pack: Some(output_pack.clone()),
2534 }))
2535 };
2536
2537 let consumer_pos = plan.steps[base_pos + 1..]
2540 .iter()
2541 .position(|unit| {
2542 matches!(
2543 unit,
2544 CuExecutionUnit::Step(step) if step
2545 .input_msg_indices_types
2546 .iter()
2547 .any(|input| input.culist_index == output_index)
2548 )
2549 })
2550 .map(|offset| base_pos + 1 + offset)
2551 .unwrap_or(base_pos + 1);
2552
2553 let mut tail = plan.steps.split_off(base_pos + 1);
2554 let suffix = tail.split_off(consumer_pos - base_pos - 1);
2555 let gap = tail;
2556
2557 let mut remaining = max_refines.max(1);
2559 remaining -= 1;
2560 plan.steps.push(refine_step());
2561 for gap_unit in gap {
2562 plan.steps.push(gap_unit);
2563 if remaining > 0 {
2564 remaining -= 1;
2565 plan.steps.push(refine_step());
2566 }
2567 }
2568 for _ in 0..remaining {
2569 plan.steps.push(refine_step());
2570 }
2571 plan.steps.extend(suffix);
2572 }
2573}
2574
2575#[cfg(test)]
2577mod tests {
2578 use super::*;
2579 use crate::config::Node;
2580 use crate::context::CuContext;
2581 use crate::cutask::CuSinkTask;
2582 use crate::cutask::{CuSrcTask, Freezable};
2583 use crate::monitoring::NoMonitor;
2584 use crate::reflect::Reflect;
2585 use bincode::Encode;
2586 use core::cell::Cell;
2587 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
2588 use serde_derive::{Deserialize, Serialize};
2589 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2590 use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
2591 #[cfg(feature = "std")]
2592 use std::sync::{Arc, Mutex};
2593
2594 struct CountingSnapshot<'a> {
2595 calls: &'a Cell<usize>,
2596 value: u32,
2597 fail: bool,
2598 }
2599
2600 impl Freezable for CountingSnapshot<'_> {
2601 fn freeze<E: bincode::enc::Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
2602 self.calls.set(self.calls.get() + 1);
2603 self.value.encode(encoder)?;
2604 if self.fail {
2605 Err(EncodeError::OtherString(
2606 "intentional freeze failure".to_string(),
2607 ))
2608 } else {
2609 Ok(())
2610 }
2611 }
2612 }
2613
2614 #[derive(Default)]
2615 struct SnapshotValue(u32);
2616
2617 impl Freezable for SnapshotValue {
2618 fn thaw<D: bincode::de::Decoder>(&mut self, decoder: &mut D) -> Result<(), DecodeError> {
2619 self.0 = u32::decode(decoder)?;
2620 Ok(())
2621 }
2622 }
2623
2624 #[test]
2625 fn keyframe_frames_freeze_once_and_roll_back_only_failed_frame() {
2626 let calls = Cell::new(0);
2627 let mut keyframe = KeyFrame::new();
2628 keyframe
2629 .serialized_tasks
2630 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2631 .unwrap();
2632 keyframe.reset(7, CuTime::from_nanos(70));
2633 keyframe
2634 .add_frozen_task(&CountingSnapshot {
2635 calls: &calls,
2636 value: 11,
2637 fail: false,
2638 })
2639 .unwrap();
2640 let committed_len = keyframe.serialized_tasks.len();
2641
2642 let failing = CountingSnapshot {
2643 calls: &calls,
2644 value: 99,
2645 fail: true,
2646 };
2647 assert!(keyframe.add_frozen_task(&failing).is_err());
2648 assert_eq!(keyframe.serialized_tasks.len(), committed_len);
2649
2650 keyframe
2651 .add_frozen_task(&CountingSnapshot {
2652 calls: &calls,
2653 value: 22,
2654 fail: false,
2655 })
2656 .unwrap();
2657 assert_eq!(calls.get(), 3, "each append must call freeze exactly once");
2658 let first_payload_len = bincode::encode_to_vec(11u32, bincode::config::standard())
2659 .unwrap()
2660 .len();
2661 let second_payload_len = bincode::encode_to_vec(22u32, bincode::config::standard())
2662 .unwrap()
2663 .len();
2664 assert_eq!(
2665 keyframe.serialized_tasks.len(),
2666 KEYFRAME_PAYLOAD_HEADER.len()
2667 + 2 * KEYFRAME_FRAME_HEADER_LEN
2668 + first_payload_len
2669 + second_payload_len,
2670 "component frames carry only a length prefix"
2671 );
2672
2673 let mut frames = KeyFramePayloadReader::new(&keyframe).unwrap();
2674 let mut first = SnapshotValue::default();
2675 thaw_keyframe_component(&mut first, frames.next_frame().unwrap()).unwrap();
2676 let mut second = SnapshotValue::default();
2677 thaw_keyframe_component(&mut second, frames.next_frame().unwrap()).unwrap();
2678 frames.finish().unwrap();
2679 assert_eq!((first.0, second.0), (11, 22));
2680 }
2681
2682 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
2683 #[test]
2684 fn preallocated_keyframe_append_does_not_allocate() {
2685 let calls = Cell::new(0);
2686 let mut keyframe = KeyFrame::new();
2687 keyframe
2688 .serialized_tasks
2689 .try_reserve_exact(MIN_KEYFRAME_CAPTURE_CAPACITY)
2690 .unwrap();
2691 keyframe.reset(3, CuTime::from_nanos(30));
2692
2693 let allocations = crate::monitoring::ScopedAllocCounter::new();
2694 keyframe
2695 .add_frozen_task(&CountingSnapshot {
2696 calls: &calls,
2697 value: 42,
2698 fail: false,
2699 })
2700 .unwrap();
2701
2702 assert_eq!(allocations.allocated(), 0);
2703 assert_eq!(calls.get(), 1);
2704 }
2705
2706 #[test]
2707 fn keyframe_reader_rejects_legacy_payload_clearly() {
2708 let keyframe = KeyFrame {
2709 culistid: 0,
2710 timestamp: CuTime::default(),
2711 serialized_tasks: vec![0, 1, 2],
2712 };
2713 let error = match KeyFramePayloadReader::new(&keyframe) {
2714 Ok(_) => panic!("legacy payload unexpectedly accepted"),
2715 Err(error) => error,
2716 };
2717 assert!(error.to_string().contains("legacy keyframe payload"));
2718 }
2719
2720 #[derive(Reflect)]
2721 pub struct TestSource {}
2722
2723 impl Freezable for TestSource {}
2724
2725 impl CuSrcTask for TestSource {
2726 type Resources<'r> = ();
2727 type Output<'m> = ();
2728 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2729 where
2730 Self: Sized,
2731 {
2732 Ok(Self {})
2733 }
2734
2735 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
2736 Ok(())
2737 }
2738 }
2739
2740 #[derive(Reflect)]
2741 pub struct TestSink {}
2742
2743 impl Freezable for TestSink {}
2744
2745 impl CuSinkTask for TestSink {
2746 type Resources<'r> = ();
2747 type Input<'m> = ();
2748
2749 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2750 where
2751 Self: Sized,
2752 {
2753 Ok(Self {})
2754 }
2755
2756 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
2757 Ok(())
2758 }
2759 }
2760
2761 type Tasks = (TestSource, TestSink);
2763 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
2764 const TEST_NBCL: usize = 2;
2765
2766 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2767 struct Msgs(());
2768
2769 impl ErasedCuStampedDataSet for Msgs {
2770 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2771 Vec::new()
2772 }
2773 }
2774
2775 impl MatchingTasks for Msgs {
2776 fn get_all_task_ids() -> &'static [&'static str] {
2777 &[]
2778 }
2779 }
2780
2781 impl CuListZeroedInit for Msgs {
2782 fn init_zeroed(&mut self) {}
2783 }
2784
2785 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2786 struct IntMsgs(i32);
2787
2788 impl ErasedCuStampedDataSet for IntMsgs {
2789 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2790 Vec::new()
2791 }
2792 }
2793
2794 impl MatchingTasks for IntMsgs {
2795 fn get_all_task_ids() -> &'static [&'static str] {
2796 &[]
2797 }
2798 }
2799
2800 impl CuListZeroedInit for IntMsgs {
2801 fn init_zeroed(&mut self) {}
2802 }
2803
2804 #[cfg(feature = "std")]
2805 fn tasks_instanciator(
2806 all_instances_configs: Vec<Option<&ComponentConfig>>,
2807 _resources: &mut ResourceManager,
2808 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
2809 ) -> CuResult<Tasks> {
2810 Ok((
2811 TestSource::new(all_instances_configs[0], ())?,
2812 TestSink::new(all_instances_configs[1], ())?,
2813 ))
2814 }
2815
2816 #[cfg(not(feature = "std"))]
2817 fn tasks_instanciator(
2818 all_instances_configs: Vec<Option<&ComponentConfig>>,
2819 _resources: &mut ResourceManager,
2820 ) -> CuResult<Tasks> {
2821 Ok((
2822 TestSource::new(all_instances_configs[0], ())?,
2823 TestSink::new(all_instances_configs[1], ())?,
2824 ))
2825 }
2826
2827 fn monitor_instanciator(
2828 _config: &CuConfig,
2829 metadata: CuMonitoringMetadata,
2830 runtime: CuMonitoringRuntime,
2831 ) -> NoMonitor {
2832 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
2833 }
2834
2835 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
2836 Ok(())
2837 }
2838
2839 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
2840 Ok(ResourceManager::new(&[]))
2841 }
2842
2843 #[derive(Debug)]
2844 struct FakeWriter {}
2845
2846 impl<E: Encode> WriteStream<E> for FakeWriter {
2847 fn log(&mut self, _obj: &E) -> CuResult<()> {
2848 Ok(())
2849 }
2850 }
2851
2852 #[cfg(not(feature = "async-cl-io"))]
2853 #[derive(Debug)]
2854 struct RecordingSyncWriter {
2855 ids: Arc<Mutex<Vec<u64>>>,
2856 last_log_bytes: usize,
2857 fail_on: Option<u64>,
2858 }
2859
2860 #[cfg(not(feature = "async-cl-io"))]
2861 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
2862 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2863 self.ids.lock().unwrap().push(culist.id);
2864 if self.fail_on == Some(culist.id) {
2865 return Err(CuError::from(format!(
2866 "logger failed for CopperList #{}",
2867 culist.id
2868 )));
2869 }
2870 Ok(())
2871 }
2872
2873 fn last_log_bytes(&self) -> Option<usize> {
2874 Some(self.last_log_bytes)
2875 }
2876 }
2877
2878 #[cfg(feature = "std")]
2879 #[derive(Debug)]
2880 struct RecordingSemanticSink {
2881 ids: Arc<Mutex<Vec<u64>>>,
2882 }
2883
2884 #[cfg(feature = "std")]
2885 impl WriteStream<CopperList<IntMsgs>> for RecordingSemanticSink {
2886 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2887 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
2888 self.ids.lock().unwrap().push(culist.id);
2889 Ok(())
2890 }
2891
2892 fn last_log_bytes(&self) -> Option<usize> {
2893 Some(23)
2894 }
2895 }
2896
2897 #[cfg(feature = "std")]
2898 impl WriteStream<CopperList<Msgs>> for RecordingSemanticSink {
2899 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
2900 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
2901 self.ids.lock().unwrap().push(culist.id);
2902 Ok(())
2903 }
2904
2905 fn last_log_bytes(&self) -> Option<usize> {
2906 Some(23)
2907 }
2908 }
2909
2910 #[cfg(feature = "std")]
2911 #[derive(Debug)]
2912 struct RecordingKeyFrameSink {
2913 ids: Arc<Mutex<Vec<u64>>>,
2914 }
2915
2916 #[cfg(feature = "std")]
2917 impl WriteStream<KeyFrame> for RecordingKeyFrameSink {
2918 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
2919 self.ids.lock().unwrap().push(keyframe.culistid);
2920 Ok(())
2921 }
2922
2923 fn last_log_bytes(&self) -> Option<usize> {
2924 Some(29)
2925 }
2926 }
2927
2928 #[test]
2929 fn test_runtime_instantiation() {
2930 let mut config = CuConfig::default();
2931 let graph = config.get_graph_mut(None).unwrap();
2932 graph.add_node(Node::new("a", "TestSource")).unwrap();
2933 graph.add_node(Node::new("b", "TestSink")).unwrap();
2934 graph.connect(0, 1, "()").unwrap();
2935 let runtime: CuResult<TestRuntime> =
2936 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2937 RobotClock::default(),
2938 &config,
2939 crate::config::DEFAULT_MISSION_ID,
2940 CuRuntimeParts::new(
2941 tasks_instanciator,
2942 &[],
2943 &[],
2944 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2945 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2946 monitor_instanciator,
2947 bridges_instanciator,
2948 ),
2949 FakeWriter {},
2950 FakeWriter {},
2951 OutputRequirements::new(true, true),
2952 )
2953 .try_with_resources_instantiator(resources_instanciator)
2954 .and_then(|builder| builder.build());
2955 assert!(runtime.is_ok());
2956 }
2957
2958 #[cfg(feature = "std")]
2959 #[test]
2960 fn downstream_requirements_are_independent_from_local_logging() {
2961 let mut config = CuConfig::default();
2962 config.logging = Some(crate::config::LoggingConfig {
2963 enable_task_logging: false,
2964 enable_keyframe_logging: false,
2965 keyframe_interval: Some(1),
2966 ..Default::default()
2967 });
2968 let graph = config.get_graph_mut(None).unwrap();
2969 graph.add_node(Node::new("a", "TestSource")).unwrap();
2970 graph.add_node(Node::new("b", "TestSink")).unwrap();
2971 graph.connect(0, 1, "()").unwrap();
2972
2973 let copperlist_ids = Arc::new(Mutex::new(Vec::new()));
2974 let keyframe_ids = Arc::new(Mutex::new(Vec::new()));
2975 let mut runtime: TestRuntime =
2976 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2977 RobotClock::default(),
2978 &config,
2979 crate::config::DEFAULT_MISSION_ID,
2980 CuRuntimeParts::new(
2981 tasks_instanciator,
2982 &[],
2983 &[],
2984 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2985 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2986 monitor_instanciator,
2987 bridges_instanciator,
2988 ),
2989 RecordingSemanticSink {
2990 ids: copperlist_ids.clone(),
2991 },
2992 RecordingKeyFrameSink {
2993 ids: keyframe_ids.clone(),
2994 },
2995 OutputRequirements::new(true, false).union(OutputRequirements::new(false, true)),
2996 )
2997 .try_with_resources_instantiator(resources_instanciator)
2998 .and_then(|builder| builder.build())
2999 .unwrap();
3000
3001 let copperlist = runtime.copperlists_manager.create().unwrap();
3002 copperlist.change_state(CopperListState::Processing);
3003 runtime.copperlists_manager.end_of_processing(0).unwrap();
3004 runtime.copperlists_manager.finish_pending().unwrap();
3005
3006 runtime
3007 .keyframes_manager
3008 .try_reset(0, &runtime.clock)
3009 .unwrap();
3010 runtime.keyframes_manager.end_of_processing(0).unwrap();
3011 runtime.keyframes_manager.finish_pending().unwrap();
3012
3013 assert_eq!(*copperlist_ids.lock().unwrap(), vec![0]);
3014 assert_eq!(*keyframe_ids.lock().unwrap(), vec![0]);
3015 }
3016
3017 #[test]
3018 fn test_rate_target_period_rejects_zero() {
3019 let err = rate_target_period(0).expect_err("zero rate target should fail");
3020 assert!(
3021 err.to_string()
3022 .contains("Runtime rate target cannot be zero"),
3023 "unexpected error: {err}"
3024 );
3025 }
3026
3027 #[test]
3028 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
3029 let (clock, mock) = RobotClock::mock();
3030 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3031 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
3032
3033 mock.set_value(10_000_000);
3034 limiter.mark_tick(&clock);
3035
3036 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
3037 }
3038
3039 #[test]
3040 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
3041 let (clock, mock) = RobotClock::mock();
3042 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
3043
3044 mock.set_value(35_000_000);
3045 limiter.mark_tick(&clock);
3046
3047 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
3048 }
3049
3050 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
3051 #[test]
3052 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
3053 let (clock, _) = RobotClock::mock();
3054 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
3055 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
3056
3057 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
3058 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
3059 }
3060
3061 #[cfg(not(feature = "async-cl-io"))]
3062 #[test]
3063 fn test_copperlists_manager_lifecycle() {
3064 let mut config = CuConfig::default();
3065 let graph = config.get_graph_mut(None).unwrap();
3066 graph.add_node(Node::new("a", "TestSource")).unwrap();
3067 graph.add_node(Node::new("b", "TestSink")).unwrap();
3068 graph.connect(0, 1, "()").unwrap();
3069
3070 let mut runtime: TestRuntime =
3071 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
3072 RobotClock::default(),
3073 &config,
3074 crate::config::DEFAULT_MISSION_ID,
3075 CuRuntimeParts::new(
3076 tasks_instanciator,
3077 &[],
3078 &[],
3079 #[cfg(all(feature = "std", feature = "parallel-rt"))]
3080 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
3081 monitor_instanciator,
3082 bridges_instanciator,
3083 ),
3084 FakeWriter {},
3085 FakeWriter {},
3086 OutputRequirements::new(true, true),
3087 )
3088 .try_with_resources_instantiator(resources_instanciator)
3089 .and_then(|builder| builder.build())
3090 .unwrap();
3091
3092 {
3094 let copperlists = &mut runtime.copperlists_manager;
3095 let culist0 = copperlists
3096 .create()
3097 .expect("Ran out of space for copper lists");
3098 let id = culist0.id;
3099 assert_eq!(id, 0);
3100 culist0.change_state(CopperListState::Processing);
3101 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3102 }
3103
3104 {
3105 let copperlists = &mut runtime.copperlists_manager;
3106 let culist1 = copperlists
3107 .create()
3108 .expect("Ran out of space for copper lists");
3109 let id = culist1.id;
3110 assert_eq!(id, 1);
3111 culist1.change_state(CopperListState::Processing);
3112 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3113 }
3114
3115 {
3116 let copperlists = &mut runtime.copperlists_manager;
3117 let culist2 = copperlists.create();
3118 assert!(culist2.is_err());
3119 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3120 let _ = copperlists.end_of_processing(1);
3122 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3123 }
3124
3125 {
3127 let copperlists = &mut runtime.copperlists_manager;
3128 let culist2 = copperlists
3129 .create()
3130 .expect("Ran out of space for copper lists");
3131 let id = culist2.id;
3132 assert_eq!(id, 2);
3133 culist2.change_state(CopperListState::Processing);
3134 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3135 let _ = copperlists.end_of_processing(0);
3137 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3139
3140 let _ = copperlists.end_of_processing(2);
3142 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3145 }
3146 }
3147
3148 #[cfg(not(feature = "async-cl-io"))]
3149 #[test]
3150 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
3151 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3152
3153 assert_eq!(copperlists.next_cl_id(), 0);
3154 assert_eq!(copperlists.last_cl_id(), 0);
3155 assert!(copperlists.peek().is_none());
3156
3157 {
3158 let culist = copperlists.create().unwrap();
3159 culist.msgs.0 = 11;
3160 assert_eq!(culist.id, 0);
3161 assert_eq!(culist.get_state(), CopperListState::Initialized);
3162 }
3163
3164 assert_eq!(copperlists.next_cl_id(), 1);
3165 assert_eq!(copperlists.last_cl_id(), 0);
3166 let peeked = copperlists.peek().unwrap();
3167 assert_eq!(peeked.id, 0);
3168 assert_eq!(peeked.msgs.0, 11);
3169 assert_eq!(peeked.get_state(), CopperListState::Initialized);
3170 }
3171
3172 #[cfg(not(feature = "async-cl-io"))]
3173 #[test]
3174 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
3175 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3176
3177 {
3178 let culist = copperlists.create().unwrap();
3179 culist.msgs.0 = 41;
3180 culist.change_state(CopperListState::Processing);
3181 assert_eq!(culist.id, 0);
3182 }
3183
3184 copperlists.end_of_processing(0).unwrap();
3185 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3186
3187 let reused = copperlists.create().unwrap();
3188 assert_eq!(reused.id, 1);
3189 assert_eq!(reused.get_state(), CopperListState::Initialized);
3190 assert_eq!(reused.msgs.0, 41);
3191 }
3192
3193 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
3194 #[test]
3195 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
3196 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
3197 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
3198
3199 {
3200 let culist = copperlists.create().unwrap();
3201 culist.msgs.0 = 10;
3202 culist.change_state(CopperListState::Processing);
3203 }
3204 {
3205 let culist = copperlists.create().unwrap();
3206 culist.msgs.0 = 20;
3207 culist.change_state(CopperListState::Processing);
3208 }
3209
3210 let _ = copperlists.end_of_processing(99);
3211 }
3212
3213 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
3214 #[test]
3215 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
3216 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
3217 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3218
3219 {
3220 let culist = copperlists.create().unwrap();
3221 culist.msgs.0 = 10;
3222 assert_eq!(culist.get_state(), CopperListState::Initialized);
3223 }
3224
3225 let _ = copperlists.end_of_processing(0);
3226 }
3227
3228 #[cfg(not(feature = "async-cl-io"))]
3229 #[test]
3230 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
3231 let ids = Arc::new(Mutex::new(Vec::new()));
3232 let mut copperlists =
3233 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
3234 ids: ids.clone(),
3235 last_log_bytes: 17,
3236 fail_on: None,
3237 })))
3238 .unwrap();
3239
3240 {
3241 let culist = copperlists.create().unwrap();
3242 culist.msgs.0 = 10;
3243 culist.change_state(CopperListState::Processing);
3244 }
3245 {
3246 let culist = copperlists.create().unwrap();
3247 culist.msgs.0 = 20;
3248 culist.change_state(CopperListState::Processing);
3249 }
3250
3251 copperlists.end_of_processing(0).unwrap();
3252 assert!(ids.lock().unwrap().is_empty());
3253 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3254
3255 copperlists.end_of_processing(1).unwrap();
3256
3257 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
3258 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3259 }
3260
3261 #[cfg(not(feature = "async-cl-io"))]
3262 #[test]
3263 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
3264 let ids = Arc::new(Mutex::new(Vec::new()));
3265 let mut copperlists =
3266 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
3267 ids: ids.clone(),
3268 last_log_bytes: 17,
3269 fail_on: None,
3270 })))
3271 .unwrap();
3272 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
3273
3274 {
3275 let culist = copperlists.create().unwrap();
3276 culist.msgs.0 = 10;
3277 culist.change_state(CopperListState::Processing);
3278 }
3279
3280 {
3281 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
3282 capture.select_slot(0);
3283 crate::monitoring::record_payload_handle_bytes(32);
3284 }
3285
3286 copperlists.end_of_processing(0).unwrap();
3287
3288 assert_eq!(*ids.lock().unwrap(), vec![0]);
3289 assert_eq!(copperlists.last_encoded_bytes, 17);
3290 assert_eq!(copperlists.last_handle_bytes, 32);
3291 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3292 }
3293
3294 #[cfg(feature = "std")]
3295 #[test]
3296 fn test_sync_manager_accepts_nonserializing_semantic_sink() {
3297 let ids = Arc::new(Mutex::new(Vec::new()));
3298 let mut copperlists =
3299 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSemanticSink {
3300 ids: ids.clone(),
3301 })))
3302 .unwrap();
3303
3304 let culist = copperlists.create().unwrap();
3305 culist.change_state(CopperListState::Processing);
3306 copperlists.end_of_processing(0).unwrap();
3307
3308 assert_eq!(*ids.lock().unwrap(), vec![0]);
3309 assert_eq!(copperlists.last_encoded_bytes, 23);
3310 assert_eq!(copperlists.last_handle_bytes, 0);
3311 }
3312
3313 #[cfg(feature = "std")]
3314 #[test]
3315 fn test_keyframe_manager_accepts_nonserializing_semantic_sink() {
3316 let ids = Arc::new(Mutex::new(Vec::new()));
3317 let mut keyframes = KeyFramesManager::new(
3318 Some(Box::new(RecordingKeyFrameSink { ids: ids.clone() })),
3319 1,
3320 )
3321 .unwrap();
3322
3323 keyframes.try_reset(7, &RobotClock::default()).unwrap();
3324 keyframes.end_of_processing(7).unwrap();
3325 keyframes.finish_pending().unwrap();
3326
3327 assert_eq!(*ids.lock().unwrap(), vec![7]);
3328 assert_eq!(keyframes.last_encoded_bytes, 29);
3329 }
3330
3331 #[cfg(not(feature = "async-cl-io"))]
3332 #[test]
3333 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
3334 let ids = Arc::new(Mutex::new(Vec::new()));
3335 let mut copperlists =
3336 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
3337 ids: ids.clone(),
3338 last_log_bytes: 17,
3339 fail_on: Some(0),
3340 })))
3341 .unwrap();
3342
3343 {
3344 let culist = copperlists.create().unwrap();
3345 culist.change_state(CopperListState::Processing);
3346 }
3347
3348 let err = copperlists.end_of_processing(0).unwrap_err();
3349
3350 assert!(
3351 err.to_string().contains("logger failed for CopperList #0"),
3352 "unexpected error: {err}"
3353 );
3354 assert_eq!(*ids.lock().unwrap(), vec![0]);
3355 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
3356 assert_eq!(copperlists.last_encoded_bytes, 0);
3357 assert_eq!(copperlists.last_handle_bytes, 0);
3358
3359 let peeked = copperlists.peek().unwrap();
3360 assert_eq!(peeked.id, 0);
3361 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
3362 }
3363
3364 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
3365 #[test]
3366 #[should_panic(
3367 expected = "sync boxed end_of_processing expected CopperList #7 to be Processing"
3368 )]
3369 fn test_sync_end_of_processing_boxed_wrong_state_panics_in_debug() {
3370 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3371 let culist = Box::new(CopperList::new(7, IntMsgs::default()));
3372
3373 let _ = copperlists.end_of_processing_boxed(culist);
3374 }
3375
3376 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3377 #[derive(Debug, Default)]
3378 struct RecordingWriter {
3379 ids: Arc<Mutex<Vec<u64>>>,
3380 }
3381
3382 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3383 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
3384 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3385 assert_eq!(culist.get_state(), CopperListState::BeingSerialized);
3386 self.ids.lock().unwrap().push(culist.id);
3387 std::thread::sleep(std::time::Duration::from_millis(2));
3388 Ok(())
3389 }
3390 }
3391
3392 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3393 #[derive(Debug)]
3394 struct BlockingWriter {
3395 ids: Arc<Mutex<Vec<u64>>>,
3396 started: SyncSender<()>,
3397 release: Arc<Mutex<Receiver<()>>>,
3398 }
3399
3400 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3401 impl WriteStream<CopperList<Msgs>> for BlockingWriter {
3402 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
3403 self.ids.lock().unwrap().push(culist.id);
3404 self.started
3405 .send(())
3406 .map_err(|_| CuError::from("failed to signal blocking writer start"))?;
3407 self.release
3408 .lock()
3409 .unwrap()
3410 .recv()
3411 .map_err(|_| CuError::from("failed to release blocking writer"))
3412 }
3413 }
3414
3415 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3416 #[derive(Debug)]
3417 struct BlockingKeyFrameWriter {
3418 ids: Arc<Mutex<Vec<u64>>>,
3419 started: SyncSender<()>,
3420 release: Arc<Mutex<Receiver<()>>>,
3421 }
3422
3423 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3424 impl WriteStream<KeyFrame> for BlockingKeyFrameWriter {
3425 fn log(&mut self, keyframe: &KeyFrame) -> CuResult<()> {
3426 self.ids.lock().unwrap().push(keyframe.culistid);
3427 self.started
3428 .send(())
3429 .map_err(|_| CuError::from("failed to signal blocking keyframe writer start"))?;
3430 self.release
3431 .lock()
3432 .unwrap()
3433 .recv()
3434 .map_err(|_| CuError::from("failed to release blocking keyframe writer"))
3435 }
3436 }
3437
3438 #[test]
3439 fn disabled_keyframes_allocate_no_capture_buffers() {
3440 let keyframes = KeyFramesManager::new(None, 1).unwrap();
3441
3442 assert!(keyframes.inner.is_none());
3443 assert!(!keyframes.captures_keyframe(0));
3444 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3445 {
3446 assert!(keyframes.spares.is_empty());
3447 assert!(keyframes.pending_producer.is_none());
3448 assert!(keyframes.worker_handle.is_none());
3449 }
3450 }
3451
3452 #[cfg(all(feature = "std", feature = "memory_monitoring"))]
3453 #[test]
3454 fn disabled_keyframe_manager_allocates_nothing() {
3455 let allocations = crate::monitoring::ScopedAllocCounter::new();
3456 let keyframes = KeyFramesManager::new(None, 1).unwrap();
3457
3458 assert_eq!(allocations.allocated(), 0);
3459 assert!(keyframes.inner.is_none());
3460 }
3461
3462 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3463 #[test]
3464 fn saturated_keyframe_handoff_skips_capture_before_freezing() {
3465 let ids = Arc::new(Mutex::new(Vec::new()));
3466 let (started_tx, started_rx) = sync_channel(1);
3467 let (release_tx, release_rx) = sync_channel(1);
3468 let mut keyframes = KeyFramesManager::new(
3469 Some(Box::new(BlockingKeyFrameWriter {
3470 ids: ids.clone(),
3471 started: started_tx,
3472 release: Arc::new(Mutex::new(release_rx)),
3473 })),
3474 1,
3475 )
3476 .unwrap();
3477 let freeze_calls = Cell::new(0);
3478 let snapshot = CountingSnapshot {
3479 calls: &freeze_calls,
3480 value: 42,
3481 fail: false,
3482 };
3483 keyframes.begin_capture_preallocation();
3484 keyframes.include_capture_capacity(&snapshot).unwrap();
3485 keyframes.finish_capture_preallocation().unwrap();
3486 freeze_calls.set(0);
3487
3488 keyframes.try_reset(0, &RobotClock::default()).unwrap();
3489 assert_ne!(keyframes.freeze_task(0, &snapshot).unwrap(), 0);
3490 keyframes.end_of_processing(0).unwrap();
3491 started_rx
3492 .recv_timeout(std::time::Duration::from_secs(1))
3493 .unwrap();
3494
3495 keyframes.try_reset(1, &RobotClock::default()).unwrap();
3496 assert_ne!(keyframes.freeze_task(1, &snapshot).unwrap(), 0);
3497 keyframes.end_of_processing(1).unwrap();
3498
3499 keyframes.try_reset(2, &RobotClock::default()).unwrap();
3500 assert_eq!(keyframes.freeze_task(2, &snapshot).unwrap(), 0);
3501 keyframes.end_of_processing(2).unwrap();
3502
3503 assert_eq!(freeze_calls.get(), 2);
3504 assert_eq!(keyframes.dropped_keyframes_total(), 1);
3505 assert_eq!(*ids.lock().unwrap(), vec![0]);
3506
3507 release_tx.send(()).unwrap();
3508 started_rx
3509 .recv_timeout(std::time::Duration::from_secs(1))
3510 .unwrap();
3511 release_tx.send(()).unwrap();
3512 keyframes.finish_pending().unwrap();
3513 assert_eq!(*ids.lock().unwrap(), vec![0, 1]);
3514 }
3515
3516 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3517 #[test]
3518 fn test_async_copperlists_manager_flushes_in_order() {
3519 let ids = Arc::new(Mutex::new(Vec::new()));
3520 let mut copperlists = CopperListsManager::<Msgs, 5>::new(Some(Box::new(RecordingWriter {
3521 ids: ids.clone(),
3522 })))
3523 .unwrap();
3524
3525 for expected_id in 0..4 {
3526 let culist = copperlists.create().unwrap();
3527 assert_eq!(culist.id, expected_id);
3528 culist.change_state(CopperListState::Processing);
3529 copperlists.end_of_processing(expected_id).unwrap();
3530 }
3531
3532 copperlists.finish_pending().unwrap();
3533 assert_eq!(copperlists.available_copper_lists().unwrap(), 5);
3534 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
3535 assert_eq!(copperlists.dropped_copperlists_total(), 0);
3536 }
3537
3538 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3539 #[test]
3540 fn test_async_handoff_drops_without_exhausting_execution_slot() {
3541 let ids = Arc::new(Mutex::new(Vec::new()));
3542 let (started_tx, started_rx) = sync_channel(1);
3543 let (release_tx, release_rx) = sync_channel(1);
3544 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
3545 ids: ids.clone(),
3546 started: started_tx,
3547 release: Arc::new(Mutex::new(release_rx)),
3548 })))
3549 .unwrap();
3550
3551 let first = copperlists.create().unwrap();
3552 first.change_state(CopperListState::Processing);
3553 copperlists.end_of_processing(0).unwrap();
3554 started_rx
3555 .recv_timeout(std::time::Duration::from_secs(1))
3556 .unwrap();
3557
3558 let second = copperlists.create().unwrap();
3559 second.change_state(CopperListState::Processing);
3560 copperlists.end_of_processing(1).unwrap();
3561
3562 assert_eq!(copperlists.dropped_copperlists_total(), 1);
3563 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3564 assert_eq!(*ids.lock().unwrap(), vec![0]);
3565
3566 release_tx.send(()).unwrap();
3567 copperlists.finish_pending().unwrap();
3568 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
3569 }
3570
3571 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3572 #[test]
3573 fn test_async_boxed_handoff_returns_dropped_copperlist_to_caller() {
3574 let ids = Arc::new(Mutex::new(Vec::new()));
3575 let (started_tx, started_rx) = sync_channel(1);
3576 let (release_tx, release_rx) = sync_channel(1);
3577 let mut copperlists = CopperListsManager::<Msgs, 2>::new(Some(Box::new(BlockingWriter {
3578 ids: ids.clone(),
3579 started: started_tx,
3580 release: Arc::new(Mutex::new(release_rx)),
3581 })))
3582 .unwrap();
3583
3584 let mut first = Box::new(CopperList::new(0, Msgs::default()));
3585 first.change_state(CopperListState::Processing);
3586 assert!(matches!(
3587 copperlists.end_of_processing_boxed(first).unwrap(),
3588 OwnedCopperListSubmission::Pending
3589 ));
3590 started_rx
3591 .recv_timeout(std::time::Duration::from_secs(1))
3592 .unwrap();
3593
3594 let mut second = Box::new(CopperList::new(1, Msgs::default()));
3595 second.change_state(CopperListState::Processing);
3596 let recycled = match copperlists.end_of_processing_boxed(second).unwrap() {
3597 OwnedCopperListSubmission::Recycled(culist) => culist,
3598 OwnedCopperListSubmission::Pending => panic!("saturated handoff accepted CopperList"),
3599 };
3600
3601 assert_eq!(recycled.id, 1);
3602 assert_eq!(recycled.get_state(), CopperListState::Free);
3603 assert_eq!(copperlists.dropped_copperlists_total(), 1);
3604 assert_eq!(*ids.lock().unwrap(), vec![0]);
3605
3606 release_tx.send(()).unwrap();
3607 let completed = copperlists.finish_pending_boxed().unwrap();
3608 assert_eq!(completed.len(), 1);
3609 assert_eq!(completed[0].id, 0);
3610 }
3611
3612 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3613 #[test]
3614 fn test_async_output_requires_a_spare_execution_slot() {
3615 let error =
3616 match CopperListsManager::<Msgs, 1>::new(Some(Box::new(RecordingWriter::default()))) {
3617 Ok(_) => panic!("async output unexpectedly accepted a single CopperList slot"),
3618 Err(error) => error,
3619 };
3620
3621 assert!(error.to_string().contains("at least two CopperList slots"));
3622 }
3623
3624 #[cfg(all(feature = "std", feature = "async-cl-io"))]
3625 #[test]
3626 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
3627 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3628
3629 {
3630 let culist = copperlists.create().unwrap();
3631 assert_eq!(culist.id, 0);
3632 assert_eq!(culist.get_state(), CopperListState::Initialized);
3633 culist.msgs.0 = 41;
3634 culist.change_state(CopperListState::Processing);
3635 }
3636
3637 copperlists.end_of_processing(0).unwrap();
3638 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
3639
3640 let reused = copperlists.create().unwrap();
3641 assert_eq!(reused.id, 1);
3642 assert_eq!(reused.get_state(), CopperListState::Initialized);
3643 assert_eq!(reused.msgs.0, 41);
3644 }
3645
3646 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
3647 #[test]
3648 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
3649 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
3650 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
3651
3652 let culist = copperlists.create().unwrap();
3653 assert_eq!(culist.id, 0);
3654 assert_eq!(culist.get_state(), CopperListState::Initialized);
3655
3656 let _ = copperlists.end_of_processing(0);
3657 }
3658
3659 #[test]
3660 fn test_runtime_task_input_order() {
3661 let mut config = CuConfig::default();
3662 let graph = config.get_graph_mut(None).unwrap();
3663 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
3664 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
3665 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
3666
3667 assert_eq!(src1_id, 0);
3668 assert_eq!(src2_id, 1);
3669
3670 let src1_type = "src1_type";
3672 let src2_type = "src2_type";
3673 graph.connect(src2_id, sink_id, src2_type).unwrap();
3674 graph.connect(src1_id, sink_id, src1_type).unwrap();
3675
3676 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
3677 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
3678 assert_eq!(src1_edge_id, 1);
3681 assert_eq!(src2_edge_id, 0);
3682
3683 let runtime = compute_runtime_plan(graph).unwrap();
3684 let sink_step = runtime
3685 .steps
3686 .iter()
3687 .find_map(|step| match step {
3688 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
3689 _ => None,
3690 })
3691 .unwrap();
3692
3693 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
3696 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
3697 }
3698
3699 #[test]
3700 fn test_runtime_output_ports_unique_ordered() {
3701 let mut config = CuConfig::default();
3702 let graph = config.get_graph_mut(None).unwrap();
3703 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3704 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3705 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3706 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
3707 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
3708
3709 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
3710 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
3711 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
3712 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
3713
3714 let runtime = compute_runtime_plan(graph).unwrap();
3715 let src_step = runtime
3716 .steps
3717 .iter()
3718 .find_map(|step| match step {
3719 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3720 _ => None,
3721 })
3722 .unwrap();
3723
3724 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3725 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
3726
3727 let dst_a_step = runtime
3728 .steps
3729 .iter()
3730 .find_map(|step| match step {
3731 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
3732 _ => None,
3733 })
3734 .unwrap();
3735 let dst_b_step = runtime
3736 .steps
3737 .iter()
3738 .find_map(|step| match step {
3739 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
3740 _ => None,
3741 })
3742 .unwrap();
3743 let dst_a2_step = runtime
3744 .steps
3745 .iter()
3746 .find_map(|step| match step {
3747 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
3748 _ => None,
3749 })
3750 .unwrap();
3751 let dst_c_step = runtime
3752 .steps
3753 .iter()
3754 .find_map(|step| match step {
3755 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
3756 _ => None,
3757 })
3758 .unwrap();
3759
3760 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
3761 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
3762 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
3763 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
3764 }
3765
3766 #[test]
3767 fn test_runtime_plan_distinguishes_channel_distinct_outputs() {
3768 let mut config = CuConfig::default();
3769 let graph = config.get_graph_mut(None).unwrap();
3770 let src_id = graph.add_node(Node::new("cam", "Cam")).unwrap();
3771 let sink_id = graph.add_node(Node::new("sink", "Sink")).unwrap();
3772
3773 graph
3777 .connect_ext(
3778 src_id,
3779 sink_id,
3780 "msg::Image",
3781 None,
3782 Some("left".to_string()),
3783 None,
3784 )
3785 .unwrap();
3786 graph
3787 .connect_ext(
3788 src_id,
3789 sink_id,
3790 "msg::Image",
3791 None,
3792 Some("right".to_string()),
3793 None,
3794 )
3795 .unwrap();
3796
3797 let runtime = compute_runtime_plan(graph).unwrap();
3798
3799 let src_step = runtime
3800 .steps
3801 .iter()
3802 .find_map(|step| match step {
3803 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3804 _ => None,
3805 })
3806 .unwrap();
3807 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3808 assert_eq!(output_pack.msg_types, vec!["msg::Image", "msg::Image"]);
3809 assert_eq!(
3810 output_pack.src_channels,
3811 vec![Some("left".to_string()), Some("right".to_string())]
3812 );
3813
3814 let sink_step = runtime
3815 .steps
3816 .iter()
3817 .find_map(|step| match step {
3818 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
3819 _ => None,
3820 })
3821 .unwrap();
3822
3823 assert_eq!(sink_step.input_msg_indices_types.len(), 2);
3824 let ports: Vec<usize> = sink_step
3825 .input_msg_indices_types
3826 .iter()
3827 .map(|input| input.src_port)
3828 .collect();
3829 assert_eq!(ports, vec![0, 1]);
3830 }
3831
3832 #[test]
3833 fn test_runtime_output_ports_fanout_single() {
3834 let mut config = CuConfig::default();
3835 let graph = config.get_graph_mut(None).unwrap();
3836 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3837 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
3838 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
3839
3840 graph.connect(src_id, dst_a_id, "i32").unwrap();
3841 graph.connect(src_id, dst_b_id, "i32").unwrap();
3842
3843 let runtime = compute_runtime_plan(graph).unwrap();
3844 let src_step = runtime
3845 .steps
3846 .iter()
3847 .find_map(|step| match step {
3848 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3849 _ => None,
3850 })
3851 .unwrap();
3852
3853 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3854 assert_eq!(output_pack.msg_types, vec!["i32"]);
3855 }
3856
3857 #[test]
3858 fn test_runtime_output_ports_include_nc_outputs() {
3859 let mut config = CuConfig::default();
3860 let graph = config.get_graph_mut(None).unwrap();
3861 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
3862 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
3863 graph.connect(src_id, dst_id, "msg::A").unwrap();
3864 graph
3865 .get_node_mut(src_id)
3866 .expect("missing source node")
3867 .add_nc_output("msg::B", usize::MAX);
3868
3869 let runtime = compute_runtime_plan(graph).unwrap();
3870 let src_step = runtime
3871 .steps
3872 .iter()
3873 .find_map(|step| match step {
3874 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3875 _ => None,
3876 })
3877 .unwrap();
3878 let dst_step = runtime
3879 .steps
3880 .iter()
3881 .find_map(|step| match step {
3882 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3883 _ => None,
3884 })
3885 .unwrap();
3886
3887 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3888 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3889 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
3890 }
3891
3892 #[test]
3893 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
3894 let txt = r#"(
3895 tasks: [
3896 (id: "src", type: "a"),
3897 (id: "regular", type: "b"),
3898 ],
3899 cnx: [
3900 (src: "src", dst: "regular", msg: "msg::A"),
3901 (src: "regular", dst: "__nc__", msg: "msg::B"),
3902 ]
3903 )"#;
3904 let config = CuConfig::deserialize_ron(txt).unwrap();
3905 let graph = config.get_graph(None).unwrap();
3906 let regular_id = graph.get_node_id_by_name("regular").unwrap();
3907
3908 let runtime = compute_runtime_plan(graph).unwrap();
3909 let regular_step = runtime
3910 .steps
3911 .iter()
3912 .find_map(|step| match step {
3913 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
3914 _ => None,
3915 })
3916 .unwrap();
3917
3918 assert_eq!(regular_step.task_type, CuTaskType::Regular);
3919 assert_eq!(
3920 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
3921 vec!["msg::B"]
3922 );
3923 }
3924
3925 #[test]
3926 fn test_runtime_output_ports_respect_connection_order_with_nc() {
3927 let txt = r#"(
3928 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3929 cnx: [
3930 (src: "src", dst: "__nc__", msg: "msg::A"),
3931 (src: "src", dst: "sink", msg: "msg::B"),
3932 ]
3933 )"#;
3934 let config = CuConfig::deserialize_ron(txt).unwrap();
3935 let graph = config.get_graph(None).unwrap();
3936 let src_id = graph.get_node_id_by_name("src").unwrap();
3937 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3938
3939 let runtime = compute_runtime_plan(graph).unwrap();
3940 let src_step = runtime
3941 .steps
3942 .iter()
3943 .find_map(|step| match step {
3944 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3945 _ => None,
3946 })
3947 .unwrap();
3948 let dst_step = runtime
3949 .steps
3950 .iter()
3951 .find_map(|step| match step {
3952 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3953 _ => None,
3954 })
3955 .unwrap();
3956
3957 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3958 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3959 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3960 }
3961
3962 #[cfg(feature = "std")]
3963 #[test]
3964 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
3965 let txt = r#"(
3966 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
3967 cnx: [
3968 (src: "src", dst: "__nc__", msg: "msg::A"),
3969 (src: "src", dst: "sink", msg: "msg::B"),
3970 ]
3971 )"#;
3972 let tmp = tempfile::NamedTempFile::new().unwrap();
3973 std::fs::write(tmp.path(), txt).unwrap();
3974 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
3975 let graph = config.get_graph(None).unwrap();
3976 let src_id = graph.get_node_id_by_name("src").unwrap();
3977 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3978
3979 let runtime = compute_runtime_plan(graph).unwrap();
3980 let src_step = runtime
3981 .steps
3982 .iter()
3983 .find_map(|step| match step {
3984 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3985 _ => None,
3986 })
3987 .unwrap();
3988 let dst_step = runtime
3989 .steps
3990 .iter()
3991 .find_map(|step| match step {
3992 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3993 _ => None,
3994 })
3995 .unwrap();
3996
3997 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3998 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
3999 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4000 }
4001
4002 #[test]
4003 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
4004 let txt = r#"(
4005 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
4006 cnx: [
4007 (src: "src", dst: "__nc__", msg: "i32"),
4008 (src: "src", dst: "sink", msg: "bool"),
4009 ]
4010 )"#;
4011 let config = CuConfig::deserialize_ron(txt).unwrap();
4012 let graph = config.get_graph(None).unwrap();
4013 let src_id = graph.get_node_id_by_name("src").unwrap();
4014 let dst_id = graph.get_node_id_by_name("sink").unwrap();
4015
4016 let runtime = compute_runtime_plan(graph).unwrap();
4017 let src_step = runtime
4018 .steps
4019 .iter()
4020 .find_map(|step| match step {
4021 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
4022 _ => None,
4023 })
4024 .unwrap();
4025 let dst_step = runtime
4026 .steps
4027 .iter()
4028 .find_map(|step| match step {
4029 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
4030 _ => None,
4031 })
4032 .unwrap();
4033
4034 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
4035 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
4036 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
4037 }
4038
4039 #[test]
4040 fn test_runtime_plan_diamond_case1() {
4041 let mut config = CuConfig::default();
4043 let graph = config.get_graph_mut(None).unwrap();
4044 let cam0_id = graph
4045 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
4046 .unwrap();
4047 let inf0_id = graph
4048 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
4049 .unwrap();
4050 let broadcast_id = graph
4051 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
4052 .unwrap();
4053
4054 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
4056 graph.connect(cam0_id, inf0_id, "i32").unwrap();
4057 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
4058
4059 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
4060 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
4061
4062 assert_eq!(edge_cam0_to_inf0, 0);
4063 assert_eq!(edge_cam0_to_broadcast, 1);
4064
4065 let runtime = compute_runtime_plan(graph).unwrap();
4066 let broadcast_step = runtime
4067 .steps
4068 .iter()
4069 .find_map(|step| match step {
4070 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
4071 _ => None,
4072 })
4073 .unwrap();
4074
4075 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
4076 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
4077 }
4078
4079 #[test]
4080 fn test_runtime_plan_diamond_case2() {
4081 let mut config = CuConfig::default();
4083 let graph = config.get_graph_mut(None).unwrap();
4084 let cam0_id = graph
4085 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
4086 .unwrap();
4087 let inf0_id = graph
4088 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
4089 .unwrap();
4090 let broadcast_id = graph
4091 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
4092 .unwrap();
4093
4094 graph.connect(cam0_id, inf0_id, "i32").unwrap();
4096 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
4097 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
4098
4099 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
4100 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
4101
4102 assert_eq!(edge_cam0_to_broadcast, 0);
4103 assert_eq!(edge_cam0_to_inf0, 1);
4104
4105 let runtime = compute_runtime_plan(graph).unwrap();
4106 let broadcast_step = runtime
4107 .steps
4108 .iter()
4109 .find_map(|step| match step {
4110 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
4111 _ => None,
4112 })
4113 .unwrap();
4114
4115 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
4116 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
4117 }
4118
4119 use crate::config::AnytimeConfig;
4122
4123 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
4124 let mut node = Node::new(id, "tasks::AnytimeTask");
4125 node.set_anytime(Some(AnytimeConfig {
4126 max_refines,
4127 ..Default::default()
4128 }));
4129 node
4130 }
4131
4132 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
4134 plan.steps
4135 .iter()
4136 .map(|unit| match unit {
4137 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
4138 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
4139 })
4140 .collect()
4141 }
4142
4143 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
4146 CuExecutionUnit::Step(Box::new(CuExecutionStep {
4147 node_id,
4148 node,
4149 task_type: CuTaskType::Regular,
4150 phase: CuStepPhase::default(),
4151 input_msg_indices_types: inputs
4152 .iter()
4153 .map(|&culist_index| CuInputMsg {
4154 culist_index,
4155 msg_type: "msg::A".to_string(),
4156 src_port: 0,
4157 edge_id: 0,
4158 connection_order: 0,
4159 })
4160 .collect(),
4161 output_msg_pack: Some(CuOutputPack {
4162 culist_index: output,
4163 msg_types: vec!["msg::A".to_string()],
4164 src_channels: vec![None],
4165 }),
4166 }))
4167 }
4168
4169 #[test]
4170 fn test_anytime_expansion_contiguous_without_gap() {
4171 let mut config = CuConfig::default();
4174 let graph = config.get_graph_mut(None).unwrap();
4175 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
4176 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
4177 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
4178 graph.connect(src_id, any_id, "msg::A").unwrap();
4179 graph.connect(any_id, sink_id, "msg::B").unwrap();
4180
4181 let mut plan = compute_runtime_plan(graph).unwrap();
4182 expand_anytime_steps(&mut plan).unwrap();
4183
4184 assert_eq!(
4185 plan_shape(&plan),
4186 vec![
4187 (src_id, CuStepPhase::Whole),
4188 (any_id, CuStepPhase::AnytimeBase),
4189 (any_id, CuStepPhase::AnytimeRefine),
4190 (any_id, CuStepPhase::AnytimeRefine),
4191 (any_id, CuStepPhase::AnytimeRefine),
4192 (sink_id, CuStepPhase::Whole),
4193 ]
4194 );
4195
4196 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
4198 let mut base_pack = None;
4199 let mut refines = Vec::new();
4200 for unit in &plan.steps {
4201 if let CuExecutionUnit::Step(step) = unit {
4202 match step.phase {
4203 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
4204 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
4205 CuStepPhase::Whole => {}
4206 }
4207 }
4208 }
4209 (base_pack, refines)
4210 };
4211 let base_pack = base_pack.unwrap();
4212 for refine in refine_steps {
4213 assert!(refine.input_msg_indices_types.is_empty());
4214 let pack = refine.output_msg_pack.as_ref().unwrap();
4215 assert_eq!(pack.culist_index, base_pack.culist_index);
4216 }
4217 }
4218
4219 #[test]
4220 fn test_anytime_expansion_interleaves_with_gap_steps() {
4221 let mut plan = CuExecutionLoop {
4224 steps: vec![
4225 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
4226 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
4227 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
4228 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
4229 ],
4230 loop_count: None,
4231 };
4232 expand_anytime_steps(&mut plan).unwrap();
4233 assert_eq!(
4234 plan_shape(&plan),
4235 vec![
4236 (0, CuStepPhase::AnytimeBase),
4237 (0, CuStepPhase::AnytimeRefine),
4238 (1, CuStepPhase::Whole),
4239 (0, CuStepPhase::AnytimeRefine),
4240 (2, CuStepPhase::Whole),
4241 (0, CuStepPhase::AnytimeRefine),
4242 (0, CuStepPhase::AnytimeRefine),
4243 (3, CuStepPhase::Whole),
4244 ]
4245 );
4246 }
4247
4248 #[test]
4249 fn test_anytime_expansion_fewer_refines_than_gaps() {
4250 let mut plan = CuExecutionLoop {
4252 steps: vec![
4253 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
4254 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
4255 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
4256 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
4257 ],
4258 loop_count: None,
4259 };
4260 expand_anytime_steps(&mut plan).unwrap();
4261 assert_eq!(
4262 plan_shape(&plan),
4263 vec![
4264 (0, CuStepPhase::AnytimeBase),
4265 (0, CuStepPhase::AnytimeRefine),
4266 (1, CuStepPhase::Whole),
4267 (0, CuStepPhase::AnytimeRefine),
4268 (2, CuStepPhase::Whole),
4269 (3, CuStepPhase::Whole),
4270 ]
4271 );
4272 }
4273
4274 #[test]
4275 fn test_anytime_expansion_without_consumer() {
4276 let mut plan = CuExecutionLoop {
4278 steps: vec![
4279 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
4280 manual_step(Node::new("other", "t"), 1, &[], 1),
4281 ],
4282 loop_count: None,
4283 };
4284 expand_anytime_steps(&mut plan).unwrap();
4285 assert_eq!(
4286 plan_shape(&plan),
4287 vec![
4288 (0, CuStepPhase::AnytimeBase),
4289 (0, CuStepPhase::AnytimeRefine),
4290 (0, CuStepPhase::AnytimeRefine),
4291 (1, CuStepPhase::Whole),
4292 ]
4293 );
4294 }
4295
4296 #[test]
4297 fn test_anytime_expansion_two_nodes_interleave() {
4298 let mut plan = CuExecutionLoop {
4301 steps: vec![
4302 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
4303 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
4304 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
4305 ],
4306 loop_count: None,
4307 };
4308 expand_anytime_steps(&mut plan).unwrap();
4309 assert_eq!(
4312 plan_shape(&plan),
4313 vec![
4314 (0, CuStepPhase::AnytimeBase),
4315 (0, CuStepPhase::AnytimeRefine),
4316 (1, CuStepPhase::AnytimeBase),
4317 (1, CuStepPhase::AnytimeRefine),
4318 (0, CuStepPhase::AnytimeRefine),
4319 (1, CuStepPhase::AnytimeRefine),
4320 (2, CuStepPhase::Whole),
4321 ]
4322 );
4323 }
4324
4325 #[test]
4326 fn test_anytime_expansion_requires_max_refines() {
4327 let mut plan = CuExecutionLoop {
4328 steps: vec![
4329 manual_step(anytime_node("any", None), 0, &[], 0),
4330 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
4331 ],
4332 loop_count: None,
4333 };
4334 let err = expand_anytime_steps(&mut plan).unwrap_err();
4335 assert!(err.to_string().contains("needs anytime.max_refines"));
4336 }
4337}