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