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
1611#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1619pub enum CuStepPhase {
1620 #[default]
1622 Whole,
1623 AnytimeBase,
1626 AnytimeRefine,
1628}
1629
1630pub struct CuExecutionStep {
1632 pub node_id: NodeId,
1634 pub node: Node,
1636 pub task_type: CuTaskType,
1638 pub phase: CuStepPhase,
1641
1642 pub input_msg_indices_types: Vec<CuInputMsg>,
1645
1646 pub output_msg_pack: Option<CuOutputPack>,
1649}
1650
1651impl Debug for CuExecutionStep {
1652 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1653 f.write_str(format!(" CuExecutionStep: Node Id: {}\n", self.node_id).as_str())?;
1654 f.write_str(format!(" task_type: {:?}\n", self.node.get_type()).as_str())?;
1655 f.write_str(format!(" task: {:?}\n", self.task_type).as_str())?;
1656 f.write_str(format!(" phase: {:?}\n", self.phase).as_str())?;
1657 f.write_str(
1658 format!(
1659 " input_msg_types: {:?}\n",
1660 self.input_msg_indices_types
1661 )
1662 .as_str(),
1663 )?;
1664 f.write_str(format!(" output_msg_pack: {:?}\n", self.output_msg_pack).as_str())?;
1665 Ok(())
1666 }
1667}
1668
1669pub struct CuExecutionLoop {
1674 pub steps: Vec<CuExecutionUnit>,
1675 pub loop_count: Option<u32>,
1676}
1677
1678impl Debug for CuExecutionLoop {
1679 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
1680 f.write_str("CuExecutionLoop:\n")?;
1681 for step in &self.steps {
1682 match step {
1683 CuExecutionUnit::Step(step) => {
1684 step.fmt(f)?;
1685 }
1686 CuExecutionUnit::Loop(l) => {
1687 l.fmt(f)?;
1688 }
1689 }
1690 }
1691
1692 f.write_str(format!(" count: {:?}", self.loop_count).as_str())?;
1693 Ok(())
1694 }
1695}
1696
1697#[derive(Debug)]
1699pub enum CuExecutionUnit {
1700 Step(Box<CuExecutionStep>),
1701 Loop(CuExecutionLoop),
1702}
1703
1704fn find_output_pack_from_nodeid(
1705 node_id: NodeId,
1706 steps: &Vec<CuExecutionUnit>,
1707) -> Option<CuOutputPack> {
1708 for step in steps {
1709 match step {
1710 CuExecutionUnit::Loop(loop_unit) => {
1711 if let Some(output_pack) = find_output_pack_from_nodeid(node_id, &loop_unit.steps) {
1712 return Some(output_pack);
1713 }
1714 }
1715 CuExecutionUnit::Step(step) if step.node_id == node_id => {
1716 return step.output_msg_pack.clone();
1717 }
1718 _ => {}
1719 }
1720 }
1721 None
1722}
1723
1724pub fn find_task_type_for_id(graph: &CuGraph, node_id: NodeId) -> CuResult<CuTaskType> {
1725 let node = graph
1726 .get_node(node_id)
1727 .ok_or_else(|| CuError::from(format!("Node id {node_id} not found")))?;
1728
1729 if node.get_flavor() == crate::config::Flavor::Task {
1730 return resolve_task_kind_for_id(graph, node_id).map(Into::into);
1731 }
1732
1733 let has_inputs = !graph.get_dst_edges(node_id)?.is_empty();
1734 let has_outputs = !graph.get_src_edges(node_id)?.is_empty();
1735 Ok(match (has_inputs, has_outputs) {
1736 (false, true) => CuTaskType::Source,
1737 (true, false) => CuTaskType::Sink,
1738 _ => CuTaskType::Regular,
1739 })
1740}
1741
1742fn sort_inputs_by_connection_order(input_msg_indices_types: &mut [CuInputMsg]) {
1747 input_msg_indices_types.sort_by_key(|input| input.connection_order);
1748}
1749
1750fn plan_tasks_tree_branch(
1752 graph: &CuGraph,
1753 mut next_culist_output_index: u32,
1754 starting_point: NodeId,
1755 plan: &mut Vec<CuExecutionUnit>,
1756) -> CuResult<(u32, bool)> {
1757 #[cfg(all(feature = "std", feature = "macro_debug"))]
1758 eprintln!("-- starting branch from node {starting_point}");
1759
1760 let mut handled = false;
1761
1762 for id in graph.bfs_nodes(starting_point) {
1763 let node_ref = graph.get_node(id).unwrap();
1764 #[cfg(all(feature = "std", feature = "macro_debug"))]
1765 eprintln!(" Visiting node: {node_ref:?}");
1766
1767 let mut input_msg_indices_types: Vec<CuInputMsg> = Vec::new();
1768 let output_msg_pack: Option<CuOutputPack>;
1769 let task_type = find_task_type_for_id(graph, id)?;
1770
1771 match task_type {
1772 CuTaskType::Source => {
1773 #[cfg(all(feature = "std", feature = "macro_debug"))]
1774 eprintln!(" → Source node, assign output index {next_culist_output_index}");
1775 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1776 if msg_types.is_empty() {
1777 return Err(CuError::from(format!(
1778 "Source node '{}' has no declared outputs",
1779 node_ref.get_id()
1780 )));
1781 }
1782 output_msg_pack = Some(CuOutputPack {
1783 culist_index: next_culist_output_index,
1784 msg_types,
1785 });
1786 next_culist_output_index += 1;
1787 }
1788 CuTaskType::Sink => {
1789 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1790 edge_ids.sort();
1791 #[cfg(all(feature = "std", feature = "macro_debug"))]
1792 eprintln!(" → Sink with incoming edges: {edge_ids:?}");
1793 for edge_id in edge_ids {
1794 let edge = graph
1795 .edge(edge_id)
1796 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1797 let pid = graph
1798 .get_node_id_by_name(edge.src.as_str())
1799 .unwrap_or_else(|| {
1800 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1801 });
1802 let output_pack = find_output_pack_from_nodeid(pid, plan);
1803 if let Some(output_pack) = output_pack {
1804 #[cfg(all(feature = "std", feature = "macro_debug"))]
1805 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
1806 let msg_type = edge.msg.as_str();
1807 let src_port = output_pack
1808 .msg_types
1809 .iter()
1810 .position(|msg| msg == msg_type)
1811 .unwrap_or_else(|| {
1812 panic!(
1813 "Missing output port for message type '{msg_type}' on node {pid}"
1814 )
1815 });
1816 input_msg_indices_types.push(CuInputMsg {
1817 culist_index: output_pack.culist_index,
1818 msg_type: msg_type.to_string(),
1819 src_port,
1820 edge_id,
1821 connection_order: edge.order,
1822 });
1823 } else {
1824 #[cfg(all(feature = "std", feature = "macro_debug"))]
1825 eprintln!(" ✗ Input from {pid} not ready, returning");
1826 return Ok((next_culist_output_index, handled));
1827 }
1828 }
1829 output_msg_pack = Some(CuOutputPack {
1830 culist_index: next_culist_output_index,
1831 msg_types: Vec::from(["()".to_string()]),
1832 });
1833 next_culist_output_index += 1;
1834 }
1835 CuTaskType::Regular => {
1836 let mut edge_ids = graph.get_dst_edges(id).unwrap_or_default();
1837 edge_ids.sort();
1838 #[cfg(all(feature = "std", feature = "macro_debug"))]
1839 eprintln!(" → Regular task with incoming edges: {edge_ids:?}");
1840 for edge_id in edge_ids {
1841 let edge = graph
1842 .edge(edge_id)
1843 .unwrap_or_else(|| panic!("Missing edge {edge_id} for node {id}"));
1844 let pid = graph
1845 .get_node_id_by_name(edge.src.as_str())
1846 .unwrap_or_else(|| {
1847 panic!("Missing source node '{}' for edge {edge_id}", edge.src)
1848 });
1849 let output_pack = find_output_pack_from_nodeid(pid, plan);
1850 if let Some(output_pack) = output_pack {
1851 #[cfg(all(feature = "std", feature = "macro_debug"))]
1852 eprintln!(" ✓ Input from {pid} ready: {output_pack:?}");
1853 let msg_type = edge.msg.as_str();
1854 let src_port = output_pack
1855 .msg_types
1856 .iter()
1857 .position(|msg| msg == msg_type)
1858 .unwrap_or_else(|| {
1859 panic!(
1860 "Missing output port for message type '{msg_type}' on node {pid}"
1861 )
1862 });
1863 input_msg_indices_types.push(CuInputMsg {
1864 culist_index: output_pack.culist_index,
1865 msg_type: msg_type.to_string(),
1866 src_port,
1867 edge_id,
1868 connection_order: edge.order,
1869 });
1870 } else {
1871 #[cfg(all(feature = "std", feature = "macro_debug"))]
1872 eprintln!(" ✗ Input from {pid} not ready, returning");
1873 return Ok((next_culist_output_index, handled));
1874 }
1875 }
1876 let msg_types = graph.get_node_output_msg_types_by_id(id)?;
1877 if msg_types.is_empty() {
1878 return Err(CuError::from(format!(
1879 "Regular node '{}' has no declared outputs",
1880 node_ref.get_id()
1881 )));
1882 }
1883 output_msg_pack = Some(CuOutputPack {
1884 culist_index: next_culist_output_index,
1885 msg_types,
1886 });
1887 next_culist_output_index += 1;
1888 }
1889 }
1890
1891 sort_inputs_by_connection_order(&mut input_msg_indices_types);
1892
1893 if let Some(pos) = plan
1894 .iter()
1895 .position(|step| matches!(step, CuExecutionUnit::Step(s) if s.node_id == id))
1896 {
1897 #[cfg(all(feature = "std", feature = "macro_debug"))]
1898 eprintln!(" → Already in plan, modifying existing step");
1899 let mut step = plan.remove(pos);
1900 if let CuExecutionUnit::Step(ref mut s) = step {
1901 s.input_msg_indices_types = input_msg_indices_types;
1902 }
1903 plan.push(step);
1904 } else {
1905 #[cfg(all(feature = "std", feature = "macro_debug"))]
1906 eprintln!(" → New step added to plan");
1907 let step = CuExecutionStep {
1908 node_id: id,
1909 node: node_ref.clone(),
1910 task_type,
1911 phase: CuStepPhase::default(),
1912 input_msg_indices_types,
1913 output_msg_pack,
1914 };
1915 plan.push(CuExecutionUnit::Step(Box::new(step)));
1916 }
1917
1918 handled = true;
1919 }
1920
1921 #[cfg(all(feature = "std", feature = "macro_debug"))]
1922 eprintln!("-- finished branch from node {starting_point} with handled={handled}");
1923 Ok((next_culist_output_index, handled))
1924}
1925
1926pub fn compute_runtime_plan(graph: &CuGraph) -> CuResult<CuExecutionLoop> {
1929 #[cfg(all(feature = "std", feature = "macro_debug"))]
1930 eprintln!("[runtime plan]");
1931 let mut plan = Vec::new();
1932 let mut next_culist_output_index = 0u32;
1933
1934 let mut queue: VecDeque<NodeId> = VecDeque::new();
1935 for node_id in graph.node_ids() {
1936 if find_task_type_for_id(graph, node_id)? == CuTaskType::Source {
1937 queue.push_back(node_id);
1938 }
1939 }
1940
1941 #[cfg(all(feature = "std", feature = "macro_debug"))]
1942 eprintln!("Initial source nodes: {queue:?}");
1943
1944 while let Some(start_node) = queue.pop_front() {
1945 #[cfg(all(feature = "std", feature = "macro_debug"))]
1946 eprintln!("→ Starting BFS from source {start_node}");
1947 for node_id in graph.bfs_nodes(start_node) {
1948 let already_in_plan = plan
1949 .iter()
1950 .any(|unit| matches!(unit, CuExecutionUnit::Step(s) if s.node_id == node_id));
1951 if already_in_plan {
1952 #[cfg(all(feature = "std", feature = "macro_debug"))]
1953 eprintln!(" → Node {node_id} already planned, skipping");
1954 continue;
1955 }
1956
1957 #[cfg(all(feature = "std", feature = "macro_debug"))]
1958 eprintln!(" Planning from node {node_id}");
1959 let (new_index, handled) =
1960 plan_tasks_tree_branch(graph, next_culist_output_index, node_id, &mut plan)?;
1961 next_culist_output_index = new_index;
1962
1963 if !handled {
1964 #[cfg(all(feature = "std", feature = "macro_debug"))]
1965 eprintln!(" ✗ Node {node_id} was not handled, skipping enqueue of neighbors");
1966 continue;
1967 }
1968
1969 #[cfg(all(feature = "std", feature = "macro_debug"))]
1970 eprintln!(" ✓ Node {node_id} handled successfully, enqueueing neighbors");
1971 for neighbor in graph.get_neighbor_ids(node_id, CuDirection::Outgoing) {
1972 #[cfg(all(feature = "std", feature = "macro_debug"))]
1973 eprintln!(" → Enqueueing neighbor {neighbor}");
1974 queue.push_back(neighbor);
1975 }
1976 }
1977 }
1978
1979 let mut planned_nodes = BTreeSet::new();
1980 for unit in &plan {
1981 if let CuExecutionUnit::Step(step) = unit {
1982 planned_nodes.insert(step.node_id);
1983 }
1984 }
1985
1986 let mut missing = Vec::new();
1987 for node_id in graph.node_ids() {
1988 if !planned_nodes.contains(&node_id) {
1989 if let Some(node) = graph.get_node(node_id) {
1990 missing.push(node.get_id().to_string());
1991 } else {
1992 missing.push(format!("node_id_{node_id}"));
1993 }
1994 }
1995 }
1996
1997 if !missing.is_empty() {
1998 missing.sort();
1999 return Err(CuError::from(format!(
2000 "Execution plan could not include all nodes. Missing: {}. Check for loopback or missing source connections.",
2001 missing.join(", ")
2002 )));
2003 }
2004
2005 Ok(CuExecutionLoop {
2006 steps: plan,
2007 loop_count: None,
2008 })
2009}
2010
2011pub fn expand_anytime_steps(plan: &mut CuExecutionLoop) -> CuResult<()> {
2029 loop {
2030 let Some(base_pos) = plan.steps.iter().position(|unit| {
2033 matches!(
2034 unit,
2035 CuExecutionUnit::Step(step) if step.phase == CuStepPhase::Whole
2036 && step.node.anytime().is_some()
2037 && !step.node.is_background()
2038 )
2039 }) else {
2040 return Ok(());
2041 };
2042
2043 let CuExecutionUnit::Step(base_step) = &mut plan.steps[base_pos] else {
2044 unreachable!("position() only matches steps");
2045 };
2046 let anytime = base_step
2047 .node
2048 .anytime()
2049 .expect("position() only matches anytime nodes");
2050 let Some(max_refines) = anytime.max_refines else {
2053 return Err(CuError::from(format!(
2054 "Task '{}': a foreground anytime task needs anytime.max_refines to expand into a static plan.",
2055 base_step.node.get_id()
2056 )));
2057 };
2058 base_step.phase = CuStepPhase::AnytimeBase;
2059 let output_pack = base_step.output_msg_pack.clone().ok_or_else(|| {
2060 CuError::from(format!(
2061 "Task '{}': an anytime task needs an output to refine.",
2062 base_step.node.get_id()
2063 ))
2064 })?;
2065 let output_index = output_pack.culist_index;
2066 let node_id = base_step.node_id;
2067 let node = base_step.node.clone();
2068 let task_type = base_step.task_type;
2069
2070 let refine_step = || {
2071 CuExecutionUnit::Step(Box::new(CuExecutionStep {
2072 node_id,
2073 node: node.clone(),
2074 task_type,
2075 phase: CuStepPhase::AnytimeRefine,
2076 input_msg_indices_types: Vec::new(),
2077 output_msg_pack: Some(output_pack.clone()),
2078 }))
2079 };
2080
2081 let consumer_pos = plan.steps[base_pos + 1..]
2084 .iter()
2085 .position(|unit| {
2086 matches!(
2087 unit,
2088 CuExecutionUnit::Step(step) if step
2089 .input_msg_indices_types
2090 .iter()
2091 .any(|input| input.culist_index == output_index)
2092 )
2093 })
2094 .map(|offset| base_pos + 1 + offset)
2095 .unwrap_or(base_pos + 1);
2096
2097 let mut tail = plan.steps.split_off(base_pos + 1);
2098 let suffix = tail.split_off(consumer_pos - base_pos - 1);
2099 let gap = tail;
2100
2101 let mut remaining = max_refines.max(1);
2103 remaining -= 1;
2104 plan.steps.push(refine_step());
2105 for gap_unit in gap {
2106 plan.steps.push(gap_unit);
2107 if remaining > 0 {
2108 remaining -= 1;
2109 plan.steps.push(refine_step());
2110 }
2111 }
2112 for _ in 0..remaining {
2113 plan.steps.push(refine_step());
2114 }
2115 plan.steps.extend(suffix);
2116 }
2117}
2118
2119#[cfg(test)]
2121mod tests {
2122 use super::*;
2123 use crate::config::Node;
2124 use crate::context::CuContext;
2125 use crate::cutask::CuSinkTask;
2126 use crate::cutask::{CuSrcTask, Freezable};
2127 use crate::monitoring::NoMonitor;
2128 use crate::reflect::Reflect;
2129 use bincode::Encode;
2130 use cu29_traits::{ErasedCuStampedData, ErasedCuStampedDataSet, MatchingTasks};
2131 use serde_derive::{Deserialize, Serialize};
2132 #[cfg(feature = "std")]
2133 use std::sync::{Arc, Mutex};
2134
2135 #[derive(Reflect)]
2136 pub struct TestSource {}
2137
2138 impl Freezable for TestSource {}
2139
2140 impl CuSrcTask for TestSource {
2141 type Resources<'r> = ();
2142 type Output<'m> = ();
2143 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2144 where
2145 Self: Sized,
2146 {
2147 Ok(Self {})
2148 }
2149
2150 fn process(&mut self, _ctx: &CuContext, _empty_msg: &mut Self::Output<'_>) -> CuResult<()> {
2151 Ok(())
2152 }
2153 }
2154
2155 #[derive(Reflect)]
2156 pub struct TestSink {}
2157
2158 impl Freezable for TestSink {}
2159
2160 impl CuSinkTask for TestSink {
2161 type Resources<'r> = ();
2162 type Input<'m> = ();
2163
2164 fn new(_config: Option<&ComponentConfig>, _resources: Self::Resources<'_>) -> CuResult<Self>
2165 where
2166 Self: Sized,
2167 {
2168 Ok(Self {})
2169 }
2170
2171 fn process(&mut self, _ctx: &CuContext, _input: &Self::Input<'_>) -> CuResult<()> {
2172 Ok(())
2173 }
2174 }
2175
2176 type Tasks = (TestSource, TestSink);
2178 type TestRuntime = CuRuntime<Tasks, (), Msgs, NoMonitor, 2>;
2179 const TEST_NBCL: usize = 2;
2180
2181 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2182 struct Msgs(());
2183
2184 impl ErasedCuStampedDataSet for Msgs {
2185 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2186 Vec::new()
2187 }
2188 }
2189
2190 impl MatchingTasks for Msgs {
2191 fn get_all_task_ids() -> &'static [&'static str] {
2192 &[]
2193 }
2194 }
2195
2196 impl CuListZeroedInit for Msgs {
2197 fn init_zeroed(&mut self) {}
2198 }
2199
2200 #[derive(Debug, Encode, Decode, Serialize, Deserialize, Default)]
2201 struct IntMsgs(i32);
2202
2203 impl ErasedCuStampedDataSet for IntMsgs {
2204 fn cumsgs(&self) -> Vec<&dyn ErasedCuStampedData> {
2205 Vec::new()
2206 }
2207 }
2208
2209 impl MatchingTasks for IntMsgs {
2210 fn get_all_task_ids() -> &'static [&'static str] {
2211 &[]
2212 }
2213 }
2214
2215 impl CuListZeroedInit for IntMsgs {
2216 fn init_zeroed(&mut self) {}
2217 }
2218
2219 #[cfg(feature = "std")]
2220 fn tasks_instanciator(
2221 all_instances_configs: Vec<Option<&ComponentConfig>>,
2222 _resources: &mut ResourceManager,
2223 _thread_pools: &[Option<Arc<rayon::ThreadPool>>],
2224 ) -> CuResult<Tasks> {
2225 Ok((
2226 TestSource::new(all_instances_configs[0], ())?,
2227 TestSink::new(all_instances_configs[1], ())?,
2228 ))
2229 }
2230
2231 #[cfg(not(feature = "std"))]
2232 fn tasks_instanciator(
2233 all_instances_configs: Vec<Option<&ComponentConfig>>,
2234 _resources: &mut ResourceManager,
2235 ) -> CuResult<Tasks> {
2236 Ok((
2237 TestSource::new(all_instances_configs[0], ())?,
2238 TestSink::new(all_instances_configs[1], ())?,
2239 ))
2240 }
2241
2242 fn monitor_instanciator(
2243 _config: &CuConfig,
2244 metadata: CuMonitoringMetadata,
2245 runtime: CuMonitoringRuntime,
2246 ) -> NoMonitor {
2247 NoMonitor::new(metadata, runtime).expect("NoMonitor::new should never fail")
2248 }
2249
2250 fn bridges_instanciator(_config: &CuConfig, _resources: &mut ResourceManager) -> CuResult<()> {
2251 Ok(())
2252 }
2253
2254 fn resources_instanciator(_config: &CuConfig) -> CuResult<ResourceManager> {
2255 Ok(ResourceManager::new(&[]))
2256 }
2257
2258 #[derive(Debug)]
2259 struct FakeWriter {}
2260
2261 impl<E: Encode> WriteStream<E> for FakeWriter {
2262 fn log(&mut self, _obj: &E) -> CuResult<()> {
2263 Ok(())
2264 }
2265 }
2266
2267 #[cfg(not(feature = "async-cl-io"))]
2268 #[derive(Debug)]
2269 struct RecordingSyncWriter {
2270 ids: Arc<Mutex<Vec<u64>>>,
2271 last_log_bytes: usize,
2272 fail_on: Option<u64>,
2273 }
2274
2275 #[cfg(not(feature = "async-cl-io"))]
2276 impl WriteStream<CopperList<IntMsgs>> for RecordingSyncWriter {
2277 fn log(&mut self, culist: &CopperList<IntMsgs>) -> CuResult<()> {
2278 self.ids.lock().unwrap().push(culist.id);
2279 if self.fail_on == Some(culist.id) {
2280 return Err(CuError::from(format!(
2281 "logger failed for CopperList #{}",
2282 culist.id
2283 )));
2284 }
2285 Ok(())
2286 }
2287
2288 fn last_log_bytes(&self) -> Option<usize> {
2289 Some(self.last_log_bytes)
2290 }
2291 }
2292
2293 #[test]
2294 fn test_runtime_instantiation() {
2295 let mut config = CuConfig::default();
2296 let graph = config.get_graph_mut(None).unwrap();
2297 graph.add_node(Node::new("a", "TestSource")).unwrap();
2298 graph.add_node(Node::new("b", "TestSink")).unwrap();
2299 graph.connect(0, 1, "()").unwrap();
2300 let runtime: CuResult<TestRuntime> =
2301 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2302 RobotClock::default(),
2303 &config,
2304 crate::config::DEFAULT_MISSION_ID,
2305 CuRuntimeParts::new(
2306 tasks_instanciator,
2307 &[],
2308 &[],
2309 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2310 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2311 monitor_instanciator,
2312 bridges_instanciator,
2313 ),
2314 FakeWriter {},
2315 FakeWriter {},
2316 )
2317 .try_with_resources_instantiator(resources_instanciator)
2318 .and_then(|builder| builder.build());
2319 assert!(runtime.is_ok());
2320 }
2321
2322 #[test]
2323 fn test_rate_target_period_rejects_zero() {
2324 let err = rate_target_period(0).expect_err("zero rate target should fail");
2325 assert!(
2326 err.to_string()
2327 .contains("Runtime rate target cannot be zero"),
2328 "unexpected error: {err}"
2329 );
2330 }
2331
2332 #[test]
2333 fn test_loop_rate_limiter_advances_to_next_period_when_on_time() {
2334 let (clock, mock) = RobotClock::mock();
2335 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2336 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(10_000_000));
2337
2338 mock.set_value(10_000_000);
2339 limiter.mark_tick(&clock);
2340
2341 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(20_000_000));
2342 }
2343
2344 #[test]
2345 fn test_loop_rate_limiter_skips_missed_periods_without_resetting_phase() {
2346 let (clock, mock) = RobotClock::mock();
2347 let mut limiter = LoopRateLimiter::from_rate_target_hz(100, &clock).unwrap();
2348
2349 mock.set_value(35_000_000);
2350 limiter.mark_tick(&clock);
2351
2352 assert_eq!(limiter.next_deadline(), CuTime::from_nanos(40_000_000));
2353 }
2354
2355 #[cfg(all(feature = "std", feature = "high-precision-limiter"))]
2356 #[test]
2357 fn test_loop_rate_limiter_spin_window_is_fixed_scheduler_window() {
2358 let (clock, _) = RobotClock::mock();
2359 let limiter = LoopRateLimiter::from_rate_target_hz(1_000, &clock).unwrap();
2360 assert_eq!(limiter.spin_window(), CuDuration::from(200_000));
2361
2362 let fast = LoopRateLimiter::from_rate_target_hz(10_000, &clock).unwrap();
2363 assert_eq!(fast.spin_window(), CuDuration::from(200_000));
2364 }
2365
2366 #[cfg(not(feature = "async-cl-io"))]
2367 #[test]
2368 fn test_copperlists_manager_lifecycle() {
2369 let mut config = CuConfig::default();
2370 let graph = config.get_graph_mut(None).unwrap();
2371 graph.add_node(Node::new("a", "TestSource")).unwrap();
2372 graph.add_node(Node::new("b", "TestSink")).unwrap();
2373 graph.connect(0, 1, "()").unwrap();
2374
2375 let mut runtime: TestRuntime =
2376 CuRuntimeBuilder::<Tasks, (), Msgs, NoMonitor, TEST_NBCL, _, _, _, _, _>::new(
2377 RobotClock::default(),
2378 &config,
2379 crate::config::DEFAULT_MISSION_ID,
2380 CuRuntimeParts::new(
2381 tasks_instanciator,
2382 &[],
2383 &[],
2384 #[cfg(all(feature = "std", feature = "parallel-rt"))]
2385 &crate::parallel_rt::DISABLED_PARALLEL_RT_METADATA,
2386 monitor_instanciator,
2387 bridges_instanciator,
2388 ),
2389 FakeWriter {},
2390 FakeWriter {},
2391 )
2392 .try_with_resources_instantiator(resources_instanciator)
2393 .and_then(|builder| builder.build())
2394 .unwrap();
2395
2396 {
2398 let copperlists = &mut runtime.copperlists_manager;
2399 let culist0 = copperlists
2400 .create()
2401 .expect("Ran out of space for copper lists");
2402 let id = culist0.id;
2403 assert_eq!(id, 0);
2404 culist0.change_state(CopperListState::Processing);
2405 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2406 }
2407
2408 {
2409 let copperlists = &mut runtime.copperlists_manager;
2410 let culist1 = copperlists
2411 .create()
2412 .expect("Ran out of space for copper lists");
2413 let id = culist1.id;
2414 assert_eq!(id, 1);
2415 culist1.change_state(CopperListState::Processing);
2416 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2417 }
2418
2419 {
2420 let copperlists = &mut runtime.copperlists_manager;
2421 let culist2 = copperlists.create();
2422 assert!(culist2.is_err());
2423 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2424 let _ = copperlists.end_of_processing(1);
2426 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2427 }
2428
2429 {
2431 let copperlists = &mut runtime.copperlists_manager;
2432 let culist2 = copperlists
2433 .create()
2434 .expect("Ran out of space for copper lists");
2435 let id = culist2.id;
2436 assert_eq!(id, 2);
2437 culist2.change_state(CopperListState::Processing);
2438 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2439 let _ = copperlists.end_of_processing(0);
2441 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2443
2444 let _ = copperlists.end_of_processing(2);
2446 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2449 }
2450 }
2451
2452 #[cfg(not(feature = "async-cl-io"))]
2453 #[test]
2454 fn test_sync_copperlists_accessors_passthrough_to_inner_manager() {
2455 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2456
2457 assert_eq!(copperlists.next_cl_id(), 0);
2458 assert_eq!(copperlists.last_cl_id(), 0);
2459 assert!(copperlists.peek().is_none());
2460
2461 {
2462 let culist = copperlists.create().unwrap();
2463 culist.msgs.0 = 11;
2464 assert_eq!(culist.id, 0);
2465 assert_eq!(culist.get_state(), CopperListState::Initialized);
2466 }
2467
2468 assert_eq!(copperlists.next_cl_id(), 1);
2469 assert_eq!(copperlists.last_cl_id(), 0);
2470 let peeked = copperlists.peek().unwrap();
2471 assert_eq!(peeked.id, 0);
2472 assert_eq!(peeked.msgs.0, 11);
2473 assert_eq!(peeked.get_state(), CopperListState::Initialized);
2474 }
2475
2476 #[cfg(not(feature = "async-cl-io"))]
2477 #[test]
2478 fn test_sync_reclaimed_slot_reuse_reinitializes_state_but_preserves_payload_storage() {
2479 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2480
2481 {
2482 let culist = copperlists.create().unwrap();
2483 culist.msgs.0 = 41;
2484 culist.change_state(CopperListState::Processing);
2485 assert_eq!(culist.id, 0);
2486 }
2487
2488 copperlists.end_of_processing(0).unwrap();
2489 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2490
2491 let reused = copperlists.create().unwrap();
2492 assert_eq!(reused.id, 1);
2493 assert_eq!(reused.get_state(), CopperListState::Initialized);
2494 assert_eq!(reused.msgs.0, 41);
2495 }
2496
2497 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2498 #[test]
2499 #[should_panic(expected = "sync end_of_processing expected exactly one active CopperList #99")]
2500 fn test_sync_end_of_processing_unknown_id_panics_in_debug() {
2501 let mut copperlists = SyncCopperListsManager::<IntMsgs, 2>::new(None).unwrap();
2502
2503 {
2504 let culist = copperlists.create().unwrap();
2505 culist.msgs.0 = 10;
2506 culist.change_state(CopperListState::Processing);
2507 }
2508 {
2509 let culist = copperlists.create().unwrap();
2510 culist.msgs.0 = 20;
2511 culist.change_state(CopperListState::Processing);
2512 }
2513
2514 let _ = copperlists.end_of_processing(99);
2515 }
2516
2517 #[cfg(all(not(feature = "async-cl-io"), debug_assertions))]
2518 #[test]
2519 #[should_panic(expected = "sync end_of_processing expected CopperList #0 to be Processing")]
2520 fn test_sync_end_of_processing_wrong_state_panics_in_debug() {
2521 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2522
2523 {
2524 let culist = copperlists.create().unwrap();
2525 culist.msgs.0 = 10;
2526 assert_eq!(culist.get_state(), CopperListState::Initialized);
2527 }
2528
2529 let _ = copperlists.end_of_processing(0);
2530 }
2531
2532 #[cfg(not(feature = "async-cl-io"))]
2533 #[test]
2534 fn test_sync_end_of_processing_serializes_done_suffix_from_newest_to_oldest() {
2535 let ids = Arc::new(Mutex::new(Vec::new()));
2536 let mut copperlists =
2537 SyncCopperListsManager::<IntMsgs, 2>::new(Some(Box::new(RecordingSyncWriter {
2538 ids: ids.clone(),
2539 last_log_bytes: 17,
2540 fail_on: None,
2541 })))
2542 .unwrap();
2543
2544 {
2545 let culist = copperlists.create().unwrap();
2546 culist.msgs.0 = 10;
2547 culist.change_state(CopperListState::Processing);
2548 }
2549 {
2550 let culist = copperlists.create().unwrap();
2551 culist.msgs.0 = 20;
2552 culist.change_state(CopperListState::Processing);
2553 }
2554
2555 copperlists.end_of_processing(0).unwrap();
2556 assert!(ids.lock().unwrap().is_empty());
2557 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2558
2559 copperlists.end_of_processing(1).unwrap();
2560
2561 assert_eq!(*ids.lock().unwrap(), vec![1, 0]);
2562 assert_eq!(copperlists.available_copper_lists().unwrap(), 2);
2563 }
2564
2565 #[cfg(not(feature = "async-cl-io"))]
2566 #[test]
2567 fn test_sync_end_of_processing_updates_logger_counters_on_success() {
2568 let ids = Arc::new(Mutex::new(Vec::new()));
2569 let mut copperlists =
2570 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2571 ids: ids.clone(),
2572 last_log_bytes: 17,
2573 fail_on: None,
2574 })))
2575 .unwrap();
2576 let io_cache = crate::monitoring::CuMsgIoCache::<1>::default();
2577
2578 {
2579 let culist = copperlists.create().unwrap();
2580 culist.msgs.0 = 10;
2581 culist.change_state(CopperListState::Processing);
2582 }
2583
2584 {
2585 let capture = crate::monitoring::start_copperlist_io_capture(&io_cache);
2586 capture.select_slot(0);
2587 crate::monitoring::record_payload_handle_bytes(32);
2588 }
2589
2590 copperlists.end_of_processing(0).unwrap();
2591
2592 assert_eq!(*ids.lock().unwrap(), vec![0]);
2593 assert_eq!(copperlists.last_encoded_bytes, 17);
2594 assert_eq!(copperlists.last_handle_bytes, 32);
2595 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2596 }
2597
2598 #[cfg(not(feature = "async-cl-io"))]
2599 #[test]
2600 fn test_sync_end_of_processing_preserves_slot_on_logger_error() {
2601 let ids = Arc::new(Mutex::new(Vec::new()));
2602 let mut copperlists =
2603 SyncCopperListsManager::<IntMsgs, 1>::new(Some(Box::new(RecordingSyncWriter {
2604 ids: ids.clone(),
2605 last_log_bytes: 17,
2606 fail_on: Some(0),
2607 })))
2608 .unwrap();
2609
2610 {
2611 let culist = copperlists.create().unwrap();
2612 culist.change_state(CopperListState::Processing);
2613 }
2614
2615 let err = copperlists.end_of_processing(0).unwrap_err();
2616
2617 assert!(
2618 err.to_string().contains("logger failed for CopperList #0"),
2619 "unexpected error: {err}"
2620 );
2621 assert_eq!(*ids.lock().unwrap(), vec![0]);
2622 assert_eq!(copperlists.available_copper_lists().unwrap(), 0);
2623 assert_eq!(copperlists.last_encoded_bytes, 0);
2624 assert_eq!(copperlists.last_handle_bytes, 0);
2625
2626 let peeked = copperlists.peek().unwrap();
2627 assert_eq!(peeked.id, 0);
2628 assert_eq!(peeked.get_state(), CopperListState::BeingSerialized);
2629 }
2630
2631 #[cfg(all(not(feature = "async-cl-io"), feature = "std", debug_assertions))]
2632 #[test]
2633 #[should_panic(
2634 expected = "sync boxed end_of_processing expected CopperList #7 to be Processing"
2635 )]
2636 fn test_sync_end_of_processing_boxed_wrong_state_panics_in_debug() {
2637 let mut copperlists = SyncCopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2638 let culist = Box::new(CopperList::new(7, IntMsgs::default()));
2639
2640 let _ = copperlists.end_of_processing_boxed(culist);
2641 }
2642
2643 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2644 #[derive(Debug, Default)]
2645 struct RecordingWriter {
2646 ids: Arc<Mutex<Vec<u64>>>,
2647 }
2648
2649 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2650 impl WriteStream<CopperList<Msgs>> for RecordingWriter {
2651 fn log(&mut self, culist: &CopperList<Msgs>) -> CuResult<()> {
2652 self.ids.lock().unwrap().push(culist.id);
2653 std::thread::sleep(std::time::Duration::from_millis(2));
2654 Ok(())
2655 }
2656 }
2657
2658 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2659 #[test]
2660 fn test_async_copperlists_manager_flushes_in_order() {
2661 let ids = Arc::new(Mutex::new(Vec::new()));
2662 let mut copperlists = CopperListsManager::<Msgs, 4>::new(Some(Box::new(RecordingWriter {
2663 ids: ids.clone(),
2664 })))
2665 .unwrap();
2666
2667 for expected_id in 0..4 {
2668 let culist = copperlists.create().unwrap();
2669 assert_eq!(culist.id, expected_id);
2670 culist.change_state(CopperListState::Processing);
2671 copperlists.end_of_processing(expected_id).unwrap();
2672 }
2673
2674 copperlists.finish_pending().unwrap();
2675 assert_eq!(copperlists.available_copper_lists().unwrap(), 4);
2676 assert_eq!(*ids.lock().unwrap(), vec![0, 1, 2, 3]);
2677 }
2678
2679 #[cfg(all(feature = "std", feature = "async-cl-io"))]
2680 #[test]
2681 fn test_async_create_reinitializes_reclaimed_slot_state_but_preserves_payload_storage() {
2682 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2683
2684 {
2685 let culist = copperlists.create().unwrap();
2686 assert_eq!(culist.id, 0);
2687 assert_eq!(culist.get_state(), CopperListState::Initialized);
2688 culist.msgs.0 = 41;
2689 culist.change_state(CopperListState::Processing);
2690 }
2691
2692 copperlists.end_of_processing(0).unwrap();
2693 assert_eq!(copperlists.available_copper_lists().unwrap(), 1);
2694
2695 let reused = copperlists.create().unwrap();
2696 assert_eq!(reused.id, 1);
2697 assert_eq!(reused.get_state(), CopperListState::Initialized);
2698 assert_eq!(reused.msgs.0, 41);
2699 }
2700
2701 #[cfg(all(feature = "std", feature = "async-cl-io", debug_assertions))]
2702 #[test]
2703 #[should_panic(expected = "async end_of_processing expected CopperList #0 to be Processing")]
2704 fn test_async_end_of_processing_wrong_state_panics_in_debug() {
2705 let mut copperlists = CopperListsManager::<IntMsgs, 1>::new(None).unwrap();
2706
2707 let culist = copperlists.create().unwrap();
2708 assert_eq!(culist.id, 0);
2709 assert_eq!(culist.get_state(), CopperListState::Initialized);
2710
2711 let _ = copperlists.end_of_processing(0);
2712 }
2713
2714 #[test]
2715 fn test_runtime_task_input_order() {
2716 let mut config = CuConfig::default();
2717 let graph = config.get_graph_mut(None).unwrap();
2718 let src1_id = graph.add_node(Node::new("a", "Source1")).unwrap();
2719 let src2_id = graph.add_node(Node::new("b", "Source2")).unwrap();
2720 let sink_id = graph.add_node(Node::new("c", "Sink")).unwrap();
2721
2722 assert_eq!(src1_id, 0);
2723 assert_eq!(src2_id, 1);
2724
2725 let src1_type = "src1_type";
2727 let src2_type = "src2_type";
2728 graph.connect(src2_id, sink_id, src2_type).unwrap();
2729 graph.connect(src1_id, sink_id, src1_type).unwrap();
2730
2731 let src1_edge_id = *graph.get_src_edges(src1_id).unwrap().first().unwrap();
2732 let src2_edge_id = *graph.get_src_edges(src2_id).unwrap().first().unwrap();
2733 assert_eq!(src1_edge_id, 1);
2736 assert_eq!(src2_edge_id, 0);
2737
2738 let runtime = compute_runtime_plan(graph).unwrap();
2739 let sink_step = runtime
2740 .steps
2741 .iter()
2742 .find_map(|step| match step {
2743 CuExecutionUnit::Step(step) if step.node_id == sink_id => Some(step),
2744 _ => None,
2745 })
2746 .unwrap();
2747
2748 assert_eq!(sink_step.input_msg_indices_types[0].msg_type, src2_type);
2751 assert_eq!(sink_step.input_msg_indices_types[1].msg_type, src1_type);
2752 }
2753
2754 #[test]
2755 fn test_runtime_output_ports_unique_ordered() {
2756 let mut config = CuConfig::default();
2757 let graph = config.get_graph_mut(None).unwrap();
2758 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2759 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
2760 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
2761 let dst_a2_id = graph.add_node(Node::new("dst_a2", "SinkA2")).unwrap();
2762 let dst_c_id = graph.add_node(Node::new("dst_c", "SinkC")).unwrap();
2763
2764 graph.connect(src_id, dst_a_id, "msg::A").unwrap();
2765 graph.connect(src_id, dst_b_id, "msg::B").unwrap();
2766 graph.connect(src_id, dst_a2_id, "msg::A").unwrap();
2767 graph.connect(src_id, dst_c_id, "msg::C").unwrap();
2768
2769 let runtime = compute_runtime_plan(graph).unwrap();
2770 let src_step = runtime
2771 .steps
2772 .iter()
2773 .find_map(|step| match step {
2774 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2775 _ => None,
2776 })
2777 .unwrap();
2778
2779 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2780 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B", "msg::C"]);
2781
2782 let dst_a_step = runtime
2783 .steps
2784 .iter()
2785 .find_map(|step| match step {
2786 CuExecutionUnit::Step(step) if step.node_id == dst_a_id => Some(step),
2787 _ => None,
2788 })
2789 .unwrap();
2790 let dst_b_step = runtime
2791 .steps
2792 .iter()
2793 .find_map(|step| match step {
2794 CuExecutionUnit::Step(step) if step.node_id == dst_b_id => Some(step),
2795 _ => None,
2796 })
2797 .unwrap();
2798 let dst_a2_step = runtime
2799 .steps
2800 .iter()
2801 .find_map(|step| match step {
2802 CuExecutionUnit::Step(step) if step.node_id == dst_a2_id => Some(step),
2803 _ => None,
2804 })
2805 .unwrap();
2806 let dst_c_step = runtime
2807 .steps
2808 .iter()
2809 .find_map(|step| match step {
2810 CuExecutionUnit::Step(step) if step.node_id == dst_c_id => Some(step),
2811 _ => None,
2812 })
2813 .unwrap();
2814
2815 assert_eq!(dst_a_step.input_msg_indices_types[0].src_port, 0);
2816 assert_eq!(dst_b_step.input_msg_indices_types[0].src_port, 1);
2817 assert_eq!(dst_a2_step.input_msg_indices_types[0].src_port, 0);
2818 assert_eq!(dst_c_step.input_msg_indices_types[0].src_port, 2);
2819 }
2820
2821 #[test]
2822 fn test_runtime_output_ports_fanout_single() {
2823 let mut config = CuConfig::default();
2824 let graph = config.get_graph_mut(None).unwrap();
2825 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2826 let dst_a_id = graph.add_node(Node::new("dst_a", "SinkA")).unwrap();
2827 let dst_b_id = graph.add_node(Node::new("dst_b", "SinkB")).unwrap();
2828
2829 graph.connect(src_id, dst_a_id, "i32").unwrap();
2830 graph.connect(src_id, dst_b_id, "i32").unwrap();
2831
2832 let runtime = compute_runtime_plan(graph).unwrap();
2833 let src_step = runtime
2834 .steps
2835 .iter()
2836 .find_map(|step| match step {
2837 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2838 _ => None,
2839 })
2840 .unwrap();
2841
2842 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2843 assert_eq!(output_pack.msg_types, vec!["i32"]);
2844 }
2845
2846 #[test]
2847 fn test_runtime_output_ports_include_nc_outputs() {
2848 let mut config = CuConfig::default();
2849 let graph = config.get_graph_mut(None).unwrap();
2850 let src_id = graph.add_node(Node::new("src", "Source")).unwrap();
2851 let dst_id = graph.add_node(Node::new("dst", "Sink")).unwrap();
2852 graph.connect(src_id, dst_id, "msg::A").unwrap();
2853 graph
2854 .get_node_mut(src_id)
2855 .expect("missing source node")
2856 .add_nc_output("msg::B", usize::MAX);
2857
2858 let runtime = compute_runtime_plan(graph).unwrap();
2859 let src_step = runtime
2860 .steps
2861 .iter()
2862 .find_map(|step| match step {
2863 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2864 _ => None,
2865 })
2866 .unwrap();
2867 let dst_step = runtime
2868 .steps
2869 .iter()
2870 .find_map(|step| match step {
2871 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2872 _ => None,
2873 })
2874 .unwrap();
2875
2876 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2877 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2878 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 0);
2879 }
2880
2881 #[test]
2882 fn test_runtime_plan_infers_regular_task_when_outputs_are_nc_only() {
2883 let txt = r#"(
2884 tasks: [
2885 (id: "src", type: "a"),
2886 (id: "regular", type: "b"),
2887 ],
2888 cnx: [
2889 (src: "src", dst: "regular", msg: "msg::A"),
2890 (src: "regular", dst: "__nc__", msg: "msg::B"),
2891 ]
2892 )"#;
2893 let config = CuConfig::deserialize_ron(txt).unwrap();
2894 let graph = config.get_graph(None).unwrap();
2895 let regular_id = graph.get_node_id_by_name("regular").unwrap();
2896
2897 let runtime = compute_runtime_plan(graph).unwrap();
2898 let regular_step = runtime
2899 .steps
2900 .iter()
2901 .find_map(|step| match step {
2902 CuExecutionUnit::Step(step) if step.node_id == regular_id => Some(step),
2903 _ => None,
2904 })
2905 .unwrap();
2906
2907 assert_eq!(regular_step.task_type, CuTaskType::Regular);
2908 assert_eq!(
2909 regular_step.output_msg_pack.as_ref().unwrap().msg_types,
2910 vec!["msg::B"]
2911 );
2912 }
2913
2914 #[test]
2915 fn test_runtime_output_ports_respect_connection_order_with_nc() {
2916 let txt = r#"(
2917 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2918 cnx: [
2919 (src: "src", dst: "__nc__", msg: "msg::A"),
2920 (src: "src", dst: "sink", msg: "msg::B"),
2921 ]
2922 )"#;
2923 let config = CuConfig::deserialize_ron(txt).unwrap();
2924 let graph = config.get_graph(None).unwrap();
2925 let src_id = graph.get_node_id_by_name("src").unwrap();
2926 let dst_id = graph.get_node_id_by_name("sink").unwrap();
2927
2928 let runtime = compute_runtime_plan(graph).unwrap();
2929 let src_step = runtime
2930 .steps
2931 .iter()
2932 .find_map(|step| match step {
2933 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2934 _ => None,
2935 })
2936 .unwrap();
2937 let dst_step = runtime
2938 .steps
2939 .iter()
2940 .find_map(|step| match step {
2941 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2942 _ => None,
2943 })
2944 .unwrap();
2945
2946 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2947 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2948 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
2949 }
2950
2951 #[cfg(feature = "std")]
2952 #[test]
2953 fn test_runtime_output_ports_respect_connection_order_with_nc_from_file() {
2954 let txt = r#"(
2955 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2956 cnx: [
2957 (src: "src", dst: "__nc__", msg: "msg::A"),
2958 (src: "src", dst: "sink", msg: "msg::B"),
2959 ]
2960 )"#;
2961 let tmp = tempfile::NamedTempFile::new().unwrap();
2962 std::fs::write(tmp.path(), txt).unwrap();
2963 let config = crate::config::read_configuration(tmp.path().to_str().unwrap()).unwrap();
2964 let graph = config.get_graph(None).unwrap();
2965 let src_id = graph.get_node_id_by_name("src").unwrap();
2966 let dst_id = graph.get_node_id_by_name("sink").unwrap();
2967
2968 let runtime = compute_runtime_plan(graph).unwrap();
2969 let src_step = runtime
2970 .steps
2971 .iter()
2972 .find_map(|step| match step {
2973 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
2974 _ => None,
2975 })
2976 .unwrap();
2977 let dst_step = runtime
2978 .steps
2979 .iter()
2980 .find_map(|step| match step {
2981 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
2982 _ => None,
2983 })
2984 .unwrap();
2985
2986 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
2987 assert_eq!(output_pack.msg_types, vec!["msg::A", "msg::B"]);
2988 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
2989 }
2990
2991 #[test]
2992 fn test_runtime_output_ports_respect_connection_order_with_nc_primitives() {
2993 let txt = r#"(
2994 tasks: [(id: "src", type: "a"), (id: "sink", type: "b")],
2995 cnx: [
2996 (src: "src", dst: "__nc__", msg: "i32"),
2997 (src: "src", dst: "sink", msg: "bool"),
2998 ]
2999 )"#;
3000 let config = CuConfig::deserialize_ron(txt).unwrap();
3001 let graph = config.get_graph(None).unwrap();
3002 let src_id = graph.get_node_id_by_name("src").unwrap();
3003 let dst_id = graph.get_node_id_by_name("sink").unwrap();
3004
3005 let runtime = compute_runtime_plan(graph).unwrap();
3006 let src_step = runtime
3007 .steps
3008 .iter()
3009 .find_map(|step| match step {
3010 CuExecutionUnit::Step(step) if step.node_id == src_id => Some(step),
3011 _ => None,
3012 })
3013 .unwrap();
3014 let dst_step = runtime
3015 .steps
3016 .iter()
3017 .find_map(|step| match step {
3018 CuExecutionUnit::Step(step) if step.node_id == dst_id => Some(step),
3019 _ => None,
3020 })
3021 .unwrap();
3022
3023 let output_pack = src_step.output_msg_pack.as_ref().unwrap();
3024 assert_eq!(output_pack.msg_types, vec!["i32", "bool"]);
3025 assert_eq!(dst_step.input_msg_indices_types[0].src_port, 1);
3026 }
3027
3028 #[test]
3029 fn test_runtime_plan_diamond_case1() {
3030 let mut config = CuConfig::default();
3032 let graph = config.get_graph_mut(None).unwrap();
3033 let cam0_id = graph
3034 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
3035 .unwrap();
3036 let inf0_id = graph
3037 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
3038 .unwrap();
3039 let broadcast_id = graph
3040 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
3041 .unwrap();
3042
3043 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
3045 graph.connect(cam0_id, inf0_id, "i32").unwrap();
3046 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
3047
3048 let edge_cam0_to_broadcast = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
3049 let edge_cam0_to_inf0 = graph.get_src_edges(cam0_id).unwrap()[1];
3050
3051 assert_eq!(edge_cam0_to_inf0, 0);
3052 assert_eq!(edge_cam0_to_broadcast, 1);
3053
3054 let runtime = compute_runtime_plan(graph).unwrap();
3055 let broadcast_step = runtime
3056 .steps
3057 .iter()
3058 .find_map(|step| match step {
3059 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
3060 _ => None,
3061 })
3062 .unwrap();
3063
3064 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
3065 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
3066 }
3067
3068 #[test]
3069 fn test_runtime_plan_diamond_case2() {
3070 let mut config = CuConfig::default();
3072 let graph = config.get_graph_mut(None).unwrap();
3073 let cam0_id = graph
3074 .add_node(Node::new("cam0", "tasks::IntegerSrcTask"))
3075 .unwrap();
3076 let inf0_id = graph
3077 .add_node(Node::new("inf0", "tasks::Integer2FloatTask"))
3078 .unwrap();
3079 let broadcast_id = graph
3080 .add_node(Node::new("broadcast", "tasks::MergingSinkTask"))
3081 .unwrap();
3082
3083 graph.connect(cam0_id, inf0_id, "i32").unwrap();
3085 graph.connect(cam0_id, broadcast_id, "i32").unwrap();
3086 graph.connect(inf0_id, broadcast_id, "f32").unwrap();
3087
3088 let edge_cam0_to_inf0 = *graph.get_src_edges(cam0_id).unwrap().first().unwrap();
3089 let edge_cam0_to_broadcast = graph.get_src_edges(cam0_id).unwrap()[1];
3090
3091 assert_eq!(edge_cam0_to_broadcast, 0);
3092 assert_eq!(edge_cam0_to_inf0, 1);
3093
3094 let runtime = compute_runtime_plan(graph).unwrap();
3095 let broadcast_step = runtime
3096 .steps
3097 .iter()
3098 .find_map(|step| match step {
3099 CuExecutionUnit::Step(step) if step.node_id == broadcast_id => Some(step),
3100 _ => None,
3101 })
3102 .unwrap();
3103
3104 assert_eq!(broadcast_step.input_msg_indices_types[0].msg_type, "i32");
3105 assert_eq!(broadcast_step.input_msg_indices_types[1].msg_type, "f32");
3106 }
3107
3108 use crate::config::AnytimeConfig;
3111
3112 fn anytime_node(id: &str, max_refines: Option<u32>) -> Node {
3113 let mut node = Node::new(id, "tasks::AnytimeTask");
3114 node.set_anytime(Some(AnytimeConfig {
3115 max_refines,
3116 ..Default::default()
3117 }));
3118 node
3119 }
3120
3121 fn plan_shape(plan: &CuExecutionLoop) -> Vec<(NodeId, CuStepPhase)> {
3123 plan.steps
3124 .iter()
3125 .map(|unit| match unit {
3126 CuExecutionUnit::Step(step) => (step.node_id, step.phase),
3127 CuExecutionUnit::Loop(_) => panic!("no loops expected"),
3128 })
3129 .collect()
3130 }
3131
3132 fn manual_step(node: Node, node_id: NodeId, inputs: &[u32], output: u32) -> CuExecutionUnit {
3135 CuExecutionUnit::Step(Box::new(CuExecutionStep {
3136 node_id,
3137 node,
3138 task_type: CuTaskType::Regular,
3139 phase: CuStepPhase::default(),
3140 input_msg_indices_types: inputs
3141 .iter()
3142 .map(|&culist_index| CuInputMsg {
3143 culist_index,
3144 msg_type: "msg::A".to_string(),
3145 src_port: 0,
3146 edge_id: 0,
3147 connection_order: 0,
3148 })
3149 .collect(),
3150 output_msg_pack: Some(CuOutputPack {
3151 culist_index: output,
3152 msg_types: vec!["msg::A".to_string()],
3153 }),
3154 }))
3155 }
3156
3157 #[test]
3158 fn test_anytime_expansion_contiguous_without_gap() {
3159 let mut config = CuConfig::default();
3162 let graph = config.get_graph_mut(None).unwrap();
3163 let src_id = graph.add_node(Node::new("src", "tasks::Src")).unwrap();
3164 let any_id = graph.add_node(anytime_node("any", Some(3))).unwrap();
3165 let sink_id = graph.add_node(Node::new("sink", "tasks::Sink")).unwrap();
3166 graph.connect(src_id, any_id, "msg::A").unwrap();
3167 graph.connect(any_id, sink_id, "msg::B").unwrap();
3168
3169 let mut plan = compute_runtime_plan(graph).unwrap();
3170 expand_anytime_steps(&mut plan).unwrap();
3171
3172 assert_eq!(
3173 plan_shape(&plan),
3174 vec![
3175 (src_id, CuStepPhase::Whole),
3176 (any_id, CuStepPhase::AnytimeBase),
3177 (any_id, CuStepPhase::AnytimeRefine),
3178 (any_id, CuStepPhase::AnytimeRefine),
3179 (any_id, CuStepPhase::AnytimeRefine),
3180 (sink_id, CuStepPhase::Whole),
3181 ]
3182 );
3183
3184 let (base_pack, refine_steps): (Option<CuOutputPack>, Vec<&CuExecutionStep>) = {
3186 let mut base_pack = None;
3187 let mut refines = Vec::new();
3188 for unit in &plan.steps {
3189 if let CuExecutionUnit::Step(step) = unit {
3190 match step.phase {
3191 CuStepPhase::AnytimeBase => base_pack = step.output_msg_pack.clone(),
3192 CuStepPhase::AnytimeRefine => refines.push(step.as_ref()),
3193 CuStepPhase::Whole => {}
3194 }
3195 }
3196 }
3197 (base_pack, refines)
3198 };
3199 let base_pack = base_pack.unwrap();
3200 for refine in refine_steps {
3201 assert!(refine.input_msg_indices_types.is_empty());
3202 let pack = refine.output_msg_pack.as_ref().unwrap();
3203 assert_eq!(pack.culist_index, base_pack.culist_index);
3204 }
3205 }
3206
3207 #[test]
3208 fn test_anytime_expansion_interleaves_with_gap_steps() {
3209 let mut plan = CuExecutionLoop {
3212 steps: vec![
3213 manual_step(anytime_node("any", Some(4)), 0, &[], 0),
3214 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
3215 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
3216 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
3217 ],
3218 loop_count: None,
3219 };
3220 expand_anytime_steps(&mut plan).unwrap();
3221 assert_eq!(
3222 plan_shape(&plan),
3223 vec![
3224 (0, CuStepPhase::AnytimeBase),
3225 (0, CuStepPhase::AnytimeRefine),
3226 (1, CuStepPhase::Whole),
3227 (0, CuStepPhase::AnytimeRefine),
3228 (2, CuStepPhase::Whole),
3229 (0, CuStepPhase::AnytimeRefine),
3230 (0, CuStepPhase::AnytimeRefine),
3231 (3, CuStepPhase::Whole),
3232 ]
3233 );
3234 }
3235
3236 #[test]
3237 fn test_anytime_expansion_fewer_refines_than_gaps() {
3238 let mut plan = CuExecutionLoop {
3240 steps: vec![
3241 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
3242 manual_step(Node::new("gap_a", "t"), 1, &[], 1),
3243 manual_step(Node::new("gap_b", "t"), 2, &[], 2),
3244 manual_step(Node::new("consumer", "t"), 3, &[0], 3),
3245 ],
3246 loop_count: None,
3247 };
3248 expand_anytime_steps(&mut plan).unwrap();
3249 assert_eq!(
3250 plan_shape(&plan),
3251 vec![
3252 (0, CuStepPhase::AnytimeBase),
3253 (0, CuStepPhase::AnytimeRefine),
3254 (1, CuStepPhase::Whole),
3255 (0, CuStepPhase::AnytimeRefine),
3256 (2, CuStepPhase::Whole),
3257 (3, CuStepPhase::Whole),
3258 ]
3259 );
3260 }
3261
3262 #[test]
3263 fn test_anytime_expansion_without_consumer() {
3264 let mut plan = CuExecutionLoop {
3266 steps: vec![
3267 manual_step(anytime_node("any", Some(2)), 0, &[], 0),
3268 manual_step(Node::new("other", "t"), 1, &[], 1),
3269 ],
3270 loop_count: None,
3271 };
3272 expand_anytime_steps(&mut plan).unwrap();
3273 assert_eq!(
3274 plan_shape(&plan),
3275 vec![
3276 (0, CuStepPhase::AnytimeBase),
3277 (0, CuStepPhase::AnytimeRefine),
3278 (0, CuStepPhase::AnytimeRefine),
3279 (1, CuStepPhase::Whole),
3280 ]
3281 );
3282 }
3283
3284 #[test]
3285 fn test_anytime_expansion_two_nodes_interleave() {
3286 let mut plan = CuExecutionLoop {
3289 steps: vec![
3290 manual_step(anytime_node("any_a", Some(2)), 0, &[], 0),
3291 manual_step(anytime_node("any_b", Some(2)), 1, &[], 1),
3292 manual_step(Node::new("consumer", "t"), 2, &[0, 1], 2),
3293 ],
3294 loop_count: None,
3295 };
3296 expand_anytime_steps(&mut plan).unwrap();
3297 assert_eq!(
3300 plan_shape(&plan),
3301 vec![
3302 (0, CuStepPhase::AnytimeBase),
3303 (0, CuStepPhase::AnytimeRefine),
3304 (1, CuStepPhase::AnytimeBase),
3305 (1, CuStepPhase::AnytimeRefine),
3306 (0, CuStepPhase::AnytimeRefine),
3307 (1, CuStepPhase::AnytimeRefine),
3308 (2, CuStepPhase::Whole),
3309 ]
3310 );
3311 }
3312
3313 #[test]
3314 fn test_anytime_expansion_requires_max_refines() {
3315 let mut plan = CuExecutionLoop {
3316 steps: vec![
3317 manual_step(anytime_node("any", None), 0, &[], 0),
3318 manual_step(Node::new("consumer", "t"), 1, &[0], 1),
3319 ],
3320 loop_count: None,
3321 };
3322 let err = expand_anytime_steps(&mut plan).unwrap_err();
3323 assert!(err.to_string().contains("needs anytime.max_refines"));
3324 }
3325}