1use crate::app::Subsystem;
6use crate::config::{ComponentConfig, CuDirection, 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::resource::ResourceManager;
24#[cfg(feature = "std")]
25use alloc::sync::Arc;
26use compact_str::CompactString;
27use cu29_clock::{ClockProvider, CuDuration, CuTime, RobotClock};
28use cu29_traits::CuResult;
29use cu29_traits::WriteStream;
30use cu29_traits::{CopperListTuple, CuError};
31#[cfg(feature = "std")]
32use rayon::ThreadPool;
33
34#[cfg(target_os = "none")]
35#[allow(unused_imports)]
36use cu29_log::{ANONYMOUS, CuLogEntry, CuLogLevel};
37#[cfg(target_os = "none")]
38#[allow(unused_imports)]
39use cu29_log_derive::info;
40#[cfg(target_os = "none")]
41#[allow(unused_imports)]
42use cu29_log_runtime::log;
43#[cfg(all(target_os = "none", debug_assertions))]
44#[allow(unused_imports)]
45use cu29_log_runtime::log_debug_mode;
46#[cfg(target_os = "none")]
47#[allow(unused_imports)]
48use cu29_value::to_value;
49
50#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
51use alloc::alloc::{alloc_zeroed, handle_alloc_error};
52use alloc::boxed::Box;
53use alloc::collections::{BTreeSet, VecDeque};
54use alloc::format;
55use alloc::string::{String, ToString};
56use alloc::vec::Vec;
57use bincode::enc::EncoderImpl;
58use bincode::enc::write::{SizeWriter, SliceWriter};
59use bincode::error::EncodeError;
60use bincode::{Decode, Encode};
61#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
62use core::alloc::Layout;
63use core::fmt::Result as FmtResult;
64use core::fmt::{Debug, Formatter};
65use core::marker::PhantomData;
66
67#[cfg(all(feature = "std", feature = "async-cl-io"))]
68use std::sync::mpsc::{Receiver, SyncSender, TryRecvError, sync_channel};
69#[cfg(all(feature = "std", feature = "async-cl-io"))]
70use std::thread::JoinHandle;
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 CLW,
146 KFW,
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 copperlists_logger: CLW,
158 keyframes_logger: KFW,
159}
160
161impl<'cfg, CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize, TI, BI, MI, CLW, KFW>
162 CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
163{
164 pub fn new(
165 clock: RobotClock,
166 config: &'cfg CuConfig,
167 mission: &'cfg str,
168 parts: CuRuntimeParts<CT, CB, P, M, NBCL, TI, BI, MI>,
169 copperlists_logger: CLW,
170 keyframes_logger: KFW,
171 ) -> Self {
172 Self {
173 clock,
174 config,
175 mission,
176 subsystem: Subsystem::new(None, 0),
177 instance_id: 0,
178 resources: None,
179 #[cfg(feature = "std")]
180 thread_pools: None,
181 parts,
182 copperlists_logger,
183 keyframes_logger,
184 }
185 }
186
187 pub fn with_subsystem(mut self, subsystem: Subsystem) -> Self {
188 self.subsystem = subsystem;
189 self
190 }
191
192 pub fn with_instance_id(mut self, instance_id: u32) -> Self {
193 self.instance_id = instance_id;
194 self
195 }
196
197 pub fn with_resources(mut self, resources: ResourceManager) -> Self {
198 self.resources = Some(resources);
199 self
200 }
201
202 pub fn try_with_resources_instantiator(
203 mut self,
204 resources_instantiator: impl FnOnce(&CuConfig) -> CuResult<ResourceManager>,
205 ) -> CuResult<Self> {
206 self.resources = Some(resources_instantiator(self.config)?);
207 Ok(self)
208 }
209
210 #[cfg(feature = "std")]
214 pub fn with_thread_pools(mut self, pools: Vec<Option<Arc<ThreadPool>>>) -> Self {
215 self.thread_pools = Some(pools);
216 self
217 }
218
219 #[cfg(feature = "std")]
220 pub fn try_with_thread_pools_instantiator(
221 mut self,
222 thread_pools_instantiator: impl FnOnce(&CuConfig) -> CuResult<Vec<Option<Arc<ThreadPool>>>>,
223 ) -> CuResult<Self> {
224 self.thread_pools = Some(thread_pools_instantiator(self.config)?);
225 Ok(self)
226 }
227}
228
229#[inline]
240pub fn perf_now(_clock: &RobotClock) -> CuTime {
241 #[cfg(all(feature = "std", feature = "sysclock-perf"))]
242 {
243 static PERF_CLOCK: std::sync::OnceLock<RobotClock> = std::sync::OnceLock::new();
244 return PERF_CLOCK.get_or_init(RobotClock::new).now();
245 }
246
247 #[allow(unreachable_code)]
248 _clock.now()
249}
250
251#[cfg(all(feature = "std", feature = "high-precision-limiter"))]
252const HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS: u64 = 200_000;
253
254#[inline]
256pub fn rate_target_period(rate_target_hz: u64) -> CuResult<CuDuration> {
257 if rate_target_hz == 0 {
258 return Err(CuError::from(
259 "Runtime rate target cannot be zero. Set runtime.rate_target_hz to at least 1.",
260 ));
261 }
262
263 if rate_target_hz > MAX_RATE_TARGET_HZ {
264 return Err(CuError::from(format!(
265 "Runtime rate target ({rate_target_hz} Hz) exceeds the supported maximum of {MAX_RATE_TARGET_HZ} Hz."
266 )));
267 }
268
269 Ok(CuDuration::from(MAX_RATE_TARGET_HZ / rate_target_hz))
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
279pub struct LoopRateLimiter {
280 period: CuDuration,
281 next_deadline: CuTime,
282}
283
284impl LoopRateLimiter {
285 #[inline]
286 pub fn from_rate_target_hz(rate_target_hz: u64, clock: &RobotClock) -> CuResult<Self> {
287 let period = rate_target_period(rate_target_hz)?;
288 Ok(Self {
289 period,
290 next_deadline: clock.now() + period,
291 })
292 }
293
294 #[inline]
295 pub fn is_ready(&self, clock: &RobotClock) -> bool {
296 self.remaining(clock).is_none()
297 }
298
299 #[inline]
300 pub fn remaining(&self, clock: &RobotClock) -> Option<CuDuration> {
301 let now = clock.now();
302 if now < self.next_deadline {
303 Some(self.next_deadline - now)
304 } else {
305 None
306 }
307 }
308
309 #[inline]
310 pub fn wait_until_ready(&self, clock: &RobotClock) {
311 let deadline = self.next_deadline;
312 let Some(remaining) = self.remaining(clock) else {
313 return;
314 };
315
316 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
317 {
318 let spin_window = self.spin_window();
319 if remaining > spin_window {
320 std::thread::sleep(std::time::Duration::from(remaining - spin_window));
321 }
322 while clock.now() < deadline {
323 core::hint::spin_loop();
324 }
325 }
326
327 #[cfg(all(feature = "std", not(feature = "high-precision-limiter")))]
328 {
329 let _ = deadline;
330 std::thread::sleep(std::time::Duration::from(remaining));
331 }
332
333 #[cfg(not(feature = "std"))]
334 {
335 let _ = remaining;
336 while clock.now() < deadline {
337 core::hint::spin_loop();
338 }
339 }
340 }
341
342 #[inline]
343 pub fn mark_tick(&mut self, clock: &RobotClock) {
344 self.advance_from(clock.now());
345 }
346
347 #[inline]
348 pub fn limit(&mut self, clock: &RobotClock) {
349 self.wait_until_ready(clock);
350 self.mark_tick(clock);
351 }
352
353 #[inline]
354 fn advance_from(&mut self, now: CuTime) {
355 let steps = if now < self.next_deadline {
356 1
357 } else {
358 (now - self.next_deadline).as_nanos() / self.period.as_nanos() + 1
359 };
360 self.next_deadline += steps * self.period;
361 }
362
363 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
364 #[inline]
365 fn spin_window(&self) -> CuDuration {
366 let _ = self.period;
367 CuDuration::from(HIGH_PRECISION_LIMITER_SPIN_WINDOW_NS)
368 }
369
370 #[cfg(test)]
371 #[inline]
372 fn next_deadline(&self) -> CuTime {
373 self.next_deadline
374 }
375}
376
377#[cfg(all(feature = "std", feature = "async-cl-io"))]
378#[doc(hidden)]
379pub trait AsyncCopperListPayload: Send {}
380
381#[cfg(all(feature = "std", feature = "async-cl-io"))]
382impl<T: Send> AsyncCopperListPayload for T {}
383
384#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
385#[doc(hidden)]
386pub trait AsyncCopperListPayload {}
387
388#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
389impl<T> AsyncCopperListPayload for T {}
390
391#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398#[doc(hidden)]
399pub enum ProcessStepOutcome {
400 Continue,
401 AbortCopperList,
402}
403
404#[doc(hidden)]
406pub type ProcessStepResult = CuResult<ProcessStepOutcome>;
407
408#[cfg(feature = "remote-debug")]
409fn encode_completed_copperlist_snapshot<P: CopperListTuple>(
410 cl: &CopperList<P>,
411) -> CuResult<Vec<u8>> {
412 bincode::encode_to_vec(cl, bincode::config::standard())
413 .map_err(|e| CuError::new_with_cause("Failed to encode completed CopperList snapshot", e))
414}
415
416#[doc(hidden)]
418pub struct SyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
419 inner: CuListsManager<P, NBCL>,
420 logger: Option<Box<dyn WriteStream<CopperList<P>>>>,
422 #[cfg(feature = "remote-debug")]
424 last_completed_encoded: Option<Vec<u8>>,
425 pub last_encoded_bytes: u64,
427 pub last_handle_bytes: u64,
429}
430
431impl<P: CopperListTuple + Default, const NBCL: usize> SyncCopperListsManager<P, NBCL> {
432 pub fn new(logger: Option<Box<dyn WriteStream<CopperList<P>>>>) -> CuResult<Self>
433 where
434 P: CuListZeroedInit,
435 {
436 Ok(Self {
437 inner: CuListsManager::new(),
438 logger,
439 #[cfg(feature = "remote-debug")]
440 last_completed_encoded: None,
441 last_encoded_bytes: 0,
442 last_handle_bytes: 0,
443 })
444 }
445
446 pub fn next_cl_id(&self) -> u64 {
447 self.inner.next_cl_id()
448 }
449
450 pub fn last_cl_id(&self) -> u64 {
451 self.inner.last_cl_id()
452 }
453
454 pub fn peek(&self) -> Option<&CopperList<P>> {
455 self.inner.peek()
456 }
457
458 #[cfg(feature = "remote-debug")]
459 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
460 self.last_completed_encoded.as_deref()
461 }
462
463 #[cfg(not(feature = "remote-debug"))]
464 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
465 None
466 }
467
468 #[cfg(feature = "remote-debug")]
469 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
470 self.last_completed_encoded = snapshot;
471 }
472
473 #[cfg(not(feature = "remote-debug"))]
474 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
475
476 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
477 where
478 P: CuListZeroedInit,
479 {
480 self.inner
481 .create()
482 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))
483 }
484
485 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
486 #[cfg(debug_assertions)]
487 self.debug_assert_end_of_processing_target(culistid);
488
489 let mut is_top = true;
490 let mut nb_done = 0;
491 self.last_encoded_bytes = 0;
492 self.last_handle_bytes = 0;
493 #[cfg(feature = "remote-debug")]
494 let last_completed_encoded = &mut self.last_completed_encoded;
495 for cl in self.inner.iter_mut() {
496 if cl.id == culistid && cl.get_state() == CopperListState::Processing {
497 cl.change_state(CopperListState::DoneProcessing);
498 #[cfg(feature = "remote-debug")]
499 {
500 *last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
501 }
502 }
503 if is_top && cl.get_state() == CopperListState::DoneProcessing {
504 if let Some(logger) = &mut self.logger {
505 cl.change_state(CopperListState::BeingSerialized);
506 logger.log(cl)?;
507 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
508 self.last_handle_bytes = take_last_completed_handle_bytes();
509 }
510 cl.change_state(CopperListState::Free);
511 nb_done += 1;
512 } else {
513 is_top = false;
514 }
515 }
516 for _ in 0..nb_done {
517 let _ = self.inner.pop();
518 }
519 Ok(())
520 }
521
522 pub fn finish_pending(&mut self) -> CuResult<()> {
523 Ok(())
524 }
525
526 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
527 Ok(NBCL - self.inner.len())
528 }
529
530 #[cfg(feature = "std")]
531 pub fn end_of_processing_boxed(
532 &mut self,
533 mut culist: Box<CopperList<P>>,
534 ) -> CuResult<OwnedCopperListSubmission<P>> {
535 #[cfg(debug_assertions)]
536 debug_assert_processing_completion_state(culist.as_ref(), "sync boxed end_of_processing");
537
538 culist.change_state(CopperListState::DoneProcessing);
539 self.last_encoded_bytes = 0;
540 self.last_handle_bytes = 0;
541 if let Some(logger) = &mut self.logger {
542 culist.change_state(CopperListState::BeingSerialized);
543 logger.log(&culist)?;
544 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
545 self.last_handle_bytes = take_last_completed_handle_bytes();
546 }
547 culist.change_state(CopperListState::Free);
548 Ok(OwnedCopperListSubmission::Recycled(culist))
549 }
550
551 #[cfg(feature = "std")]
552 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
553 Ok(None)
554 }
555
556 #[cfg(feature = "std")]
557 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
558 Err(CuError::from(
559 "Synchronous CopperList I/O cannot block waiting for boxed completions",
560 ))
561 }
562
563 #[cfg(feature = "std")]
564 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
565 Ok(Vec::new())
566 }
567
568 #[cfg(debug_assertions)]
569 fn debug_assert_end_of_processing_target(&self, culistid: u64) {
570 let mut matches = 0usize;
571 let mut state = None;
572 for cl in self.inner.iter() {
573 if cl.id == culistid {
574 matches += 1;
575 state = Some(cl.get_state());
576 }
577 }
578
579 assert_eq!(
580 matches, 1,
581 "sync end_of_processing expected exactly one active CopperList #{culistid}, found {matches}"
582 );
583 assert_eq!(
584 state,
585 Some(CopperListState::Processing),
586 "sync end_of_processing expected CopperList #{culistid} to be Processing, found {:?}",
587 state
588 );
589 }
590}
591
592#[cfg(feature = "std")]
594#[doc(hidden)]
595pub enum OwnedCopperListSubmission<P: CopperListTuple> {
596 Recycled(Box<CopperList<P>>),
598 Pending,
600}
601
602#[cfg(all(feature = "std", feature = "async-cl-io"))]
603struct AsyncCopperListCompletion<P: CopperListTuple> {
604 culist: Box<CopperList<P>>,
605 log_result: CuResult<(u64, u64)>,
606}
607
608#[cfg(all(feature = "std", any(feature = "async-cl-io", feature = "parallel-rt")))]
609fn allocate_zeroed_copperlist<P>() -> Box<CopperList<P>>
610where
611 P: CopperListTuple + CuListZeroedInit,
612{
613 let mut culist = unsafe {
615 let layout = Layout::new::<CopperList<P>>();
616 let ptr = alloc_zeroed(layout) as *mut CopperList<P>;
617 if ptr.is_null() {
618 handle_alloc_error(layout);
619 }
620 Box::from_raw(ptr)
621 };
622 culist.msgs.init_zeroed();
623 culist
624}
625
626#[cfg(all(feature = "std", feature = "parallel-rt"))]
627pub fn allocate_boxed_copperlists<P, const NBCL: usize>() -> Vec<Box<CopperList<P>>>
628where
629 P: CopperListTuple + CuListZeroedInit,
630{
631 let mut free_pool = Vec::with_capacity(NBCL);
632 for _ in 0..NBCL {
633 free_pool.push(allocate_zeroed_copperlist::<P>());
634 }
635 free_pool
636}
637
638#[cfg(all(feature = "std", feature = "async-cl-io"))]
640#[doc(hidden)]
641pub struct AsyncCopperListsManager<P: CopperListTuple + Default, const NBCL: usize> {
642 free_pool: Vec<Box<CopperList<P>>>,
643 current: Option<Box<CopperList<P>>>,
644 #[cfg(feature = "remote-debug")]
645 last_completed_encoded: Option<Vec<u8>>,
646 pending_count: usize,
647 next_cl_id: u64,
648 pending_sender: Option<SyncSender<Box<CopperList<P>>>>,
649 completion_receiver: Option<Receiver<AsyncCopperListCompletion<P>>>,
650 worker_handle: Option<JoinHandle<()>>,
651 pub last_encoded_bytes: u64,
653 pub last_handle_bytes: u64,
655}
656
657#[cfg(all(feature = "std", feature = "async-cl-io"))]
658impl<P: CopperListTuple + Default, const NBCL: usize> AsyncCopperListsManager<P, NBCL> {
659 pub fn new(logger: Option<Box<dyn WriteStream<CopperList<P>>>>) -> CuResult<Self>
660 where
661 P: CuListZeroedInit + AsyncCopperListPayload + 'static,
662 {
663 let mut free_pool = Vec::with_capacity(NBCL);
664 for _ in 0..NBCL {
665 free_pool.push(allocate_zeroed_copperlist::<P>());
666 }
667
668 let (pending_sender, completion_receiver, worker_handle) = if let Some(mut logger) = logger
669 {
670 let (pending_sender, pending_receiver) = sync_channel::<Box<CopperList<P>>>(NBCL);
671 let (completion_sender, completion_receiver) =
672 sync_channel::<AsyncCopperListCompletion<P>>(NBCL);
673 let worker_handle = std::thread::Builder::new()
674 .name("cu-async-cl-io".to_string())
675 .spawn(move || {
676 while let Ok(mut culist) = pending_receiver.recv() {
677 culist.change_state(CopperListState::BeingSerialized);
678 let log_result = logger.log(&culist).map(|_| {
679 (
680 logger.last_log_bytes().unwrap_or(0) as u64,
681 take_last_completed_handle_bytes(),
682 )
683 });
684 let should_stop = log_result.is_err();
685 if completion_sender
686 .send(AsyncCopperListCompletion { culist, log_result })
687 .is_err()
688 {
689 break;
690 }
691 if should_stop {
692 break;
693 }
694 }
695 })
696 .map_err(|e| {
697 CuError::from("Failed to spawn async CopperList serializer thread")
698 .add_cause(e.to_string().as_str())
699 })?;
700 (
701 Some(pending_sender),
702 Some(completion_receiver),
703 Some(worker_handle),
704 )
705 } else {
706 (None, None, None)
707 };
708
709 Ok(Self {
710 free_pool,
711 current: None,
712 #[cfg(feature = "remote-debug")]
713 last_completed_encoded: None,
714 pending_count: 0,
715 next_cl_id: 0,
716 pending_sender,
717 completion_receiver,
718 worker_handle,
719 last_encoded_bytes: 0,
720 last_handle_bytes: 0,
721 })
722 }
723
724 pub fn next_cl_id(&self) -> u64 {
725 self.next_cl_id
726 }
727
728 pub fn last_cl_id(&self) -> u64 {
729 self.next_cl_id.saturating_sub(1)
730 }
731
732 pub fn peek(&self) -> Option<&CopperList<P>> {
733 self.current.as_deref()
734 }
735
736 #[cfg(feature = "remote-debug")]
737 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
738 self.last_completed_encoded.as_deref()
739 }
740
741 #[cfg(not(feature = "remote-debug"))]
742 pub fn last_completed_encoded(&self) -> Option<&[u8]> {
743 None
744 }
745
746 #[cfg(feature = "remote-debug")]
747 pub fn set_last_completed_encoded(&mut self, snapshot: Option<Vec<u8>>) {
748 self.last_completed_encoded = snapshot;
749 }
750
751 #[cfg(not(feature = "remote-debug"))]
752 pub fn set_last_completed_encoded(&mut self, _snapshot: Option<Vec<u8>>) {}
753
754 pub fn create(&mut self) -> CuResult<&mut CopperList<P>>
755 where
756 P: CuListZeroedInit,
757 {
758 if self.current.is_some() {
759 return Err(CuError::from(
760 "Attempted to create a CopperList while another one is still active",
761 ));
762 }
763
764 self.reclaim_completed()?;
765 while self.free_pool.is_empty() {
766 self.wait_for_completion()?;
767 }
768
769 let culist = self
770 .free_pool
771 .pop()
772 .ok_or_else(|| CuError::from("Ran out of space for copper lists"))?;
773 self.current = Some(culist);
774
775 let current = self
776 .current
777 .as_mut()
778 .expect("current CopperList is missing");
779 current.reset_for_runtime_use(self.next_cl_id);
780 self.next_cl_id += 1;
781 Ok(current.as_mut())
782 }
783
784 #[cfg(feature = "remote-debug")]
785 fn capture_completed_snapshot(&mut self, cl: &CopperList<P>) -> CuResult<()> {
786 self.last_completed_encoded = Some(encode_completed_copperlist_snapshot(cl)?);
787 Ok(())
788 }
789
790 #[cfg(not(feature = "remote-debug"))]
791 fn capture_completed_snapshot(&mut self, _cl: &CopperList<P>) -> CuResult<()> {
792 Ok(())
793 }
794
795 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
796 self.reclaim_completed()?;
797
798 let mut culist = self.current.take().ok_or_else(|| {
799 CuError::from("Attempted to finish processing without an active CopperList")
800 })?;
801
802 if culist.id != culistid {
803 return Err(CuError::from(format!(
804 "Attempted to finish CopperList #{culistid} while CopperList #{} is active",
805 culist.id
806 )));
807 }
808 #[cfg(debug_assertions)]
809 debug_assert_processing_completion_state(culist.as_ref(), "async end_of_processing");
810
811 culist.change_state(CopperListState::DoneProcessing);
812 self.capture_completed_snapshot(&culist)?;
813 self.last_encoded_bytes = 0;
814 self.last_handle_bytes = 0;
815
816 if let Some(pending_sender) = &self.pending_sender {
817 culist.change_state(CopperListState::QueuedForSerialization);
818 pending_sender.send(culist).map_err(|e| {
819 CuError::from("Failed to enqueue CopperList for async serialization")
820 .add_cause(e.to_string().as_str())
821 })?;
822 self.pending_count += 1;
823 self.reclaim_completed()?;
824 } else {
825 culist.change_state(CopperListState::Free);
826 self.free_pool.push(culist);
827 }
828
829 Ok(())
830 }
831
832 pub fn finish_pending(&mut self) -> CuResult<()> {
833 if self.current.is_some() {
834 return Err(CuError::from(
835 "Cannot flush CopperList I/O while a CopperList is still active",
836 ));
837 }
838
839 while self.pending_count > 0 {
840 self.wait_for_completion()?;
841 }
842 Ok(())
843 }
844
845 pub fn available_copper_lists(&mut self) -> CuResult<usize> {
846 self.reclaim_completed()?;
847 Ok(self.free_pool.len())
848 }
849
850 pub fn end_of_processing_boxed(
851 &mut self,
852 mut culist: Box<CopperList<P>>,
853 ) -> CuResult<OwnedCopperListSubmission<P>> {
854 self.reclaim_completed()?;
855 #[cfg(debug_assertions)]
856 debug_assert_processing_completion_state(culist.as_ref(), "async boxed end_of_processing");
857 culist.change_state(CopperListState::DoneProcessing);
858 self.capture_completed_snapshot(&culist)?;
859 self.last_encoded_bytes = 0;
860 self.last_handle_bytes = 0;
861
862 if let Some(pending_sender) = &self.pending_sender {
863 culist.change_state(CopperListState::QueuedForSerialization);
864 pending_sender.send(culist).map_err(|e| {
865 CuError::from("Failed to enqueue CopperList for async serialization")
866 .add_cause(e.to_string().as_str())
867 })?;
868 self.pending_count += 1;
869 self.reclaim_completed()?;
870 Ok(OwnedCopperListSubmission::Pending)
871 } else {
872 culist.change_state(CopperListState::Free);
873 Ok(OwnedCopperListSubmission::Recycled(culist))
874 }
875 }
876
877 pub fn try_reclaim_boxed(&mut self) -> CuResult<Option<Box<CopperList<P>>>> {
878 let recv_result = {
879 let Some(completion_receiver) = self.completion_receiver.as_ref() else {
880 return Ok(None);
881 };
882 completion_receiver.try_recv()
883 };
884 match recv_result {
885 Ok(completion) => self.handle_completion(completion).map(Some),
886 Err(TryRecvError::Empty) => Ok(None),
887 Err(TryRecvError::Disconnected) => Err(CuError::from(
888 "Async CopperList serializer thread disconnected unexpectedly",
889 )),
890 }
891 }
892
893 pub fn wait_reclaim_boxed(&mut self) -> CuResult<Box<CopperList<P>>> {
894 let completion = self
895 .completion_receiver
896 .as_ref()
897 .ok_or_else(|| {
898 CuError::from("No async CopperList serializer is active to return a free slot")
899 })?
900 .recv()
901 .map_err(|e| {
902 CuError::from("Failed to receive completion from async CopperList serializer")
903 .add_cause(e.to_string().as_str())
904 })?;
905 self.handle_completion(completion)
906 }
907
908 pub fn finish_pending_boxed(&mut self) -> CuResult<Vec<Box<CopperList<P>>>> {
909 let mut reclaimed = Vec::with_capacity(self.pending_count);
910 if self.current.is_some() {
911 return Err(CuError::from(
912 "Cannot flush CopperList I/O while a CopperList is still active",
913 ));
914 }
915 while self.pending_count > 0 {
916 reclaimed.push(self.wait_reclaim_boxed()?);
917 }
918 Ok(reclaimed)
919 }
920
921 fn reclaim_completed(&mut self) -> CuResult<()> {
922 loop {
923 let Some(culist) = self.try_reclaim_boxed()? else {
924 break;
925 };
926 self.free_pool.push(culist);
927 }
928 Ok(())
929 }
930
931 fn wait_for_completion(&mut self) -> CuResult<()> {
932 let culist = self.wait_reclaim_boxed()?;
933 self.free_pool.push(culist);
934 Ok(())
935 }
936
937 fn handle_completion(
938 &mut self,
939 mut completion: AsyncCopperListCompletion<P>,
940 ) -> CuResult<Box<CopperList<P>>> {
941 self.pending_count = self.pending_count.saturating_sub(1);
942 if let Ok((encoded_bytes, handle_bytes)) = completion.log_result.as_ref() {
943 self.last_encoded_bytes = *encoded_bytes;
944 self.last_handle_bytes = *handle_bytes;
945 }
946 completion.culist.change_state(CopperListState::Free);
947 completion.log_result?;
948 Ok(completion.culist)
949 }
950
951 fn shutdown_worker(&mut self) -> CuResult<()> {
952 self.finish_pending()?;
953 self.pending_sender.take();
954 if let Some(worker_handle) = self.worker_handle.take() {
955 worker_handle.join().map_err(|_| {
956 CuError::from("Async CopperList serializer thread panicked while joining")
957 })?;
958 }
959 Ok(())
960 }
961}
962
963#[cfg(all(feature = "std", feature = "async-cl-io"))]
964impl<P: CopperListTuple + Default, const NBCL: usize> Drop for AsyncCopperListsManager<P, NBCL> {
965 fn drop(&mut self) {
966 let _ = self.shutdown_worker();
967 }
968}
969
970#[cfg(all(feature = "std", debug_assertions))]
971fn debug_assert_processing_completion_state<P: CopperListTuple>(
972 culist: &CopperList<P>,
973 context: &str,
974) {
975 assert_eq!(
976 culist.get_state(),
977 CopperListState::Processing,
978 "{context} expected CopperList #{} to be Processing, found {}",
979 culist.id,
980 culist.get_state()
981 );
982}
983
984#[cfg(all(feature = "std", feature = "async-cl-io"))]
985#[doc(hidden)]
986pub type CopperListsManager<P, const NBCL: usize> = AsyncCopperListsManager<P, NBCL>;
987
988#[cfg(not(all(feature = "std", feature = "async-cl-io")))]
989#[doc(hidden)]
990pub type CopperListsManager<P, const NBCL: usize> = SyncCopperListsManager<P, NBCL>;
991
992pub struct KeyFramesManager {
994 inner: KeyFrame,
996
997 forced_timestamp: Option<CuTime>,
999
1000 locked: bool,
1002
1003 logger: Option<Box<dyn WriteStream<KeyFrame>>>,
1005
1006 keyframe_interval: u32,
1008
1009 pub last_encoded_bytes: u64,
1011}
1012
1013impl KeyFramesManager {
1014 fn is_keyframe(&self, culistid: u64) -> bool {
1015 self.logger.is_some() && culistid.is_multiple_of(self.keyframe_interval as u64)
1016 }
1017
1018 #[inline]
1019 pub fn captures_keyframe(&self, culistid: u64) -> bool {
1020 self.is_keyframe(culistid)
1021 }
1022
1023 pub fn reset(&mut self, culistid: u64, clock: &RobotClock) {
1024 if self.is_keyframe(culistid) {
1025 if self.locked && self.inner.culistid == culistid {
1027 return;
1028 }
1029 let ts = self.forced_timestamp.take().unwrap_or_else(|| clock.now());
1030 self.inner.reset(culistid, ts);
1031 self.locked = false;
1032 }
1033 }
1034
1035 #[cfg(feature = "std")]
1037 pub fn set_forced_timestamp(&mut self, ts: CuTime) {
1038 self.forced_timestamp = Some(ts);
1039 }
1040
1041 pub fn freeze_task(&mut self, culistid: u64, task: &impl Freezable) -> CuResult<usize> {
1042 if self.is_keyframe(culistid) {
1043 if self.locked {
1044 return Ok(0);
1046 }
1047 if self.inner.culistid != culistid {
1048 return Err(CuError::from(format!(
1049 "Freezing task for culistid {} but current keyframe is {}",
1050 culistid, self.inner.culistid
1051 )));
1052 }
1053 self.inner
1054 .add_frozen_task(task)
1055 .map_err(|e| CuError::from(format!("Failed to serialize task: {e}")))
1056 } else {
1057 Ok(0)
1058 }
1059 }
1060
1061 pub fn freeze_any(&mut self, culistid: u64, item: &impl Freezable) -> CuResult<usize> {
1063 self.freeze_task(culistid, item)
1064 }
1065
1066 pub fn end_of_processing(&mut self, culistid: u64) -> CuResult<()> {
1067 if self.is_keyframe(culistid) {
1068 let logger = self.logger.as_mut().unwrap();
1069 logger.log(&self.inner)?;
1070 self.last_encoded_bytes = logger.last_log_bytes().unwrap_or(0) as u64;
1071 self.locked = false;
1073 Ok(())
1074 } else {
1075 self.last_encoded_bytes = 0;
1077 Ok(())
1078 }
1079 }
1080
1081 #[cfg(feature = "std")]
1083 pub fn lock_keyframe(&mut self, keyframe: &KeyFrame) {
1084 self.inner = keyframe.clone();
1085 self.forced_timestamp = Some(keyframe.timestamp);
1086 self.locked = true;
1087 }
1088}
1089
1090pub struct CuRuntime<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> {
1094 clock: RobotClock,
1096
1097 subsystem_code: u16,
1099
1100 #[doc(hidden)]
1102 pub instance_id: u32,
1103
1104 #[doc(hidden)]
1106 pub tasks: CT,
1107
1108 #[doc(hidden)]
1110 pub bridges: CB,
1111
1112 #[doc(hidden)]
1114 pub resources: ResourceManager,
1115
1116 #[cfg(feature = "std")]
1119 #[doc(hidden)]
1120 pub thread_pools: Vec<Option<Arc<ThreadPool>>>,
1121
1122 #[doc(hidden)]
1124 pub monitor: M,
1125
1126 #[cfg(feature = "std")]
1132 #[doc(hidden)]
1133 pub execution_probe: ExecutionProbeHandle,
1134 #[cfg(not(feature = "std"))]
1135 #[doc(hidden)]
1136 pub execution_probe: RuntimeExecutionProbe,
1137
1138 #[doc(hidden)]
1140 pub copperlists_manager: CopperListsManager<P, NBCL>,
1141
1142 #[doc(hidden)]
1144 pub keyframes_manager: KeyFramesManager,
1145
1146 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1148 #[doc(hidden)]
1149 pub parallel_rt: ParallelRt<NBCL>,
1150
1151 #[doc(hidden)]
1153 pub runtime_config: RuntimeConfig,
1154}
1155
1156impl<
1158 CT,
1159 CB,
1160 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload,
1161 M: CuMonitor,
1162 const NBCL: usize,
1163> ClockProvider for CuRuntime<CT, CB, P, M, NBCL>
1164{
1165 fn get_clock(&self) -> RobotClock {
1166 self.clock.clone()
1167 }
1168}
1169
1170impl<CT, CB, P: CopperListTuple, M: CuMonitor, const NBCL: usize> CuRuntime<CT, CB, P, M, NBCL> {
1171 #[inline]
1173 pub fn clock(&self) -> RobotClock {
1174 self.clock.clone()
1175 }
1176
1177 #[doc(hidden)]
1179 #[inline]
1180 pub fn clock_ref(&self) -> &RobotClock {
1181 &self.clock
1182 }
1183
1184 #[inline]
1186 pub fn subsystem_code(&self) -> u16 {
1187 self.subsystem_code
1188 }
1189
1190 #[inline]
1192 pub fn instance_id(&self) -> u32 {
1193 self.instance_id
1194 }
1195}
1196
1197#[cfg(feature = "std")]
1198impl<
1199 'cfg,
1200 CT,
1201 CB,
1202 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1203 M: CuMonitor,
1204 const NBCL: usize,
1205 TI,
1206 BI,
1207 MI,
1208 CLW,
1209 KFW,
1210> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
1211where
1212 TI: for<'c> Fn(
1213 Vec<Option<&'c ComponentConfig>>,
1214 &mut ResourceManager,
1215 &[Option<Arc<ThreadPool>>],
1216 ) -> CuResult<CT>,
1217 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1218 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1219 CLW: WriteStream<CopperList<P>> + 'static,
1220 KFW: WriteStream<KeyFrame> + 'static,
1221{
1222 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1223 let Self {
1224 clock,
1225 config,
1226 mission,
1227 subsystem,
1228 instance_id,
1229 resources,
1230 thread_pools,
1231 parts,
1232 copperlists_logger,
1233 keyframes_logger,
1234 } = self;
1235 let mut resources =
1236 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1237 let thread_pools = thread_pools.unwrap_or_default();
1238
1239 let graph = config.get_graph(Some(mission))?;
1240 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1241 .get_all_nodes()
1242 .iter()
1243 .map(|(_, node)| node.get_instance_config())
1244 .collect();
1245
1246 let tasks =
1247 (parts.tasks_instanciator)(all_instances_configs, &mut resources, &thread_pools)?;
1248
1249 #[cfg(feature = "std")]
1250 let execution_probe = std::sync::Arc::new(RuntimeExecutionProbe::default());
1251 #[cfg(not(feature = "std"))]
1252 let execution_probe = RuntimeExecutionProbe::default();
1253 let monitor_metadata = CuMonitoringMetadata::new(
1254 CompactString::from(mission),
1255 parts.monitored_components,
1256 parts.culist_component_mapping,
1257 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1258 build_monitor_topology(config, mission)?,
1259 None,
1260 )?
1261 .with_subsystem_id(subsystem.id())
1262 .with_instance_id(instance_id);
1263 #[cfg(feature = "std")]
1264 let monitor_runtime =
1265 CuMonitoringRuntime::new(MonitorExecutionProbe::from_shared(execution_probe.clone()));
1266 #[cfg(not(feature = "std"))]
1267 let monitor_runtime = CuMonitoringRuntime::unavailable();
1268 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1269 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1270
1271 let (copperlists_logger, keyframes_logger, keyframe_interval) = match &config.logging {
1272 Some(logging_config) if logging_config.enable_task_logging => (
1273 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1274 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1275 logging_config.keyframe_interval.unwrap(),
1276 ),
1277 Some(_) => (None, None, 0),
1278 None => (
1279 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1280 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1281 DEFAULT_KEYFRAME_INTERVAL,
1282 ),
1283 };
1284
1285 let copperlists_manager = CopperListsManager::new(copperlists_logger)?;
1286 #[cfg(target_os = "none")]
1287 {
1288 let cl_size = core::mem::size_of::<CopperList<P>>();
1289 let total_bytes = cl_size.saturating_mul(NBCL);
1290 info!(
1291 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1292 NBCL, cl_size, total_bytes
1293 );
1294 }
1295
1296 let keyframes_manager = KeyFramesManager {
1297 inner: KeyFrame::new(),
1298 logger: keyframes_logger,
1299 keyframe_interval,
1300 last_encoded_bytes: 0,
1301 forced_timestamp: None,
1302 locked: false,
1303 };
1304 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1305 let parallel_rt = ParallelRt::new(parts.parallel_rt_metadata)?;
1306
1307 let runtime_config = config.runtime.clone().unwrap_or_default();
1308 runtime_config.validate()?;
1309
1310 Ok(CuRuntime {
1311 subsystem_code: subsystem.code(),
1312 instance_id,
1313 tasks,
1314 bridges,
1315 resources,
1316 thread_pools,
1317 monitor,
1318 execution_probe,
1319 clock,
1320 copperlists_manager,
1321 keyframes_manager,
1322 #[cfg(all(feature = "std", feature = "parallel-rt"))]
1323 parallel_rt,
1324 runtime_config,
1325 })
1326 }
1327}
1328
1329#[cfg(not(feature = "std"))]
1330impl<
1331 'cfg,
1332 CT,
1333 CB,
1334 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1335 M: CuMonitor,
1336 const NBCL: usize,
1337 TI,
1338 BI,
1339 MI,
1340 CLW,
1341 KFW,
1342> CuRuntimeBuilder<'cfg, CT, CB, P, M, NBCL, TI, BI, MI, CLW, KFW>
1343where
1344 TI: for<'c> Fn(Vec<Option<&'c ComponentConfig>>, &mut ResourceManager) -> CuResult<CT>,
1345 BI: Fn(&CuConfig, &mut ResourceManager) -> CuResult<CB>,
1346 MI: Fn(&CuConfig, CuMonitoringMetadata, CuMonitoringRuntime) -> M,
1347 CLW: WriteStream<CopperList<P>> + 'static,
1348 KFW: WriteStream<KeyFrame> + 'static,
1349{
1350 pub fn build(self) -> CuResult<CuRuntime<CT, CB, P, M, NBCL>> {
1351 let Self {
1352 clock,
1353 config,
1354 mission,
1355 subsystem,
1356 instance_id,
1357 resources,
1358 parts,
1359 copperlists_logger,
1360 keyframes_logger,
1361 } = self;
1362 let mut resources =
1363 resources.ok_or_else(|| CuError::from("Resources missing from CuRuntimeBuilder"))?;
1364
1365 let graph = config.get_graph(Some(mission))?;
1366 let all_instances_configs: Vec<Option<&ComponentConfig>> = graph
1367 .get_all_nodes()
1368 .iter()
1369 .map(|(_, node)| node.get_instance_config())
1370 .collect();
1371
1372 let tasks = (parts.tasks_instanciator)(all_instances_configs, &mut resources)?;
1373
1374 let execution_probe = RuntimeExecutionProbe::default();
1375 let monitor_metadata = CuMonitoringMetadata::new(
1376 CompactString::from(mission),
1377 parts.monitored_components,
1378 parts.culist_component_mapping,
1379 CopperListInfo::new(core::mem::size_of::<CopperList<P>>(), NBCL),
1380 build_monitor_topology(config, mission)?,
1381 None,
1382 )?
1383 .with_subsystem_id(subsystem.id())
1384 .with_instance_id(instance_id);
1385 let monitor_runtime = CuMonitoringRuntime::unavailable();
1386 let monitor = (parts.monitor_instanciator)(config, monitor_metadata, monitor_runtime);
1387 let bridges = (parts.bridges_instanciator)(config, &mut resources)?;
1388
1389 let (copperlists_logger, keyframes_logger, keyframe_interval) = match &config.logging {
1390 Some(logging_config) if logging_config.enable_task_logging => (
1391 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1392 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1393 logging_config.keyframe_interval.unwrap(),
1394 ),
1395 Some(_) => (None, None, 0),
1396 None => (
1397 Some(Box::new(copperlists_logger) as Box<dyn WriteStream<CopperList<P>>>),
1398 Some(Box::new(keyframes_logger) as Box<dyn WriteStream<KeyFrame>>),
1399 DEFAULT_KEYFRAME_INTERVAL,
1400 ),
1401 };
1402
1403 let copperlists_manager = CopperListsManager::new(copperlists_logger)?;
1404 #[cfg(target_os = "none")]
1405 {
1406 let cl_size = core::mem::size_of::<CopperList<P>>();
1407 let total_bytes = cl_size.saturating_mul(NBCL);
1408 info!(
1409 "CuRuntimeBuilder: copperlists count={} cl_size={} total_bytes={}",
1410 NBCL, cl_size, total_bytes
1411 );
1412 }
1413
1414 let keyframes_manager = KeyFramesManager {
1415 inner: KeyFrame::new(),
1416 logger: keyframes_logger,
1417 keyframe_interval,
1418 last_encoded_bytes: 0,
1419 forced_timestamp: None,
1420 locked: false,
1421 };
1422
1423 let runtime_config = config.runtime.clone().unwrap_or_default();
1424 runtime_config.validate()?;
1425
1426 Ok(CuRuntime {
1427 subsystem_code: subsystem.code(),
1428 instance_id,
1429 tasks,
1430 bridges,
1431 resources,
1432 monitor,
1433 execution_probe,
1434 clock,
1435 copperlists_manager,
1436 keyframes_manager,
1437 runtime_config,
1438 })
1439 }
1440}
1441
1442#[derive(Clone, Encode, Decode)]
1446pub struct KeyFrame {
1447 pub culistid: u64,
1449 pub timestamp: CuTime,
1451 pub serialized_tasks: Vec<u8>,
1453}
1454
1455impl KeyFrame {
1456 fn new() -> Self {
1457 KeyFrame {
1458 culistid: 0,
1459 timestamp: CuTime::default(),
1460 serialized_tasks: Vec::new(),
1461 }
1462 }
1463
1464 fn reset(&mut self, culistid: u64, timestamp: CuTime) {
1466 self.culistid = culistid;
1467 self.timestamp = timestamp;
1468 self.serialized_tasks.clear();
1469 }
1470
1471 fn add_frozen_task(&mut self, task: &impl Freezable) -> Result<usize, EncodeError> {
1473 let cfg = bincode::config::standard();
1474 let mut sizer = EncoderImpl::<_, _>::new(SizeWriter::default(), cfg);
1475 BincodeAdapter(task).encode(&mut sizer)?;
1476 let need = sizer.into_writer().bytes_written as usize;
1477
1478 let start = self.serialized_tasks.len();
1479 self.serialized_tasks.resize(start + need, 0);
1480 let mut enc =
1481 EncoderImpl::<_, _>::new(SliceWriter::new(&mut self.serialized_tasks[start..]), cfg);
1482 BincodeAdapter(task).encode(&mut enc)?;
1483 Ok(need)
1484 }
1485}
1486
1487#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1489pub enum RuntimeLifecycleConfigSource {
1490 ProgrammaticOverride,
1491 ExternalFile,
1492 BundledDefault,
1493}
1494
1495#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1497pub struct RuntimeLifecycleStackInfo {
1498 pub app_name: String,
1499 pub app_version: String,
1500 pub git_commit: Option<String>,
1501 pub git_dirty: Option<bool>,
1502 pub subsystem_id: Option<String>,
1503 pub subsystem_code: u16,
1504 pub instance_id: u32,
1505}
1506
1507#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1509pub enum RuntimeLifecycleEvent {
1510 Instantiated {
1511 config_source: RuntimeLifecycleConfigSource,
1512 effective_config_ron: String,
1513 stack: RuntimeLifecycleStackInfo,
1514 },
1515 MissionStarted {
1516 mission: String,
1517 },
1518 MissionStopped {
1519 mission: String,
1520 reason: String,
1523 },
1524 Panic {
1526 message: String,
1527 file: Option<String>,
1528 line: Option<u32>,
1529 column: Option<u32>,
1530 },
1531 ShutdownCompleted,
1532}
1533
1534#[derive(Clone, Encode, Decode, Debug, PartialEq, Eq)]
1536pub struct RuntimeLifecycleRecord {
1537 pub timestamp: CuTime,
1538 pub event: RuntimeLifecycleEvent,
1539}
1540
1541impl<
1542 CT,
1543 CB,
1544 P: CopperListTuple + CuListZeroedInit + Default + AsyncCopperListPayload + 'static,
1545 M: CuMonitor,
1546 const NBCL: usize,
1547> CuRuntime<CT, CB, P, M, NBCL>
1548{
1549 #[inline]
1553 pub fn record_execution_marker(&self, marker: ExecutionMarker) {
1554 self.execution_probe.record(marker);
1555 }
1556
1557 #[inline]
1562 pub fn execution_probe_ref(&self) -> &RuntimeExecutionProbe {
1563 #[cfg(feature = "std")]
1564 {
1565 self.execution_probe.as_ref()
1566 }
1567
1568 #[cfg(not(feature = "std"))]
1569 {
1570 &self.execution_probe
1571 }
1572 }
1573}
1574
1575#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1580pub enum CuTaskType {
1581 Source,
1582 Regular,
1583 Sink,
1584}
1585
1586impl From<TaskKind> for CuTaskType {
1587 fn from(value: TaskKind) -> Self {
1588 match value {
1589 TaskKind::Source => CuTaskType::Source,
1590 TaskKind::Regular => CuTaskType::Regular,
1591 TaskKind::Sink => CuTaskType::Sink,
1592 }
1593 }
1594}
1595
1596#[derive(Debug, Clone)]
1597pub struct CuOutputPack {
1598 pub culist_index: u32,
1599 pub msg_types: Vec<String>,
1600}
1601
1602#[derive(Debug, Clone)]
1603pub struct CuInputMsg {
1604 pub culist_index: u32,
1605 pub msg_type: String,
1606 pub src_port: usize,
1607 pub edge_id: usize,
1608 pub connection_order: usize,
1609}
1610
1611pub struct CuExecutionStep {
1613 pub node_id: NodeId,
1615 pub node: Node,
1617 pub task_type: CuTaskType,
1619
1620 pub input_msg_indices_types: Vec<CuInputMsg>,
1622
1623 pub output_msg_pack: Option<CuOutputPack>,
1625}
1626
1627impl Debug for CuExecutionStep {
1628 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1629 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
1630 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
1631 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
1632 f.write_str(
1633 format!(
1634 " input_msg_types: {:?}\n",
1635 self.input_msg_indices_types
1636 )
1637 .as_str(),
1638 )?;
1639 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
1640 Ok(())
1641 }
1642}
1643
1644pub struct CuExecutionLoop {
1649 pub steps: Vec<CuExecutionUnit>,
1650 pub loop_count: Option<u32>,
1651}
1652
1653impl Debug for CuExecutionLoop {
1654 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1655 f.write_str("CuExecutionLoop:\n")?;
1656 for step in &self.steps {
1657 match step {
1658 CuExecutionUnit::Step(step) => {
1659 step.fmt(f)?;
1660 }
1661 CuExecutionUnit::Loop(l) => {
1662 l.fmt(f)?;
1663 }
1664 }
1665 }
1666
1667 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
1668 Ok(())
1669 }
1670}
1671
1672#[derive(Debug)]
1674pub enum CuExecutionUnit {
1675 Step(Box<CuExecutionStep>),
1676 Loop(CuExecutionLoop),
1677}
1678
1679fn find_output_pack_from_nodeid(
1680 node_id: NodeId,
1681 steps: &Vec<CuExecutionUnit>,
1682) -> Option<CuOutputPack> {
1683 for step in steps {
1684 match step {
1685 CuExecutionUnit::Loop(loop_unit) => {
1686 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
1687 return Some(output_pack);
1688 }
1689 }
1690 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1691 return step.output_msg_pack.clone();
1692 }
1693 _ => {}
1694 }
1695 }
1696 None
1697}
1698
1699pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
1700 let node = graph
1701 .get_node(node_id)
1702 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
1703
1704 if node.get_flavor() == crate::config::Flavor::Task {
1705 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
1706 }
1707
1708 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
1709 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
1710 Ok(match (has_inputs, has_outputs) {
1711 (false, true) => CuTaskType::Source,
1712 (true, false) => CuTaskType::Sink,
1713 _ => CuTaskType::Regular,
1714 })
1715}
1716
1717fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
1722 input_msg_indices_types.sort_by_key(|input| input.connection_order);
1723}
1724
1725fn plan_tasks_tree_branch(
1727 graph: &CuGraph,
1728 mut next_culist_output_index: u32,
1729 starting_point: NodeId,
1730 plan: &mut Vec<CuExecutionUnit>,
1731) -> CuResult<(u32, bool)> {
1732 #[cfg(all(feature = "std", feature = "macro_debug"))]
1733 eprintln!("-- starting branch from node {starting_point}");
1734
1735 let mut handled = false;
1736
1737 for id in graph.bfs_nodes(starting_point) {
1738 let node_ref = graph.get_node(id).unwrap();
1739 #[cfg(all(feature = "std", feature = "macro_debug"))]
1740 eprintln!(" Visiting node: {node_ref:?}");
1741
1742 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1743 let output_msg_pack: Option<CuOutputPack>;
1744 let task_type = find_task_type_for_id(graph, id)?;
1745
1746 match task_type {
1747 CuTaskType::Source => {
1748 #[cfg(all(feature = "std", feature = "macro_debug"))]
1749 eprintln!(" → Source node, assign output index {next_culist_output_index}");
1750 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1751 if msg_types.is_empty() {
1752 return Err(CuError::from(format!(
1753 "Source node '{}' has no declared outputs",
1754 node_ref.get_id()
1755 )));
1756 }
1757 output_msg_pack = Some(CuOutputPack {
1758 culist_index: next_culist_output_index,
1759 msg_types,
1760 });
1761 next_culist_output_index += 1;
1762 }
1763 CuTaskType::Sink => {
1764 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1765 edge_ids.sort();
1766 #[cfg(all(feature = "std", feature = "macro_debug"))]
1767 eprintln!(" → Sink with incoming edges: {edge_ids:?}");
1768 for edge_id in edge_ids {
1769 let edge = graph
1770 .edge(edge_id)
1771 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1772 let pid = graph
1773 .get_node_id_by_name(edge.src.as_str())
1774 .unwrap_or_else(|| {
1775 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1776 });
1777 let output_pack = find_output_pack_from_nodeid(pid, plan);
1778 if let Some(output_pack) = output_pack {
1779 #[cfg(all(feature = "std", feature = "macro_debug"))]
1780 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
1781 let msg_type = edge.msg.as_str();
1782 let src_port = output_pack
1783 .msg_types
1784 .iter()
1785 .position(|msg| msg == msg_type)
1786 .unwrap_or_else(|| {
1787 panic!(
1788 "Missing output port for message type '{msg_type}' on node {pid}"
1789 )
1790 });
1791 input_msg_indices_types.push(CuInputMsg {
1792 culist_index: output_pack.culist_index,
1793 msg_type: msg_type.to_string(),
1794 src_port,
1795 edge_id,
1796 connection_order: edge.order,
1797 });
1798 } else {
1799 #[cfg(all(feature = "std", feature = "macro_debug"))]
1800 eprintln!(" ✗ Input from {pid} not ready, returning");
1801 return Ok((next_culist_output_index, handled));
1802 }
1803 }
1804 output_msg_pack = Some(CuOutputPack {
1805 culist_index: next_culist_output_index,
1806 msg_types: Vec::from(["()".to_string()]),
1807 });
1808 next_culist_output_index += 1;
1809 }
1810 CuTaskType::Regular => {
1811 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1812 edge_ids.sort();
1813 #[cfg(all(feature = "std", feature = "macro_debug"))]
1814 eprintln!(" → Regular task with incoming edges: {edge_ids:?}");
1815 for edge_id in edge_ids {
1816 let edge = graph
1817 .edge(edge_id)
1818 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1819 let pid = graph
1820 .get_node_id_by_name(edge.src.as_str())
1821 .unwrap_or_else(|| {
1822 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1823 });
1824 let output_pack = find_output_pack_from_nodeid(pid, plan);
1825 if let Some(output_pack) = output_pack {
1826 #[cfg(all(feature = "std", feature = "macro_debug"))]
1827 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
1828 let msg_type = edge.msg.as_str();
1829 let src_port = output_pack
1830 .msg_types
1831 .iter()
1832 .position(|msg| msg == msg_type)
1833 .unwrap_or_else(|| {
1834 panic!(
1835 "Missing output port for message type '{msg_type}' on node {pid}"
1836 )
1837 });
1838 input_msg_indices_types.push(CuInputMsg {
1839 culist_index: output_pack.culist_index,
1840 msg_type: msg_type.to_string(),
1841 src_port,
1842 edge_id,
1843 connection_order: edge.order,
1844 });
1845 } else {
1846 #[cfg(all(feature = "std", feature = "macro_debug"))]
1847 eprintln!(" ✗ Input from {pid} not ready, returning");
1848 return Ok((next_culist_output_index, handled));
1849 }
1850 }
1851 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1852 if msg_types.is_empty() {
1853 return Err(CuError::from(format!(
1854 "Regular node '{}' has no declared outputs",
1855 node_ref.get_id()
1856 )));
1857 }
1858 output_msg_pack = Some(CuOutputPack {
1859 culist_index: next_culist_output_index,
1860 msg_types,
1861 });
1862 next_culist_output_index += 1;
1863 }
1864 }
1865
1866 sort_inputs_by_connection_order(&mut input_msg_indices_types);
1867
1868 if let Some(pos) = plan
1869 .iter()
1870 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
1871 {
1872 #[cfg(all(feature = "std", feature = "macro_debug"))]
1873 eprintln!(" → Already in plan, modifying existing step");
1874 let mut step = plan.remove(pos);
1875 if let CuExecutionUnit::Step(ref mut s) = step {
1876 s.input_msg_indices_types = input_msg_indices_types;
1877 }
1878 plan.push(step);
1879 } else {
1880 #[cfg(all(feature = "std", feature = "macro_debug"))]
1881 eprintln!(" → New step added to plan");
1882 let step = CuExecutionStep {
1883 node_id: id,
1884 node: node_ref.clone(),
1885 task_type,
1886 input_msg_indices_types,
1887 output_msg_pack,
1888 };
1889 plan.push(CuExecutionUnit::Step(Box::new(step)));
1890 }
1891
1892 handled = true;
1893 }
1894
1895 #[cfg(all(feature = "std", feature = "macro_debug"))]
1896 eprintln!("-- finished branch from node {starting_point} with handled={handled}");
1897 Ok((next_culist_output_index, handled))
1898}
1899
1900pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
1903 #[cfg(all(feature = "std", feature = "macro_debug"))]
1904 eprintln!("[runtime plan]");
1905 let mut plan = Vec::new();
1906 let mut next_culist_output_index = 0u32;
1907
1908 let mut queue: VecDeque<NodeId> = VecDeque::new();
1909 for node_id in graph.node_ids() {
1910 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
1911 queue.push_back(node_id);
1912 }
1913 }
1914
1915 #[cfg(all(feature = "std", feature = "macro_debug"))]
1916 eprintln!("Initial source nodes: {queue:?}");
1917
1918 while let Some(start_node) = queue.pop_front() {
1919 #[cfg(all(feature = "std", feature = "macro_debug"))]
1920 eprintln!("→ Starting BFS from source {start_node}");
1921 for node_id in graph.bfs_nodes(start_node) {
1922 let already_in_plan = plan
1923 .iter()
1924 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
1925 if already_in_plan {
1926 #[cfg(all(feature = "std", feature = "macro_debug"))]
1927 eprintln!(" → Node {node_id} already planned, skipping");
1928 continue;
1929 }
1930
1931 #[cfg(all(feature = "std", feature = "macro_debug"))]
1932 eprintln!(" Planning from node {node_id}");
1933 let (new_index, handled) =
1934 plan_tasks_tree_branch(graph, next_culist_output_index, node_id, &mut plan)?;
1935 next_culist_output_index = new_index;
1936
1937 if !handled {
1938 #[cfg(all(feature = "std", feature = "macro_debug"))]
1939 eprintln!(" ✗ Node {node_id} was not handled, skipping enqueue of neighbors");
1940 continue;
1941 }
1942
1943 #[cfg(all(feature = "std", feature = "macro_debug"))]
1944 eprintln!(" ✓ Node {node_id} handled successfully, enqueueing neighbors");
1945 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
1946 #[cfg(all(feature = "std", feature = "macro_debug"))]
1947 eprintln!(" → Enqueueing neighbor {neighbor}");
1948 queue.push_back(neighbor);
1949 }
1950 }
1951 }
1952
1953 let mut planned_nodes = BTreeSet::new();
1954 for unit in &plan {
1955 if let CuExecutionUnit::Step(step) = unit {
1956 planned_nodes.insert(step.node_id);
1957 }
1958 }
1959
1960 let mut missing = Vec::new();
1961 for node_id in graph.node_ids() {
1962 if !planned_nodes.contains(&node_id) {
1963 if let Some(node) = graph.get_node(node_id) {
1964 missing.push(node.get_id().to_string());
1965 } else {
1966 missing.push(format!("node_id_{node_id}"));
1967 }
1968 }
1969 }
1970
1971 if !missing.is_empty() {
1972 missing.sort();
1973 return Err(CuError::from(format!(
1974 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
1975 missing.join(", ")
1976 )));
1977 }
1978
1979 Ok(CuExecutionLoop {
1980 steps: plan,
1981 loop_count: None,
1982 })
1983}
1984
1985#[cfg(test)]
1987mod tests {
1988 use super::*;
1989 use crate::config::Node;
1990 use crate::context::CuContext;
1991 use crate::cutask::CuSinkTask;
1992 use crate::cutask::{CuSrcTask, Freezable};
1993 use crate::monitoring::NoMonitor;
1994 use crate::reflect::Reflect;
1995 use bincode::Encode;
1996 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
1997 use serde_derive::{Deserialize, Serialize};
1998 #[cfg(feature = "std")]
1999 use std::sync::{Arc, Mutex};
2000
2001 #[derive(Reflect)]
2002 pub struct TestSource {}
2003
2004 impl Freezable for TestSource {}
2005
2006 impl CuSrcTask for TestSource {
2007 type Resources<'r> = ();
2008 type Output<'m> = ();
2009 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2010 where
2011 Self: Sized,
2012 {
2013 Ok(Self {})
2014 }
2015
2016 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
2017 Ok(())
2018 }
2019 }
2020
2021 #[derive(Reflect)]
2022 pub struct TestSink {}
2023
2024 impl Freezable for TestSink {}
2025
2026 impl CuSinkTask for TestSink {
2027 type Resources<'r> = ();
2028 type Input<'m> = ();
2029
2030 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2031 where
2032 Self: Sized,
2033 {
2034 Ok(Self {})
2035 }
2036
2037 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
2038 Ok(())
2039 }
2040 }
2041
2042 type Tasks = (TestSource, TestSink);
2044 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
2045 const TEST_NBCL: usize = 2;
2046
2047 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2048 struct Msgs(());
2049
2050 impl ErasedCuStampedDataSet for Msgs {
2051 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2052 Vec::new()
2053 }
2054 }
2055
2056 impl MatchingTasks for Msgs {
2057 fn get_all_task_ids() -> &'static [&'static str] {
2058 &[]
2059 }
2060 }
2061
2062 impl CuListZeroedInit for Msgs {
2063 fn init_zeroed(&mut self) {}
2064 }
2065
2066 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2067 struct IntMsgs(i32);
2068
2069 impl ErasedCuStampedDataSet for IntMsgs {
2070 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2071 Vec::new()
2072 }
2073 }
2074
2075 impl MatchingTasks for IntMsgs {
2076 fn get_all_task_ids() -> &'static [&'static str] {
2077 &[]
2078 }
2079 }
2080
2081 impl CuListZeroedInit for IntMsgs {
2082 fn init_zeroed(&mut self) {}
2083 }
2084
2085 #[cfg(feature = "std")]
2086 fn tasks_instanciator(
2087 all_instances_configs: Vec<Option<&ComponentConfig>>,
2088 _resources: &mut ResourceManager,
2089 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
2090 ) -> CuResult<Tasks> {
2091 Ok((
2092 TestSource::new(all_instances_configs[0], ())?,
2093 TestSink::new(all_instances_configs[1], ())?,
2094 ))
2095 }
2096
2097 #[cfg(not(feature = "std"))]
2098 fn tasks_instanciator(
2099 all_instances_configs: Vec<Option<&ComponentConfig>>,
2100 _resources: &mut ResourceManager,
2101 ) -> CuResult<Tasks> {
2102 Ok((
2103 TestSource::new(all_instances_configs[0], ())?,
2104 TestSink::new(all_instances_configs[1], ())?,
2105 ))
2106 }
2107
2108 fn monitor_instanciator(
2109 _config: &CuConfig,
2110 metadata: CuMonitoringMetadata,
2111 runtime: CuMonitoringRuntime,
2112 ) -> NoMonitor {
2113 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
2114 }
2115
2116 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
2117 Ok(())
2118 }
2119
2120 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
2121 Ok(ResourceManager::new(&[]))
2122 }
2123
2124 #[derive(Debug)]
2125 struct FakeWriter {}
2126
2127 impl<E: Encode> WriteStream<E> for FakeWriter {
2128 fn log(&mut self, _obj: &E) -> CuResult<()> {
2129 Ok(())
2130 }
2131 }
2132
2133 #[cfg(not(feature = "async-cl-io"))]
2134 #[derive(Debug)]
2135 struct RecordingSyncWriter {
2136 ids: Arc<Mutex<Vec<u64>>>,
2137 last_log_bytes: usize,
2138 fail_on: Option<u64>,
2139 }
2140
2141 #[cfg(not(feature = "async-cl-io"))]
2142 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
2143 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2144 self.ids.lock().unwrap().push(culist.id);
2145 if self.fail_on == Some(culist.id) {
2146 return Err(CuError::from(format!(
2147 "logger failed for CopperList #{}",
2148 culist.id
2149 )));
2150 }
2151 Ok(())
2152 }
2153
2154 fn last_log_bytes(&self) -> Option<usize> {
2155 Some(self.last_log_bytes)
2156 }
2157 }
2158
2159 #[test]
2160 fn test_runtime_instantiation() {
2161 let mut config = CuConfig::default();
2162 let graph = config.get_graph_mut(None).unwrap();
2163 graph.add_node(Node::new("a", "TestSource")).unwrap();
2164 graph.add_node(Node::new("b", "TestSink")).unwrap();
2165 graph.connect(0, 1, "()").unwrap();
2166 let runtime: CuResult<TestRuntime> =
2167 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2168 RobotClock::default(),
2169 &config,
2170 crate::config::DEFAULT_MISSION_ID,
2171 CuRuntimeParts::new(
2172 tasks_instanciator,
2173 &[],
2174 &[],
2175 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2176 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2177 monitor_instanciator,
2178 bridges_instanciator,
2179 ),
2180 FakeWriter {},
2181 FakeWriter {},
2182 )
2183 .try_with_resources_instantiator(resources_instanciator)
2184 .and_then(|builder| builder.build());
2185 assert!(runtime.is_ok());
2186 }
2187
2188 #[test]
2189 fn test_rate_target_period_rejects_zero() {
2190 let err = rate_target_period(0).expect_err("zero rate target should fail");
2191 assert!(
2192 err.to_string()
2193 .contains("Runtime rate target cannot be zero"),
2194 "unexpected error: {err}"
2195 );
2196 }
2197
2198 #[test]
2199 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
2200 let (clock, mock) = RobotClock::mock();
2201 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2202 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
2203
2204 mock.set_value(10_000_000);
2205 limiter.mark_tick(&clock);
2206
2207 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
2208 }
2209
2210 #[test]
2211 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
2212 let (clock, mock) = RobotClock::mock();
2213 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2214
2215 mock.set_value(35_000_000);
2216 limiter.mark_tick(&clock);
2217
2218 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
2219 }
2220
2221 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
2222 #[test]
2223 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
2224 let (clock, _) = RobotClock::mock();
2225 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
2226 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
2227
2228 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
2229 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
2230 }
2231
2232 #[cfg(not(feature = "async-cl-io"))]
2233 #[test]
2234 fn test_copperlists_manager_lifecycle() {
2235 let mut config = CuConfig::default();
2236 let graph = config.get_graph_mut(None).unwrap();
2237 graph.add_node(Node::new("a", "TestSource")).unwrap();
2238 graph.add_node(Node::new("b", "TestSink")).unwrap();
2239 graph.connect(0, 1, "()").unwrap();
2240
2241 let mut runtime: TestRuntime =
2242 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2243 RobotClock::default(),
2244 &config,
2245 crate::config::DEFAULT_MISSION_ID,
2246 CuRuntimeParts::new(
2247 tasks_instanciator,
2248 &[],
2249 &[],
2250 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2251 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2252 monitor_instanciator,
2253 bridges_instanciator,
2254 ),
2255 FakeWriter {},
2256 FakeWriter {},
2257 )
2258 .try_with_resources_instantiator(resources_instanciator)
2259 .and_then(|builder| builder.build())
2260 .unwrap();
2261
2262 {
2264 let copperlists = &mut runtime.copperlists_manager;
2265 let culist0 = copperlists
2266 .create()
2267 .expect("Ran out of space for copper lists");
2268 let id = culist0.id;
2269 assert_eq!(id, 0);
2270 culist0.change_state(CopperListState::Processing);
2271 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2272 }
2273
2274 {
2275 let copperlists = &mut runtime.copperlists_manager;
2276 let culist1 = copperlists
2277 .create()
2278 .expect("Ran out of space for copper lists");
2279 let id = culist1.id;
2280 assert_eq!(id, 1);
2281 culist1.change_state(CopperListState::Processing);
2282 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2283 }
2284
2285 {
2286 let copperlists = &mut runtime.copperlists_manager;
2287 let culist2 = copperlists.create();
2288 assert!(culist2.is_err());
2289 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2290 let _ = copperlists.end_of_processing(1);
2292 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2293 }
2294
2295 {
2297 let copperlists = &mut runtime.copperlists_manager;
2298 let culist2 = copperlists
2299 .create()
2300 .expect("Ran out of space for copper lists");
2301 let id = culist2.id;
2302 assert_eq!(id, 2);
2303 culist2.change_state(CopperListState::Processing);
2304 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2305 let _ = copperlists.end_of_processing(0);
2307 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2309
2310 let _ = copperlists.end_of_processing(2);
2312 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2315 }
2316 }
2317
2318 #[cfg(not(feature = "async-cl-io"))]
2319 #[test]
2320 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
2321 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2322
2323 assert_eq!(copperlists.next_cl_id(), 0);
2324 assert_eq!(copperlists.last_cl_id(), 0);
2325 assert!(copperlists.peek().is_none());
2326
2327 {
2328 let culist = copperlists.create().unwrap();
2329 culist.msgs.0 = 11;
2330 assert_eq!(culist.id, 0);
2331 assert_eq!(culist.get_state(), CopperListState::Initialized);
2332 }
2333
2334 assert_eq!(copperlists.next_cl_id(), 1);
2335 assert_eq!(copperlists.last_cl_id(), 0);
2336 let peeked = copperlists.peek().unwrap();
2337 assert_eq!(peeked.id, 0);
2338 assert_eq!(peeked.msgs.0, 11);
2339 assert_eq!(peeked.get_state(), CopperListState::Initialized);
2340 }
2341
2342 #[cfg(not(feature = "async-cl-io"))]
2343 #[test]
2344 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
2345 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2346
2347 {
2348 let culist = copperlists.create().unwrap();
2349 culist.msgs.0 = 41;
2350 culist.change_state(CopperListState::Processing);
2351 assert_eq!(culist.id, 0);
2352 }
2353
2354 copperlists.end_of_processing(0).unwrap();
2355 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2356
2357 let reused = copperlists.create().unwrap();
2358 assert_eq!(reused.id, 1);
2359 assert_eq!(reused.get_state(), CopperListState::Initialized);
2360 assert_eq!(reused.msgs.0, 41);
2361 }
2362
2363 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2364 #[test]
2365 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
2366 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
2367 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2368
2369 {
2370 let culist = copperlists.create().unwrap();
2371 culist.msgs.0 = 10;
2372 culist.change_state(CopperListState::Processing);
2373 }
2374 {
2375 let culist = copperlists.create().unwrap();
2376 culist.msgs.0 = 20;
2377 culist.change_state(CopperListState::Processing);
2378 }
2379
2380 let _ = copperlists.end_of_processing(99);
2381 }
2382
2383 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2384 #[test]
2385 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
2386 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
2387 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2388
2389 {
2390 let culist = copperlists.create().unwrap();
2391 culist.msgs.0 = 10;
2392 assert_eq!(culist.get_state(), CopperListState::Initialized);
2393 }
2394
2395 let _ = copperlists.end_of_processing(0);
2396 }
2397
2398 #[cfg(not(feature = "async-cl-io"))]
2399 #[test]
2400 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
2401 let ids = Arc::new(Mutex::new(Vec::new()));
2402 let mut copperlists =
2403 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
2404 ids: ids.clone(),
2405 last_log_bytes: 17,
2406 fail_on: None,
2407 })))
2408 .unwrap();
2409
2410 {
2411 let culist = copperlists.create().unwrap();
2412 culist.msgs.0 = 10;
2413 culist.change_state(CopperListState::Processing);
2414 }
2415 {
2416 let culist = copperlists.create().unwrap();
2417 culist.msgs.0 = 20;
2418 culist.change_state(CopperListState::Processing);
2419 }
2420
2421 copperlists.end_of_processing(0).unwrap();
2422 assert!(ids.lock().unwrap().is_empty());
2423 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2424
2425 copperlists.end_of_processing(1).unwrap();
2426
2427 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
2428 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2429 }
2430
2431 #[cfg(not(feature = "async-cl-io"))]
2432 #[test]
2433 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
2434 let ids = Arc::new(Mutex::new(Vec::new()));
2435 let mut copperlists =
2436 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2437 ids: ids.clone(),
2438 last_log_bytes: 17,
2439 fail_on: None,
2440 })))
2441 .unwrap();
2442 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
2443
2444 {
2445 let culist = copperlists.create().unwrap();
2446 culist.msgs.0 = 10;
2447 culist.change_state(CopperListState::Processing);
2448 }
2449
2450 {
2451 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
2452 capture.select_slot(0);
2453 crate::monitoring::record_payload_handle_bytes(32);
2454 }
2455
2456 copperlists.end_of_processing(0).unwrap();
2457
2458 assert_eq!(*ids.lock().unwrap(), vec![0]);
2459 assert_eq!(copperlists.last_encoded_bytes, 17);
2460 assert_eq!(copperlists.last_handle_bytes, 32);
2461 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2462 }
2463
2464 #[cfg(not(feature = "async-cl-io"))]
2465 #[test]
2466 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
2467 let ids = Arc::new(Mutex::new(Vec::new()));
2468 let mut copperlists =
2469 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2470 ids: ids.clone(),
2471 last_log_bytes: 17,
2472 fail_on: Some(0),
2473 })))
2474 .unwrap();
2475
2476 {
2477 let culist = copperlists.create().unwrap();
2478 culist.change_state(CopperListState::Processing);
2479 }
2480
2481 let err = copperlists.end_of_processing(0).unwrap_err();
2482
2483 assert!(
2484 err.to_string().contains("logger failed for CopperList #0"),
2485 "unexpected error: {err}"
2486 );
2487 assert_eq!(*ids.lock().unwrap(), vec![0]);
2488 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2489 assert_eq!(copperlists.last_encoded_bytes, 0);
2490 assert_eq!(copperlists.last_handle_bytes, 0);
2491
2492 let peeked = copperlists.peek().unwrap();
2493 assert_eq!(peeked.id, 0);
2494 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
2495 }
2496
2497 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
2498 #[test]
2499 #[should_panic(
2500 expected = "sync boxed end_of_processing expected CopperList #7 to be Processing"
2501 )]
2502 fn test_sync_end_of_processing_boxed_wrong_state_panics_in_debug() {
2503 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2504 let culist = Box::new(CopperList::new(7, IntMsgs::default()));
2505
2506 let _ = copperlists.end_of_processing_boxed(culist);
2507 }
2508
2509 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2510 #[derive(Debug, Default)]
2511 struct RecordingWriter {
2512 ids: Arc<Mutex<Vec<u64>>>,
2513 }
2514
2515 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2516 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
2517 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
2518 self.ids.lock().unwrap().push(culist.id);
2519 std::thread::sleep(std::time::Duration::from_millis(2));
2520 Ok(())
2521 }
2522 }
2523
2524 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2525 #[test]
2526 fn test_async_copperlists_manager_flushes_in_order() {
2527 let ids = Arc::new(Mutex::new(Vec::new()));
2528 let mut copperlists = CopperListsManager::<Msgs, 4>::new(Some(Box::new(RecordingWriter {
2529 ids: ids.clone(),
2530 })))
2531 .unwrap();
2532
2533 for expected_id in 0..4 {
2534 let culist = copperlists.create().unwrap();
2535 assert_eq!(culist.id, expected_id);
2536 culist.change_state(CopperListState::Processing);
2537 copperlists.end_of_processing(expected_id).unwrap();
2538 }
2539
2540 copperlists.finish_pending().unwrap();
2541 assert_eq!(copperlists.available_copper_lists().unwrap(), 4);
2542 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
2543 }
2544
2545 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2546 #[test]
2547 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
2548 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2549
2550 {
2551 let culist = copperlists.create().unwrap();
2552 assert_eq!(culist.id, 0);
2553 assert_eq!(culist.get_state(), CopperListState::Initialized);
2554 culist.msgs.0 = 41;
2555 culist.change_state(CopperListState::Processing);
2556 }
2557
2558 copperlists.end_of_processing(0).unwrap();
2559 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2560
2561 let reused = copperlists.create().unwrap();
2562 assert_eq!(reused.id, 1);
2563 assert_eq!(reused.get_state(), CopperListState::Initialized);
2564 assert_eq!(reused.msgs.0, 41);
2565 }
2566
2567 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
2568 #[test]
2569 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
2570 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
2571 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2572
2573 let culist = copperlists.create().unwrap();
2574 assert_eq!(culist.id, 0);
2575 assert_eq!(culist.get_state(), CopperListState::Initialized);
2576
2577 let _ = copperlists.end_of_processing(0);
2578 }
2579
2580 #[test]
2581 fn test_runtime_task_input_order() {
2582 let mut config = CuConfig::default();
2583 let graph = config.get_graph_mut(None).unwrap();
2584 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
2585 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
2586 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
2587
2588 assert_eq!(src1_id, 0);
2589 assert_eq!(src2_id, 1);
2590
2591 let src1_type = "src1_type";
2593 let src2_type = "src2_type";
2594 graph.connect(src2_id, sink_id, src2_type).unwrap();
2595 graph.connect(src1_id, sink_id, src1_type).unwrap();
2596
2597 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2598 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2599 assert_eq!(src1_edge_id, 1);
2602 assert_eq!(src2_edge_id, 0);
2603
2604 let runtime = compute_runtime_plan(graph).unwrap();
2605 let sink_step = runtime
2606 .steps
2607 .iter()
2608 .find_map(|step| match step {
2609 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
2610 _ => None,
2611 })
2612 .unwrap();
2613
2614 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
2617 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
2618 }
2619
2620 #[test]
2621 fn test_runtime_output_ports_unique_ordered() {
2622 let mut config = CuConfig::default();
2623 let graph = config.get_graph_mut(None).unwrap();
2624 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2625 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
2626 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
2627 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
2628 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
2629
2630 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
2631 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
2632 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
2633 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
2634
2635 let runtime = compute_runtime_plan(graph).unwrap();
2636 let src_step = runtime
2637 .steps
2638 .iter()
2639 .find_map(|step| match step {
2640 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2641 _ => None,
2642 })
2643 .unwrap();
2644
2645 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2646 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
2647
2648 let dst_a_step = runtime
2649 .steps
2650 .iter()
2651 .find_map(|step| match step {
2652 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
2653 _ => None,
2654 })
2655 .unwrap();
2656 let dst_b_step = runtime
2657 .steps
2658 .iter()
2659 .find_map(|step| match step {
2660 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
2661 _ => None,
2662 })
2663 .unwrap();
2664 let dst_a2_step = runtime
2665 .steps
2666 .iter()
2667 .find_map(|step| match step {
2668 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
2669 _ => None,
2670 })
2671 .unwrap();
2672 let dst_c_step = runtime
2673 .steps
2674 .iter()
2675 .find_map(|step| match step {
2676 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
2677 _ => None,
2678 })
2679 .unwrap();
2680
2681 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
2682 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
2683 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
2684 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
2685 }
2686
2687 #[test]
2688 fn test_runtime_output_ports_fanout_single() {
2689 let mut config = CuConfig::default();
2690 let graph = config.get_graph_mut(None).unwrap();
2691 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2692 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
2693 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
2694
2695 graph.connect(src_id, dst_a_id, "i32").unwrap();
2696 graph.connect(src_id, dst_b_id, "i32").unwrap();
2697
2698 let runtime = compute_runtime_plan(graph).unwrap();
2699 let src_step = runtime
2700 .steps
2701 .iter()
2702 .find_map(|step| match step {
2703 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2704 _ => None,
2705 })
2706 .unwrap();
2707
2708 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2709 assert_eq!(output_pack.msg_types, vec!["i32"]);
2710 }
2711
2712 #[test]
2713 fn test_runtime_output_ports_include_nc_outputs() {
2714 let mut config = CuConfig::default();
2715 let graph = config.get_graph_mut(None).unwrap();
2716 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2717 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
2718 graph.connect(src_id, dst_id, "msg::A").unwrap();
2719 graph
2720 .get_node_mut(src_id)
2721 .expect("missing source node")
2722 .add_nc_output("msg::B", usize::MAX);
2723
2724 let runtime = compute_runtime_plan(graph).unwrap();
2725 let src_step = runtime
2726 .steps
2727 .iter()
2728 .find_map(|step| match step {
2729 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2730 _ => None,
2731 })
2732 .unwrap();
2733 let dst_step = runtime
2734 .steps
2735 .iter()
2736 .find_map(|step| match step {
2737 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2738 _ => None,
2739 })
2740 .unwrap();
2741
2742 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2743 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2744 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
2745 }
2746
2747 #[test]
2748 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
2749 let txt = r#"(
2750 tasks: [
2751 (id: "src", type: "a"),
2752 (id: "regular", type: "b"),
2753 ],
2754 cnx: [
2755 (src: "src", dst: "regular", msg: "msg::A"),
2756 (src: "regular", dst: "__nc__", msg: "msg::B"),
2757 ]
2758 )"#;
2759 let config = CuConfig::deserialize_ron(txt).unwrap();
2760 let graph = config.get_graph(None).unwrap();
2761 let regular_id = graph.get_node_id_by_name("regular").unwrap();
2762
2763 let runtime = compute_runtime_plan(graph).unwrap();
2764 let regular_step = runtime
2765 .steps
2766 .iter()
2767 .find_map(|step| match step {
2768 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
2769 _ => None,
2770 })
2771 .unwrap();
2772
2773 assert_eq!(regular_step.task_type, CuTaskType::Regular);
2774 assert_eq!(
2775 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
2776 vec!["msg::B"]
2777 );
2778 }
2779
2780 #[test]
2781 fn test_runtime_output_ports_respect_connection_order_with_nc() {
2782 let txt = r#"(
2783 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2784 cnx: [
2785 (src: "src", dst: "__nc__", msg: "msg::A"),
2786 (src: "src", dst: "sink", msg: "msg::B"),
2787 ]
2788 )"#;
2789 let config = CuConfig::deserialize_ron(txt).unwrap();
2790 let graph = config.get_graph(None).unwrap();
2791 let src_id = graph.get_node_id_by_name("src").unwrap();
2792 let dst_id = graph.get_node_id_by_name("sink").unwrap();
2793
2794 let runtime = compute_runtime_plan(graph).unwrap();
2795 let src_step = runtime
2796 .steps
2797 .iter()
2798 .find_map(|step| match step {
2799 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2800 _ => None,
2801 })
2802 .unwrap();
2803 let dst_step = runtime
2804 .steps
2805 .iter()
2806 .find_map(|step| match step {
2807 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2808 _ => None,
2809 })
2810 .unwrap();
2811
2812 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2813 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2814 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
2815 }
2816
2817 #[cfg(feature = "std")]
2818 #[test]
2819 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
2820 let txt = r#"(
2821 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2822 cnx: [
2823 (src: "src", dst: "__nc__", msg: "msg::A"),
2824 (src: "src", dst: "sink", msg: "msg::B"),
2825 ]
2826 )"#;
2827 let tmp = tempfile::NamedTempFile::new().unwrap();
2828 std::fs::write(tmp.path(), txt).unwrap();
2829 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
2830 let graph = config.get_graph(None).unwrap();
2831 let src_id = graph.get_node_id_by_name("src").unwrap();
2832 let dst_id = graph.get_node_id_by_name("sink").unwrap();
2833
2834 let runtime = compute_runtime_plan(graph).unwrap();
2835 let src_step = runtime
2836 .steps
2837 .iter()
2838 .find_map(|step| match step {
2839 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2840 _ => None,
2841 })
2842 .unwrap();
2843 let dst_step = runtime
2844 .steps
2845 .iter()
2846 .find_map(|step| match step {
2847 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2848 _ => None,
2849 })
2850 .unwrap();
2851
2852 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2853 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2854 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
2855 }
2856
2857 #[test]
2858 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
2859 let txt = r#"(
2860 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2861 cnx: [
2862 (src: "src", dst: "__nc__", msg: "i32"),
2863 (src: "src", dst: "sink", msg: "bool"),
2864 ]
2865 )"#;
2866 let config = CuConfig::deserialize_ron(txt).unwrap();
2867 let graph = config.get_graph(None).unwrap();
2868 let src_id = graph.get_node_id_by_name("src").unwrap();
2869 let dst_id = graph.get_node_id_by_name("sink").unwrap();
2870
2871 let runtime = compute_runtime_plan(graph).unwrap();
2872 let src_step = runtime
2873 .steps
2874 .iter()
2875 .find_map(|step| match step {
2876 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2877 _ => None,
2878 })
2879 .unwrap();
2880 let dst_step = runtime
2881 .steps
2882 .iter()
2883 .find_map(|step| match step {
2884 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2885 _ => None,
2886 })
2887 .unwrap();
2888
2889 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2890 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
2891 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
2892 }
2893
2894 #[test]
2895 fn test_runtime_plan_diamond_case1() {
2896 let mut config = CuConfig::default();
2898 let graph = config.get_graph_mut(None).unwrap();
2899 let cam0_id = graph
2900 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
2901 .unwrap();
2902 let inf0_id = graph
2903 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
2904 .unwrap();
2905 let broadcast_id = graph
2906 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
2907 .unwrap();
2908
2909 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
2911 graph.connect(cam0_id, inf0_id, "i32").unwrap();
2912 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
2913
2914 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
2915 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
2916
2917 assert_eq!(edge_cam0_to_inf0, 0);
2918 assert_eq!(edge_cam0_to_broadcast, 1);
2919
2920 let runtime = compute_runtime_plan(graph).unwrap();
2921 let broadcast_step = runtime
2922 .steps
2923 .iter()
2924 .find_map(|step| match step {
2925 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
2926 _ => None,
2927 })
2928 .unwrap();
2929
2930 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
2931 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
2932 }
2933
2934 #[test]
2935 fn test_runtime_plan_diamond_case2() {
2936 let mut config = CuConfig::default();
2938 let graph = config.get_graph_mut(None).unwrap();
2939 let cam0_id = graph
2940 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
2941 .unwrap();
2942 let inf0_id = graph
2943 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
2944 .unwrap();
2945 let broadcast_id = graph
2946 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
2947 .unwrap();
2948
2949 graph.connect(cam0_id, inf0_id, "i32").unwrap();
2951 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
2952 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
2953
2954 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
2955 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
2956
2957 assert_eq!(edge_cam0_to_broadcast, 0);
2958 assert_eq!(edge_cam0_to_inf0, 1);
2959
2960 let runtime = compute_runtime_plan(graph).unwrap();
2961 let broadcast_step = runtime
2962 .steps
2963 .iter()
2964 .find_map(|step| match step {
2965 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
2966 _ => None,
2967 })
2968 .unwrap();
2969
2970 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
2971 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
2972 }
2973}